Repository: GoogleContainerTools/jib Branch: master Commit: 24b44450133f Files: 1151 Total size: 4.0 MB Directory structure: gitextract_7d1c7m1x/ ├── .allstar/ │ └── binary_artifacts.yaml ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ └── issue_report.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── RELEASE_TEMPLATES/ │ │ ├── cli_release_checklist.md │ │ ├── core_release_checklist.md │ │ └── plugin_release_checklist.md │ ├── dependabot.yml │ └── workflows/ │ ├── gradle-wrapper-validation.yml │ ├── jib-cli-release.yml │ ├── prepare-release.yml │ ├── sonar.yml │ └── unit-tests.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── STYLE_GUIDE.md ├── build.gradle ├── config/ │ └── checkstyle/ │ ├── checkstyle-suppressions.xml │ └── copyright-java.header ├── docs/ │ ├── configure-gcp-credentials.md │ ├── default_base_image.md │ ├── faq.md │ ├── google-cloud-build.md │ ├── privacy.md │ └── self_sign_cert.md ├── examples/ │ ├── README.md │ ├── dropwizard/ │ │ ├── .mvn/ │ │ │ └── wrapper/ │ │ │ ├── MavenWrapperDownloader.java │ │ │ └── maven-wrapper.properties │ │ ├── README.md │ │ ├── mvnw │ │ ├── mvnw.cmd │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── example/ │ │ │ ├── JibExampleApplication.java │ │ │ ├── JibExampleConfiguration.java │ │ │ ├── api/ │ │ │ │ └── Saying.java │ │ │ ├── config/ │ │ │ │ └── HelloWorldConfiguration.java │ │ │ ├── health/ │ │ │ │ └── TemplateHealthCheck.java │ │ │ └── resources/ │ │ │ └── HelloWorldResource.java │ │ └── resources/ │ │ ├── banner.txt │ │ └── dropwizard.yml │ ├── helloworld/ │ │ ├── README.md │ │ ├── build.gradle │ │ ├── gradle/ │ │ │ └── wrapper/ │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ │ ├── gradlew │ │ ├── gradlew.bat │ │ ├── pom.xml │ │ ├── settings.gradle │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── example/ │ │ │ └── HelloWorld.java │ │ └── resources/ │ │ └── world │ ├── java-agent/ │ │ ├── README.md │ │ ├── build.gradle │ │ ├── gradle/ │ │ │ └── wrapper/ │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ │ ├── gradle.properties │ │ ├── gradlew │ │ ├── gradlew.bat │ │ ├── pom.xml │ │ ├── settings.gradle │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── example/ │ │ │ └── HelloWorld.java │ │ └── resources/ │ │ └── world │ ├── ktor/ │ │ ├── README.md │ │ ├── build.gradle.kts │ │ ├── gradle/ │ │ │ └── wrapper/ │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ │ ├── gradle.properties │ │ ├── gradlew │ │ ├── gradlew.bat │ │ ├── settings.gradle.kts │ │ └── src/ │ │ └── main/ │ │ ├── kotlin/ │ │ │ └── example/ │ │ │ └── ktor/ │ │ │ └── App.kt │ │ └── resources/ │ │ ├── application.conf │ │ └── logback.xml │ ├── micronaut/ │ │ ├── README.md │ │ ├── build.gradle │ │ ├── gradle/ │ │ │ └── wrapper/ │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ │ ├── gradle.properties │ │ ├── gradlew │ │ ├── gradlew.bat │ │ ├── settings.gradle │ │ └── src/ │ │ ├── main/ │ │ │ ├── groovy/ │ │ │ │ └── example/ │ │ │ │ └── micronaut/ │ │ │ │ ├── Application.groovy │ │ │ │ └── HelloController.groovy │ │ │ └── resources/ │ │ │ ├── application.yml │ │ │ └── logback.xml │ │ └── test/ │ │ └── groovy/ │ │ └── example/ │ │ └── micronaut/ │ │ └── HelloControllerSpec.groovy │ ├── multi-module/ │ │ ├── .mvn/ │ │ │ └── wrapper/ │ │ │ ├── MavenWrapperDownloader.java │ │ │ └── maven-wrapper.properties │ │ ├── README.md │ │ ├── build.gradle │ │ ├── gradle/ │ │ │ └── wrapper/ │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ │ ├── gradle-build.sh │ │ ├── gradlew │ │ ├── gradlew.bat │ │ ├── hello-service/ │ │ │ ├── build.gradle │ │ │ ├── gradle.properties │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── hello/ │ │ │ ├── Application.java │ │ │ └── HelloController.java │ │ ├── kubernetes.yaml │ │ ├── maven-build.sh │ │ ├── mvnw │ │ ├── mvnw.cmd │ │ ├── name-service/ │ │ │ ├── build.gradle │ │ │ ├── gradle.properties │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── name/ │ │ │ ├── Application.java │ │ │ └── NameController.java │ │ ├── pom.xml │ │ ├── settings.gradle │ │ └── shared-library/ │ │ ├── build.gradle │ │ ├── gradle.properties │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── common/ │ │ └── SharedUtils.java │ ├── spring-boot/ │ │ ├── .mvn/ │ │ │ └── wrapper/ │ │ │ ├── MavenWrapperDownloader.java │ │ │ └── maven-wrapper.properties │ │ ├── README.md │ │ ├── build.gradle │ │ ├── gradle/ │ │ │ └── wrapper/ │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ │ ├── gradlew │ │ ├── gradlew.bat │ │ ├── mvnw │ │ ├── mvnw.cmd │ │ ├── pom.xml │ │ ├── settings.gradle │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── hello/ │ │ ├── Application.java │ │ └── HelloController.java │ └── vertx/ │ ├── README.md │ ├── build.gradle │ ├── gradle/ │ │ └── wrapper/ │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ ├── settings.gradle │ └── src/ │ └── main/ │ ├── java/ │ │ └── example/ │ │ └── vertx/ │ │ └── MainVerticle.java │ └── resources/ │ └── logback.xml ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── jib-build-plan/ │ ├── CHANGELOG.md │ ├── build.gradle │ ├── gradle.properties │ ├── kokoro/ │ │ └── release_build.sh │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── google/ │ │ └── cloud/ │ │ └── tools/ │ │ └── jib/ │ │ ├── api/ │ │ │ └── buildplan/ │ │ │ ├── AbsoluteUnixPath.java │ │ │ ├── ContainerBuildPlan.java │ │ │ ├── FileEntriesLayer.java │ │ │ ├── FileEntry.java │ │ │ ├── FilePermissions.java │ │ │ ├── FilePermissionsProvider.java │ │ │ ├── ImageFormat.java │ │ │ ├── LayerObject.java │ │ │ ├── ModificationTimeProvider.java │ │ │ ├── OwnershipProvider.java │ │ │ ├── Platform.java │ │ │ ├── Port.java │ │ │ └── RelativeUnixPath.java │ │ └── buildplan/ │ │ └── UnixPathParser.java │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── google/ │ │ └── cloud/ │ │ └── tools/ │ │ └── jib/ │ │ ├── api/ │ │ │ └── buildplan/ │ │ │ ├── AbsoluteUnixPathTest.java │ │ │ ├── ContainerBuildPlanTest.java │ │ │ ├── FileEntriesLayerTest.java │ │ │ ├── FileEntryTest.java │ │ │ ├── FilePermissionsTest.java │ │ │ ├── PortTest.java │ │ │ └── RelativeUnixPathTest.java │ │ └── buildplan/ │ │ └── UnixPathParserTest.java │ └── resources/ │ └── core/ │ ├── fileA │ └── layer/ │ ├── a/ │ │ └── b/ │ │ └── bar │ ├── c/ │ │ └── cat │ └── foo ├── jib-cli/ │ ├── CHANGELOG.md │ ├── README.md │ ├── build.gradle │ ├── gradle.properties │ ├── scripts/ │ │ └── update_gcs_latest.sh │ └── src/ │ ├── integration-test/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── google/ │ │ │ └── cloud/ │ │ │ └── tools/ │ │ │ └── jib/ │ │ │ └── cli/ │ │ │ ├── JarCommandTest.java │ │ │ ├── TestProject.java │ │ │ └── WarCommandTest.java │ │ └── resources/ │ │ ├── jarTest/ │ │ │ ├── spring-boot/ │ │ │ │ ├── build-layered.gradle │ │ │ │ ├── build.gradle │ │ │ │ ├── gradle/ │ │ │ │ │ └── wrapper/ │ │ │ │ │ ├── gradle-wrapper.jar │ │ │ │ │ └── gradle-wrapper.properties │ │ │ │ ├── gradlew │ │ │ │ ├── gradlew.bat │ │ │ │ ├── settings-layered.gradle │ │ │ │ ├── settings.gradle │ │ │ │ └── src/ │ │ │ │ └── main/ │ │ │ │ └── java/ │ │ │ │ └── hello/ │ │ │ │ ├── Application.java │ │ │ │ └── HelloController.java │ │ │ └── standard/ │ │ │ ├── HelloWorld.java │ │ │ ├── dep/ │ │ │ │ └── A.java │ │ │ └── dep2/ │ │ │ └── B.java │ │ └── warTest/ │ │ ├── build.gradle │ │ ├── gradle/ │ │ │ └── wrapper/ │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ │ ├── gradlew │ │ ├── gradlew.bat │ │ ├── settings.gradle │ │ └── src/ │ │ ├── extra_js/ │ │ │ └── bogus.js │ │ ├── extra_static/ │ │ │ └── bogus.html │ │ └── main/ │ │ ├── java/ │ │ │ └── example/ │ │ │ └── HelloWorld.java │ │ ├── resources/ │ │ │ └── world │ │ └── webapp/ │ │ ├── META-INF/ │ │ │ └── MANIFEST.MF │ │ ├── WEB-INF/ │ │ │ └── web.xml │ │ └── index.html │ ├── java-templates/ │ │ └── com/ │ │ └── google/ │ │ └── cloud/ │ │ └── tools/ │ │ └── jib/ │ │ └── cli/ │ │ └── VersionInfo.java.template │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── google/ │ │ └── cloud/ │ │ └── tools/ │ │ └── jib/ │ │ └── cli/ │ │ ├── ArtifactLayers.java │ │ ├── ArtifactProcessor.java │ │ ├── ArtifactProcessors.java │ │ ├── Build.java │ │ ├── CacheDirectories.java │ │ ├── CommonCliOptions.java │ │ ├── CommonContainerConfigCliOptions.java │ │ ├── ContainerBuilders.java │ │ ├── Containerizers.java │ │ ├── Credentials.java │ │ ├── Instants.java │ │ ├── Jar.java │ │ ├── JibCli.java │ │ ├── ShortErrorMessageHandler.java │ │ ├── War.java │ │ ├── buildfile/ │ │ │ ├── ArchiveLayerSpec.java │ │ │ ├── BaseImageSpec.java │ │ │ ├── BuildFileSpec.java │ │ │ ├── BuildFiles.java │ │ │ ├── CopySpec.java │ │ │ ├── FileLayerSpec.java │ │ │ ├── FilePropertiesSpec.java │ │ │ ├── FilePropertiesStack.java │ │ │ ├── LayerSpec.java │ │ │ ├── Layers.java │ │ │ ├── LayersSpec.java │ │ │ ├── PlatformSpec.java │ │ │ └── Validator.java │ │ ├── jar/ │ │ │ ├── JarFiles.java │ │ │ ├── JarLayers.java │ │ │ ├── ProcessingMode.java │ │ │ ├── SpringBootExplodedProcessor.java │ │ │ ├── SpringBootPackagedProcessor.java │ │ │ ├── StandardExplodedProcessor.java │ │ │ └── StandardPackagedProcessor.java │ │ ├── logging/ │ │ │ ├── CliLogger.java │ │ │ ├── ConsoleOutput.java │ │ │ ├── HttpTraceLevel.java │ │ │ └── Verbosity.java │ │ └── war/ │ │ ├── StandardWarExplodedProcessor.java │ │ └── WarFiles.java │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── google/ │ │ └── cloud/ │ │ └── tools/ │ │ └── jib/ │ │ ├── ArtifactProcessorsTest.java │ │ ├── api/ │ │ │ ├── ContainerizerTestProxy.java │ │ │ ├── HttpRequestTester.java │ │ │ └── JibContainerBuilderTestHelper.java │ │ └── cli/ │ │ ├── BuildTest.java │ │ ├── CacheDirectoriesTest.java │ │ ├── ContainerBuildersTest.java │ │ ├── ContainerizersTest.java │ │ ├── CredentialsTest.java │ │ ├── InstantsTest.java │ │ ├── JarTest.java │ │ ├── JibCliTest.java │ │ ├── WarTest.java │ │ ├── buildfile/ │ │ │ ├── ArchiveLayerSpecTest.java │ │ │ ├── BaseImageSpecTest.java │ │ │ ├── BuildFileSpecTest.java │ │ │ ├── BuildFilesTest.java │ │ │ ├── CopySpecTest.java │ │ │ ├── FileLayerSpecTest.java │ │ │ ├── FilePropertiesSpecTest.java │ │ │ ├── FilePropertiesStackTest.java │ │ │ ├── LayerSpecTest.java │ │ │ ├── LayersSpecTest.java │ │ │ ├── LayersTest.java │ │ │ ├── PlatformSpecTest.java │ │ │ └── ValidatorTest.java │ │ ├── jar/ │ │ │ ├── JarFilesTest.java │ │ │ ├── SpringBootExplodedProcessorTest.java │ │ │ ├── SpringBootPackagedProcessorTest.java │ │ │ ├── StandardExplodedProcessorTest.java │ │ │ └── StandardPackagedProcessorTest.java │ │ ├── logging/ │ │ │ └── CliLoggerTest.java │ │ └── war/ │ │ ├── StandardWarExplodedProcessorTest.java │ │ └── WarFilesTest.java │ └── resources/ │ ├── buildfiles/ │ │ ├── layers/ │ │ │ ├── archiveLayerTest/ │ │ │ │ └── layers.yaml │ │ │ ├── fileTest/ │ │ │ │ ├── default/ │ │ │ │ │ ├── layers.yaml │ │ │ │ │ ├── toDir.txt │ │ │ │ │ └── toFile.txt │ │ │ │ ├── failWithExcludes/ │ │ │ │ │ ├── layers.yaml │ │ │ │ │ └── toFile.txt │ │ │ │ └── failWithIncludes/ │ │ │ │ ├── layers.yaml │ │ │ │ └── toFile.txt │ │ │ ├── includesExcludesTest/ │ │ │ │ ├── layers.yaml │ │ │ │ └── project/ │ │ │ │ ├── excludedDir/ │ │ │ │ │ └── exclude.me │ │ │ │ ├── includedDir/ │ │ │ │ │ └── include.me │ │ │ │ └── wild.card │ │ │ ├── pathDoesNotExist/ │ │ │ │ └── layers.yaml │ │ │ ├── propertiesTest/ │ │ │ │ ├── dir/ │ │ │ │ │ └── file.txt │ │ │ │ └── layers.yaml │ │ │ └── writeToRoot/ │ │ │ ├── dir/ │ │ │ │ └── file.txt │ │ │ └── layers.yaml │ │ └── projects/ │ │ ├── allDefaults/ │ │ │ └── jib.yaml │ │ ├── allProperties/ │ │ │ ├── altYamls/ │ │ │ │ └── alt-jib.yaml │ │ │ ├── jib.yaml │ │ │ └── project/ │ │ │ └── script.sh │ │ └── templating/ │ │ ├── missingVar.yaml │ │ ├── multiLine.yaml │ │ └── valid.yaml │ ├── jar/ │ │ ├── java18.jar │ │ ├── spring-boot/ │ │ │ ├── springboot_layered.jar │ │ │ ├── springboot_layered_allEmptyLayers.jar │ │ │ ├── springboot_layered_singleEmptyLayer.jar │ │ │ ├── springboot_notLayered.jar │ │ │ └── springboot_sample.jar │ │ └── standard/ │ │ ├── dependency1 │ │ ├── dependency2 │ │ ├── dependency3-SNAPSHOT-1.jar │ │ ├── directory/ │ │ │ └── dependency4 │ │ ├── emptyStandardJar.jar │ │ ├── jarWithInvalidClass.jar │ │ ├── singleDepJar.jar │ │ ├── standardJarWithClassPath.jar │ │ ├── standardJarWithOnlyClasses.jar │ │ └── standardJarWithoutClassPath.jar │ └── war/ │ └── standard/ │ ├── allLayers/ │ │ ├── META-INF/ │ │ │ └── context.xml │ │ ├── Test.jsp │ │ └── WEB-INF/ │ │ ├── classes/ │ │ │ └── package/ │ │ │ └── test.properties │ │ ├── lib/ │ │ │ ├── dependency-1.0.0.jar │ │ │ └── dependencyX-1.0.0-SNAPSHOT.jar │ │ └── web.xml │ ├── noWebInfClasses/ │ │ ├── META-INF/ │ │ │ └── context.xml │ │ └── WEB-INF/ │ │ └── lib/ │ │ ├── dependency-1.0.0.jar │ │ └── dependencyX-1.0.0-SNAPSHOT.jar │ └── noWebInfLib/ │ └── META-INF/ │ └── context.xml ├── jib-core/ │ ├── CHANGELOG.md │ ├── README.md │ ├── build.gradle │ ├── examples/ │ │ ├── README.md │ │ └── build.gradle/ │ │ └── README.md │ ├── gradle.properties │ ├── kokoro/ │ │ └── release_build.sh │ └── src/ │ ├── integration-test/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── google/ │ │ │ └── cloud/ │ │ │ └── tools/ │ │ │ └── jib/ │ │ │ ├── Command.java │ │ │ ├── IntegrationTestingConfiguration.java │ │ │ ├── api/ │ │ │ │ ├── ContainerizerIntegrationTest.java │ │ │ │ ├── JibIntegrationTest.java │ │ │ │ ├── JibMultiPlatformIntegrationTest.java │ │ │ │ └── ReproducibleImageTest.java │ │ │ └── registry/ │ │ │ ├── BearerAuthenticationIntegrationTest.java │ │ │ ├── BlobCheckerIntegrationTest.java │ │ │ ├── BlobPullerIntegrationTest.java │ │ │ ├── BlobPusherIntegrationTest.java │ │ │ ├── LocalRegistry.java │ │ │ ├── ManifestCheckerIntegrationTest.java │ │ │ ├── ManifestPullerIntegrationTest.java │ │ │ ├── ManifestPusherIntegrationTest.java │ │ │ └── credentials/ │ │ │ └── DockerCredentialHelperIntegrationTest.java │ │ └── resources/ │ │ ├── core/ │ │ │ └── hello │ │ └── credentials.json │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── google/ │ │ │ └── cloud/ │ │ │ └── tools/ │ │ │ └── jib/ │ │ │ ├── ProjectInfo.java │ │ │ ├── api/ │ │ │ │ ├── CacheDirectoryCreationException.java │ │ │ │ ├── Containerizer.java │ │ │ │ ├── Credential.java │ │ │ │ ├── CredentialRetriever.java │ │ │ │ ├── DescriptorDigest.java │ │ │ │ ├── DockerClient.java │ │ │ │ ├── DockerDaemonImage.java │ │ │ │ ├── DockerInfoDetails.java │ │ │ │ ├── ImageDetails.java │ │ │ │ ├── ImageReference.java │ │ │ │ ├── InsecureRegistryException.java │ │ │ │ ├── InvalidImageReferenceException.java │ │ │ │ ├── JavaContainerBuilder.java │ │ │ │ ├── Jib.java │ │ │ │ ├── JibContainer.java │ │ │ │ ├── JibContainerBuilder.java │ │ │ │ ├── JibContainerDescription.java │ │ │ │ ├── JibEvent.java │ │ │ │ ├── LayerConfiguration.java │ │ │ │ ├── LayerEntry.java │ │ │ │ ├── LogEvent.java │ │ │ │ ├── MainClassFinder.java │ │ │ │ ├── Ports.java │ │ │ │ ├── RegistryAuthenticationFailedException.java │ │ │ │ ├── RegistryException.java │ │ │ │ ├── RegistryImage.java │ │ │ │ ├── RegistryUnauthorizedException.java │ │ │ │ └── TarImage.java │ │ │ ├── blob/ │ │ │ │ ├── Blob.java │ │ │ │ ├── BlobDescriptor.java │ │ │ │ ├── Blobs.java │ │ │ │ ├── FileBlob.java │ │ │ │ ├── InputStreamBlob.java │ │ │ │ ├── JsonBlob.java │ │ │ │ ├── StringBlob.java │ │ │ │ └── WritableContentsBlob.java │ │ │ ├── builder/ │ │ │ │ ├── ProgressEventDispatcher.java │ │ │ │ ├── Timer.java │ │ │ │ ├── TimerEventDispatcher.java │ │ │ │ └── steps/ │ │ │ │ ├── AuthenticatePushStep.java │ │ │ │ ├── BuildAndCacheApplicationLayerStep.java │ │ │ │ ├── BuildImageStep.java │ │ │ │ ├── BuildManifestListOrSingleManifestStep.java │ │ │ │ ├── BuildResult.java │ │ │ │ ├── CheckManifestStep.java │ │ │ │ ├── LoadDockerStep.java │ │ │ │ ├── LocalBaseImageSteps.java │ │ │ │ ├── ObtainBaseImageLayerStep.java │ │ │ │ ├── PlatformChecker.java │ │ │ │ ├── PreparedLayer.java │ │ │ │ ├── PullBaseImageStep.java │ │ │ │ ├── PushBlobStep.java │ │ │ │ ├── PushContainerConfigurationStep.java │ │ │ │ ├── PushImageStep.java │ │ │ │ ├── PushLayerStep.java │ │ │ │ ├── RegistryCredentialRetriever.java │ │ │ │ ├── StepsRunner.java │ │ │ │ ├── ThrottledProgressEventDispatcherWrapper.java │ │ │ │ └── WriteTarFileStep.java │ │ │ ├── cache/ │ │ │ │ ├── Cache.java │ │ │ │ ├── CacheCorruptedException.java │ │ │ │ ├── CacheStorageFiles.java │ │ │ │ ├── CacheStorageReader.java │ │ │ │ ├── CacheStorageWriter.java │ │ │ │ ├── CachedLayer.java │ │ │ │ ├── LayerEntriesSelector.java │ │ │ │ └── Retry.java │ │ │ ├── configuration/ │ │ │ │ ├── BuildContext.java │ │ │ │ ├── ContainerConfiguration.java │ │ │ │ ├── DockerHealthCheck.java │ │ │ │ └── ImageConfiguration.java │ │ │ ├── docker/ │ │ │ │ ├── CliDockerClient.java │ │ │ │ ├── DockerClientResolver.java │ │ │ │ └── json/ │ │ │ │ └── DockerManifestEntryTemplate.java │ │ │ ├── event/ │ │ │ │ ├── EventHandlers.java │ │ │ │ ├── Handler.java │ │ │ │ ├── events/ │ │ │ │ │ ├── ProgressEvent.java │ │ │ │ │ └── TimerEvent.java │ │ │ │ └── progress/ │ │ │ │ ├── Allocation.java │ │ │ │ ├── AllocationCompletionTracker.java │ │ │ │ ├── ProgressEventHandler.java │ │ │ │ └── ThrottledAccumulatingConsumer.java │ │ │ ├── filesystem/ │ │ │ │ ├── DirectoryWalker.java │ │ │ │ ├── FileOperations.java │ │ │ │ ├── LockFile.java │ │ │ │ ├── PathConsumer.java │ │ │ │ ├── TempDirectoryProvider.java │ │ │ │ └── XdgDirectories.java │ │ │ ├── frontend/ │ │ │ │ └── CredentialRetrieverFactory.java │ │ │ ├── global/ │ │ │ │ └── JibSystemProperties.java │ │ │ ├── hash/ │ │ │ │ ├── CountingDigestOutputStream.java │ │ │ │ ├── Digests.java │ │ │ │ └── WritableContents.java │ │ │ ├── http/ │ │ │ │ ├── Authorization.java │ │ │ │ ├── BlobHttpContent.java │ │ │ │ ├── FailoverHttpClient.java │ │ │ │ ├── NotifyingOutputStream.java │ │ │ │ ├── Request.java │ │ │ │ ├── Response.java │ │ │ │ └── ResponseException.java │ │ │ ├── image/ │ │ │ │ ├── DigestOnlyLayer.java │ │ │ │ ├── Image.java │ │ │ │ ├── ImageTarball.java │ │ │ │ ├── Layer.java │ │ │ │ ├── LayerCountMismatchException.java │ │ │ │ ├── LayerPropertyNotFoundException.java │ │ │ │ ├── ReferenceLayer.java │ │ │ │ ├── ReferenceNoDiffIdLayer.java │ │ │ │ ├── ReproducibleLayerBuilder.java │ │ │ │ └── json/ │ │ │ │ ├── BadContainerConfigurationFormatException.java │ │ │ │ ├── BuildableManifestTemplate.java │ │ │ │ ├── ContainerConfigurationTemplate.java │ │ │ │ ├── DescriptorDigestDeserializer.java │ │ │ │ ├── DescriptorDigestSerializer.java │ │ │ │ ├── HistoryEntry.java │ │ │ │ ├── ImageMetadataTemplate.java │ │ │ │ ├── ImageToJsonTranslator.java │ │ │ │ ├── JsonToImageTranslator.java │ │ │ │ ├── ManifestAndConfigTemplate.java │ │ │ │ ├── ManifestListGenerator.java │ │ │ │ ├── ManifestListTemplate.java │ │ │ │ ├── ManifestTemplate.java │ │ │ │ ├── OciIndexTemplate.java │ │ │ │ ├── OciManifestTemplate.java │ │ │ │ ├── PlatformNotFoundInBaseImageException.java │ │ │ │ ├── UnknownManifestFormatException.java │ │ │ │ ├── UnlistedPlatformInManifestListException.java │ │ │ │ ├── V21ManifestTemplate.java │ │ │ │ ├── V22ManifestListTemplate.java │ │ │ │ └── V22ManifestTemplate.java │ │ │ ├── json/ │ │ │ │ ├── JsonTemplate.java │ │ │ │ └── JsonTemplateMapper.java │ │ │ ├── registry/ │ │ │ │ ├── AbstractManifestPuller.java │ │ │ │ ├── AuthenticationMethodRetriever.java │ │ │ │ ├── BlobChecker.java │ │ │ │ ├── BlobPuller.java │ │ │ │ ├── BlobPusher.java │ │ │ │ ├── ErrorCodes.java │ │ │ │ ├── ErrorResponseUtil.java │ │ │ │ ├── ManifestAndDigest.java │ │ │ │ ├── ManifestChecker.java │ │ │ │ ├── ManifestPuller.java │ │ │ │ ├── ManifestPusher.java │ │ │ │ ├── RegistryAliasGroup.java │ │ │ │ ├── RegistryAuthenticator.java │ │ │ │ ├── RegistryClient.java │ │ │ │ ├── RegistryCredentialsNotSentException.java │ │ │ │ ├── RegistryEndpointCaller.java │ │ │ │ ├── RegistryEndpointProvider.java │ │ │ │ ├── RegistryEndpointRequestProperties.java │ │ │ │ ├── RegistryErrorException.java │ │ │ │ ├── RegistryErrorExceptionBuilder.java │ │ │ │ ├── UnexpectedBlobDigestException.java │ │ │ │ ├── credentials/ │ │ │ │ │ ├── CredentialHelperNotFoundException.java │ │ │ │ │ ├── CredentialHelperUnhandledServerUrlException.java │ │ │ │ │ ├── CredentialRetrievalException.java │ │ │ │ │ ├── DockerConfig.java │ │ │ │ │ ├── DockerConfigCredentialRetriever.java │ │ │ │ │ ├── DockerCredentialHelper.java │ │ │ │ │ └── json/ │ │ │ │ │ └── DockerConfigTemplate.java │ │ │ │ └── json/ │ │ │ │ ├── ErrorEntryTemplate.java │ │ │ │ └── ErrorResponseTemplate.java │ │ │ └── tar/ │ │ │ ├── TarExtractor.java │ │ │ └── TarStreamBuilder.java │ │ └── resources/ │ │ └── commons-logging.properties │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── google/ │ │ └── cloud/ │ │ └── tools/ │ │ └── jib/ │ │ ├── MultithreadedExecutor.java │ │ ├── api/ │ │ │ ├── ContainerizerTest.java │ │ │ ├── CredentialTest.java │ │ │ ├── DescriptorDigestTest.java │ │ │ ├── DockerClientResolverTest.java │ │ │ ├── DockerDaemonImageTest.java │ │ │ ├── ImageReferenceTest.java │ │ │ ├── JavaContainerBuilderTest.java │ │ │ ├── JibContainerBuilderTest.java │ │ │ ├── JibContainerTest.java │ │ │ ├── MainClassFinderTest.java │ │ │ ├── PortsTest.java │ │ │ ├── RegistryImageTest.java │ │ │ └── TarImageTest.java │ │ ├── blob/ │ │ │ └── BlobTest.java │ │ ├── builder/ │ │ │ ├── ProgressEventDispatcherTest.java │ │ │ ├── TimerEventDispatcherTest.java │ │ │ ├── TimerTest.java │ │ │ └── steps/ │ │ │ ├── BuildAndCacheApplicationLayerStepTest.java │ │ │ ├── BuildImageStepTest.java │ │ │ ├── BuildManifestListOrSingleManifestStepTest.java │ │ │ ├── BuildResultTest.java │ │ │ ├── LocalBaseImageStepsTest.java │ │ │ ├── ObtainBaseImageLayerStepTest.java │ │ │ ├── PlatformCheckerTest.java │ │ │ ├── PullBaseImageStepTest.java │ │ │ ├── PushBlobStepTest.java │ │ │ ├── PushImageStepTest.java │ │ │ ├── RegistryCredentialRetrieverTest.java │ │ │ └── StepsRunnerTest.java │ │ ├── cache/ │ │ │ ├── CacheStorageFilesTest.java │ │ │ ├── CacheStorageReaderTest.java │ │ │ ├── CacheStorageWriterTest.java │ │ │ ├── CacheTest.java │ │ │ ├── CachedLayerTest.java │ │ │ ├── LayerEntriesSelectorTest.java │ │ │ └── RetryTest.java │ │ ├── configuration/ │ │ │ ├── BuildContextTest.java │ │ │ ├── ContainerConfigurationTest.java │ │ │ └── DockerHealthCheckTest.java │ │ ├── docker/ │ │ │ ├── AnotherDockerClient.java │ │ │ ├── CliDockerClientTest.java │ │ │ └── json/ │ │ │ └── DockerManifestEntryTemplateTest.java │ │ ├── event/ │ │ │ ├── EventHandlersTest.java │ │ │ ├── events/ │ │ │ │ ├── LogEventTest.java │ │ │ │ └── ProgressEventTest.java │ │ │ └── progress/ │ │ │ ├── AllocationCompletionTrackerTest.java │ │ │ ├── AllocationTest.java │ │ │ └── ProgressEventHandlerTest.java │ │ ├── filesystem/ │ │ │ ├── DirectoryWalkerTest.java │ │ │ ├── FileOperationsTest.java │ │ │ ├── LockFileTest.java │ │ │ ├── TempDirectoryProviderTest.java │ │ │ └── XdgDirectoriesTest.java │ │ ├── frontend/ │ │ │ └── CredentialRetrieverFactoryTest.java │ │ ├── global/ │ │ │ └── JibSystemPropertiesTest.java │ │ ├── hash/ │ │ │ └── CountingDigestOutputStreamTest.java │ │ ├── http/ │ │ │ ├── FailoverHttpClientTest.java │ │ │ ├── NotifyingOutputStreamTest.java │ │ │ ├── RequestTest.java │ │ │ ├── RequestWrapper.java │ │ │ ├── ResponseTest.java │ │ │ ├── TestWebServer.java │ │ │ └── WithServerFailoverHttpClientTest.java │ │ ├── image/ │ │ │ ├── ImageTarballTest.java │ │ │ ├── ImageTest.java │ │ │ ├── LayerTest.java │ │ │ ├── ReproducibleLayerBuilderTest.java │ │ │ └── json/ │ │ │ ├── ContainerConfigurationTemplateTest.java │ │ │ ├── ImageToJsonTranslatorTest.java │ │ │ ├── JsonToImageTranslatorTest.java │ │ │ ├── ManifestListGeneratorTest.java │ │ │ ├── OciIndexTemplateTest.java │ │ │ ├── OciManifestTemplateTest.java │ │ │ ├── V21ManifestTemplateTest.java │ │ │ ├── V22ManifestListTemplateTest.java │ │ │ └── V22ManifestTemplateTest.java │ │ ├── json/ │ │ │ └── JsonTemplateMapperTest.java │ │ ├── registry/ │ │ │ ├── AuthenticationMethodRetrieverTest.java │ │ │ ├── BlobCheckerTest.java │ │ │ ├── BlobPullerTest.java │ │ │ ├── BlobPusherTest.java │ │ │ ├── DockerRegistryBearerTokenTest.java │ │ │ ├── ErrorResponseUtilTest.java │ │ │ ├── ManifestPullerTest.java │ │ │ ├── ManifestPusherTest.java │ │ │ ├── PlainHttpClient.java │ │ │ ├── RegistryAliasGroupTest.java │ │ │ ├── RegistryAuthenticationFailedExceptionTest.java │ │ │ ├── RegistryAuthenticatorTest.java │ │ │ ├── RegistryClientTest.java │ │ │ ├── RegistryEndpointCallerTest.java │ │ │ ├── RegistryErrorExceptionBuilderTest.java │ │ │ └── credentials/ │ │ │ ├── DockerConfigCredentialRetrieverTest.java │ │ │ ├── DockerConfigTest.java │ │ │ └── DockerCredentialHelperTest.java │ │ └── tar/ │ │ ├── TarExtractorTest.java │ │ └── TarStreamBuilderTest.java │ └── resources/ │ ├── META-INF/ │ │ └── services/ │ │ └── com.google.cloud.tools.jib.api.DockerClient │ ├── core/ │ │ ├── TestWebServer-keystore │ │ ├── application/ │ │ │ ├── dependencies/ │ │ │ │ ├── dependency-1.0.0.jar │ │ │ │ ├── libraryA.jar │ │ │ │ ├── libraryB.jar │ │ │ │ └── more/ │ │ │ │ └── dependency-1.0.0.jar │ │ │ ├── resources/ │ │ │ │ ├── resourceA │ │ │ │ ├── resourceB │ │ │ │ └── world │ │ │ └── snapshot-dependencies/ │ │ │ └── dependency-1.0.0-SNAPSHOT.jar │ │ ├── blobA │ │ ├── class-finder-tests/ │ │ │ └── simple/ │ │ │ └── NotEvenAClass.txt │ │ ├── directoryA/ │ │ │ └── .gitkeep │ │ ├── docker/ │ │ │ └── emptyFile │ │ ├── extraction/ │ │ │ └── test-cache/ │ │ │ └── local/ │ │ │ ├── 5e701122d3347fae0758cd5b7f0692c686fcd07b0e7fd9c4a125fbdbbedc04dd/ │ │ │ │ └── 0011328ac5dfe3dde40c7c5e0e00c98d1833a3aeae2bfb668cf9eb965c229c7f │ │ │ ├── config/ │ │ │ │ └── 066872f17ae819f846a6d5abcfc3165abe13fb0a157640fa8cb7af81077670c0 │ │ │ └── f1ac3015bcbf0ada4750d728626eb10f0f585199e2b667dcd79e49f0e926178e/ │ │ │ └── c10ef24a5cef5092bbcb5a5666721cff7b86ce978c203a958d1fc86ee6c19f94 │ │ ├── fileA │ │ ├── fileB │ │ ├── json/ │ │ │ ├── basic.json │ │ │ ├── basic_list.json │ │ │ ├── containerconfig.json │ │ │ ├── dockerconfig.json │ │ │ ├── dockerconfig_extra_matches.json │ │ │ ├── dockerconfig_identity_token.json │ │ │ ├── dockerconfig_index_docker_io_v1.json │ │ │ ├── legacy_dockercfg │ │ │ ├── loadmanifest.json │ │ │ ├── loadmanifest2.json │ │ │ ├── metadata-v2.json │ │ │ ├── metadata-v3.json │ │ │ ├── metadata.json │ │ │ ├── metadata_corrupted.json │ │ │ ├── metadata_windows-v2.json │ │ │ ├── metadata_windows.json │ │ │ ├── ociindex.json │ │ │ ├── ociindex_platforms.json │ │ │ ├── ocimanifest.json │ │ │ ├── translated_ocimanifest.json │ │ │ ├── translated_v22manifest.json │ │ │ ├── v21manifest.json │ │ │ ├── v22manifest.json │ │ │ ├── v22manifest_list.json │ │ │ └── v22manifest_optional_properties.json │ │ ├── layer/ │ │ │ ├── a/ │ │ │ │ └── b/ │ │ │ │ └── bar │ │ │ ├── c/ │ │ │ │ └── cat │ │ │ └── foo │ │ ├── random-contents/ │ │ │ ├── file1 │ │ │ ├── file2 │ │ │ └── sub-directory/ │ │ │ ├── file3 │ │ │ ├── file4 │ │ │ └── leaf/ │ │ │ ├── file5 │ │ │ └── file6 │ │ └── webAppSampleDockerfile │ └── mockito-extensions/ │ └── org.mockito.plugins.MockMaker ├── jib-gradle-plugin/ │ ├── CHANGELOG.md │ ├── README.md │ ├── build.gradle │ ├── gradle.properties │ ├── scripts/ │ │ ├── release.sh │ │ └── update_gcs_latest.sh │ └── src/ │ ├── integration-test/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── google/ │ │ │ └── cloud/ │ │ │ └── tools/ │ │ │ └── jib/ │ │ │ └── gradle/ │ │ │ ├── DefaultTargetProjectIntegrationTest.java │ │ │ ├── EmptyProjectIntegrationTest.java │ │ │ ├── GradleLayerConfigurationIntegrationTest.java │ │ │ ├── JibRunHelper.java │ │ │ ├── SingleProjectIntegrationTest.java │ │ │ ├── SpringBootProjectIntegrationTest.java │ │ │ └── WarProjectIntegrationTest.java │ │ └── resources/ │ │ └── gradle/ │ │ └── projects/ │ │ ├── default-target/ │ │ │ ├── build.gradle │ │ │ ├── gradle.properties │ │ │ ├── libs/ │ │ │ │ └── dependency-1.0.0.jar │ │ │ ├── settings.gradle │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── test/ │ │ │ │ └── HelloWorld.java │ │ │ └── resources/ │ │ │ └── world │ │ ├── empty/ │ │ │ ├── build-broken-user.gradle │ │ │ ├── build.gradle │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── test/ │ │ │ └── Empty.java │ │ ├── multiproject/ │ │ │ ├── a_packaged/ │ │ │ │ ├── build.gradle │ │ │ │ └── src/ │ │ │ │ └── main/ │ │ │ │ └── java/ │ │ │ │ └── com/ │ │ │ │ └── test/ │ │ │ │ └── Empty.java │ │ │ ├── b_dependency/ │ │ │ │ ├── build.gradle │ │ │ │ └── src/ │ │ │ │ └── main/ │ │ │ │ └── java/ │ │ │ │ └── com/ │ │ │ │ └── test/ │ │ │ │ └── Empty.java │ │ │ ├── build.gradle │ │ │ └── settings.gradle │ │ ├── simple/ │ │ │ ├── build-configuration.gradle │ │ │ ├── build-cred-helper.gradle │ │ │ ├── build-dockerclient.gradle │ │ │ ├── build-extra-dirs-filtering.gradle │ │ │ ├── build-extra-dirs.gradle │ │ │ ├── build-extra-dirs2.gradle │ │ │ ├── build-extra-dirs3.gradle │ │ │ ├── build-jar-containerization.gradle │ │ │ ├── build-java11-incompatible.gradle │ │ │ ├── build-java11.gradle │ │ │ ├── build-java17.gradle │ │ │ ├── build-local-base.gradle │ │ │ ├── build-multi-platform.gradle │ │ │ ├── build-timestamps-custom.gradle │ │ │ ├── build.gradle │ │ │ ├── complex-build.gradle │ │ │ ├── libs/ │ │ │ │ ├── dependency-1.0.0.jar │ │ │ │ ├── dependency2 │ │ │ │ └── dependency3 │ │ │ ├── mock-docker.sh │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── custom-extra-dir/ │ │ │ │ ├── bar/ │ │ │ │ │ └── cat │ │ │ │ └── foo │ │ │ ├── custom-extra-dir2/ │ │ │ │ └── baz │ │ │ ├── custom-extra-dir3/ │ │ │ │ ├── cat.json │ │ │ │ ├── cat.txt │ │ │ │ └── sub/ │ │ │ │ ├── a.json │ │ │ │ └── a.txt │ │ │ ├── custom-extra-dir4/ │ │ │ │ ├── bar │ │ │ │ └── foo │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── test/ │ │ │ │ └── HelloWorld.java │ │ │ └── resources/ │ │ │ └── world │ │ ├── spring-boot/ │ │ │ ├── build.gradle │ │ │ ├── settings.gradle │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── hello/ │ │ │ ├── Application.java │ │ │ └── HelloController.java │ │ └── war_servlet25/ │ │ ├── build-tomcat.gradle │ │ ├── build.gradle │ │ └── src/ │ │ ├── extra_js/ │ │ │ └── bogus.js │ │ ├── extra_static/ │ │ │ └── bogus.html │ │ └── main/ │ │ ├── java/ │ │ │ └── example/ │ │ │ └── HelloWorld.java │ │ ├── resources/ │ │ │ └── world │ │ └── webapp/ │ │ ├── META-INF/ │ │ │ └── MANIFEST.MF │ │ ├── WEB-INF/ │ │ │ └── web.xml │ │ └── index.html │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── google/ │ │ └── cloud/ │ │ └── tools/ │ │ └── jib/ │ │ └── gradle/ │ │ ├── AuthParameters.java │ │ ├── BaseImageParameters.java │ │ ├── BuildDockerTask.java │ │ ├── BuildImageTask.java │ │ ├── BuildTarTask.java │ │ ├── ContainerParameters.java │ │ ├── CredHelperParameters.java │ │ ├── DockerClientParameters.java │ │ ├── ExtensionParameters.java │ │ ├── ExtensionParametersSpec.java │ │ ├── ExtraDirectoriesParameters.java │ │ ├── ExtraDirectoryParameters.java │ │ ├── ExtraDirectoryParametersSpec.java │ │ ├── GradleHelpfulSuggestions.java │ │ ├── GradleProjectProperties.java │ │ ├── GradleRawConfiguration.java │ │ ├── JibExtension.java │ │ ├── JibPlugin.java │ │ ├── JibTask.java │ │ ├── OutputPathsParameters.java │ │ ├── PlatformParameters.java │ │ ├── PlatformParametersSpec.java │ │ ├── TargetImageParameters.java │ │ ├── TaskCommon.java │ │ └── skaffold/ │ │ ├── CheckJibVersionTask.java │ │ ├── FilesTaskV2.java │ │ ├── InitTask.java │ │ ├── SkaffoldParameters.java │ │ ├── SkaffoldSyncParameters.java │ │ ├── SkaffoldWatchParameters.java │ │ └── SyncMapTask.java │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── google/ │ │ └── cloud/ │ │ └── tools/ │ │ └── jib/ │ │ └── gradle/ │ │ ├── GradleProjectPropertiesExtensionTest.java │ │ ├── GradleProjectPropertiesTest.java │ │ ├── GradleRawConfigurationTest.java │ │ ├── JibExtensionTest.java │ │ ├── JibPluginTest.java │ │ ├── TaskCommonTest.java │ │ ├── TestProject.java │ │ └── skaffold/ │ │ ├── FilesTaskV2Test.java │ │ ├── InitTaskTest.java │ │ └── SyncMapTaskTest.java │ └── resources/ │ └── gradle/ │ ├── application/ │ │ ├── build/ │ │ │ └── resources/ │ │ │ └── main/ │ │ │ ├── resourceA │ │ │ ├── resourceB │ │ │ └── world │ │ ├── dependencies/ │ │ │ ├── another/ │ │ │ │ └── one/ │ │ │ │ └── dependency-1.0.0.jar │ │ │ ├── dependency-1.0.0.jar │ │ │ ├── dependencyX-1.0.0-SNAPSHOT.jar │ │ │ ├── library.jarC.jar │ │ │ ├── libraryA.jar │ │ │ ├── libraryB.jar │ │ │ └── more/ │ │ │ └── dependency-1.0.0.jar │ │ └── extra-directory/ │ │ └── foo │ ├── plugin-test/ │ │ └── build.gradle │ ├── projects/ │ │ ├── all-local-multi-service/ │ │ │ ├── build.gradle │ │ │ ├── complex-service/ │ │ │ │ ├── build.gradle │ │ │ │ ├── libs/ │ │ │ │ │ ├── dependency-1.0.0.jar │ │ │ │ │ └── dependencyX-1.0.0-SNAPSHOT.jar │ │ │ │ └── src/ │ │ │ │ └── main/ │ │ │ │ ├── extra-resources-1/ │ │ │ │ │ └── resource1.txt │ │ │ │ ├── extra-resources-2/ │ │ │ │ │ └── resource2.txt │ │ │ │ ├── java/ │ │ │ │ │ └── com/ │ │ │ │ │ └── test/ │ │ │ │ │ └── HelloWorld.java │ │ │ │ └── other-jib/ │ │ │ │ └── extra-file │ │ │ ├── gradle.properties │ │ │ ├── lib/ │ │ │ │ ├── build.gradle │ │ │ │ └── src/ │ │ │ │ └── main/ │ │ │ │ ├── java/ │ │ │ │ │ └── com/ │ │ │ │ │ └── lib/ │ │ │ │ │ └── Lib.java │ │ │ │ └── resources/ │ │ │ │ └── hi.txt │ │ │ ├── settings.gradle │ │ │ └── simple-service/ │ │ │ ├── build.gradle │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── test/ │ │ │ └── HelloWorld.java │ │ ├── lazy-evaluation/ │ │ │ ├── build-extra-dirs.gradle │ │ │ ├── build.gradle │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── test/ │ │ │ │ └── HelloWorld.java │ │ │ └── updated-custom-extra-dir/ │ │ │ └── foo │ │ ├── multi-service/ │ │ │ ├── build.gradle │ │ │ ├── complex-service/ │ │ │ │ ├── build.gradle │ │ │ │ ├── local-m2-repo/ │ │ │ │ │ └── com/ │ │ │ │ │ └── google/ │ │ │ │ │ └── cloud/ │ │ │ │ │ └── tools/ │ │ │ │ │ └── tiny-test-lib/ │ │ │ │ │ ├── 0.0.1-SNAPSHOT/ │ │ │ │ │ │ ├── maven-metadata-local.xml │ │ │ │ │ │ ├── tiny-test-lib-0.0.1-SNAPSHOT.jar │ │ │ │ │ │ └── tiny-test-lib-0.0.1-SNAPSHOT.pom │ │ │ │ │ └── maven-metadata-local.xml │ │ │ │ └── src/ │ │ │ │ └── main/ │ │ │ │ ├── extra-resources-1/ │ │ │ │ │ └── resource1.txt │ │ │ │ ├── extra-resources-2/ │ │ │ │ │ └── resource2.txt │ │ │ │ ├── java/ │ │ │ │ │ └── com/ │ │ │ │ │ └── test/ │ │ │ │ │ └── HelloWorld.java │ │ │ │ └── other-jib/ │ │ │ │ └── extra-file │ │ │ ├── gradle.properties │ │ │ ├── lib/ │ │ │ │ ├── build.gradle │ │ │ │ └── src/ │ │ │ │ ├── main/ │ │ │ │ │ ├── java/ │ │ │ │ │ │ └── com/ │ │ │ │ │ │ └── lib/ │ │ │ │ │ │ └── Lib.java │ │ │ │ │ └── resources/ │ │ │ │ │ └── hi.txt │ │ │ │ └── test/ │ │ │ │ └── java/ │ │ │ │ └── com/ │ │ │ │ └── lib/ │ │ │ │ └── LibTest.java │ │ │ ├── settings.gradle │ │ │ └── simple-service/ │ │ │ ├── build.gradle │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── test/ │ │ │ └── HelloWorld.java │ │ ├── platform/ │ │ │ ├── build.gradle │ │ │ ├── platform/ │ │ │ │ └── build.gradle │ │ │ ├── service/ │ │ │ │ ├── build.gradle │ │ │ │ └── src/ │ │ │ │ └── main/ │ │ │ │ └── java/ │ │ │ │ └── com/ │ │ │ │ └── test/ │ │ │ │ └── HelloWorld.java │ │ │ └── settings.gradle │ │ ├── simple/ │ │ │ ├── build.gradle │ │ │ ├── libs/ │ │ │ │ └── dependency-1.0.0.jar │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── custom-extra-dir/ │ │ │ │ ├── bar/ │ │ │ │ │ └── cat │ │ │ │ └── foo │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── test/ │ │ │ │ └── HelloWorld.java │ │ │ └── resources/ │ │ │ └── world │ │ ├── skaffold-config/ │ │ │ ├── build.gradle │ │ │ ├── libs/ │ │ │ │ └── dependency-1.0.0.jar │ │ │ ├── other/ │ │ │ │ └── file.txt │ │ │ ├── script.gradle │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ ├── test/ │ │ │ │ │ └── HelloWorld.java │ │ │ │ └── test2/ │ │ │ │ └── GoodbyeWorld.java │ │ │ ├── jib/ │ │ │ │ ├── bar/ │ │ │ │ │ └── cat │ │ │ │ └── foo │ │ │ └── resources/ │ │ │ └── world │ │ └── war_servlet25/ │ │ ├── build.gradle │ │ ├── pom-tomcat.xml │ │ ├── pom.xml │ │ └── src/ │ │ ├── extra_js/ │ │ │ └── bogus.js │ │ ├── extra_static/ │ │ │ └── bogus.html │ │ └── main/ │ │ ├── java/ │ │ │ └── example/ │ │ │ └── HelloWorld.java │ │ ├── resources/ │ │ │ └── world │ │ └── webapp/ │ │ ├── META-INF/ │ │ │ └── MANIFEST.MF │ │ ├── WEB-INF/ │ │ │ └── web.xml │ │ └── index.html │ └── webapp/ │ ├── META-INF/ │ │ └── context.xml │ ├── Test.jsp │ └── WEB-INF/ │ ├── classes/ │ │ └── package/ │ │ └── test.properties │ ├── lib/ │ │ ├── dependency-1.0.0.jar │ │ └── dependencyX-1.0.0-SNAPSHOT.jar │ └── web.xml ├── jib-gradle-plugin-extension-api/ │ ├── CHANGELOG.md │ ├── build.gradle │ ├── gradle.properties │ ├── kokoro/ │ │ └── release_build.sh │ └── src/ │ └── main/ │ └── java/ │ └── com/ │ └── google/ │ └── cloud/ │ └── tools/ │ └── jib/ │ └── gradle/ │ └── extension/ │ ├── GradleData.java │ └── JibGradlePluginExtension.java ├── jib-maven-plugin/ │ ├── CHANGELOG.md │ ├── README.md │ ├── build.gradle │ ├── gradle.properties │ ├── kokoro/ │ │ └── release_build.sh │ ├── scripts/ │ │ └── update_gcs_latest.sh │ └── src/ │ ├── integration-test/ │ │ └── java/ │ │ └── com/ │ │ └── google/ │ │ └── cloud/ │ │ └── tools/ │ │ └── jib/ │ │ └── maven/ │ │ ├── BuildDockerMojoIntegrationTest.java │ │ ├── BuildImageMojoIntegrationTest.java │ │ └── BuildTarMojoIntegrationTest.java │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── google/ │ │ └── cloud/ │ │ └── tools/ │ │ └── jib/ │ │ └── maven/ │ │ ├── BuildDockerMojo.java │ │ ├── BuildImageMojo.java │ │ ├── BuildTarMojo.java │ │ ├── JibPluginConfiguration.java │ │ ├── MavenExtensionData.java │ │ ├── MavenHelpfulSuggestions.java │ │ ├── MavenProjectProperties.java │ │ ├── MavenRawConfiguration.java │ │ ├── MavenSettingsProxyProvider.java │ │ ├── MavenSettingsServerCredentials.java │ │ ├── MojoCommon.java │ │ └── skaffold/ │ │ ├── CheckJibVersionMojo.java │ │ ├── FilesMojoV2.java │ │ ├── InitMojo.java │ │ ├── PackageGoalsMojo.java │ │ ├── SkaffoldBindingMojo.java │ │ ├── SkaffoldConfiguration.java │ │ └── SyncMapMojo.java │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── google/ │ │ └── cloud/ │ │ └── tools/ │ │ └── jib/ │ │ └── maven/ │ │ ├── JibPluginConfigurationTest.java │ │ ├── MavenProjectPropertiesExtensionTest.java │ │ ├── MavenProjectPropertiesTest.java │ │ ├── MavenRawConfigurationTest.java │ │ ├── MavenSettingsProxyProviderTest.java │ │ ├── MavenSettingsServerCredentialsTest.java │ │ ├── MojoCommonTest.java │ │ ├── SettingsFixture.java │ │ ├── SkippedGoalVerifier.java │ │ ├── TestProject.java │ │ ├── TestRepository.java │ │ └── skaffold/ │ │ ├── CheckJibVersionMojoTest.java │ │ ├── FilesMojoV2KotlinTest.java │ │ ├── FilesMojoV2Test.java │ │ ├── InitMojoTest.java │ │ ├── PackageGoalsMojoTest.java │ │ └── SyncMapMojoTest.java │ └── resources/ │ ├── maven/ │ │ ├── application/ │ │ │ ├── dependencies/ │ │ │ │ ├── another/ │ │ │ │ │ └── one/ │ │ │ │ │ └── dependency-1.0.0.jar │ │ │ │ ├── dependency-1.0.0.jar │ │ │ │ ├── dependencyX-1.0.0-SNAPSHOT.jar │ │ │ │ ├── library.jarC.jar │ │ │ │ ├── libraryA.jar │ │ │ │ ├── libraryB.jar │ │ │ │ └── more/ │ │ │ │ └── dependency-1.0.0.jar │ │ │ ├── output/ │ │ │ │ ├── directory/ │ │ │ │ │ └── somefile │ │ │ │ ├── resourceA │ │ │ │ ├── resourceB │ │ │ │ └── world │ │ │ └── source/ │ │ │ ├── HelloWorld.java │ │ │ ├── package/ │ │ │ │ └── some.java │ │ │ └── some.java │ │ ├── projects/ │ │ │ ├── default-target/ │ │ │ │ ├── libs/ │ │ │ │ │ └── dependency-1.0.0.jar │ │ │ │ ├── pom.xml │ │ │ │ └── src/ │ │ │ │ └── main/ │ │ │ │ ├── java/ │ │ │ │ │ └── com/ │ │ │ │ │ └── test/ │ │ │ │ │ └── HelloWorld.java │ │ │ │ └── resources/ │ │ │ │ └── world │ │ │ ├── empty/ │ │ │ │ ├── pom-broken-user.xml │ │ │ │ ├── pom.xml │ │ │ │ └── src/ │ │ │ │ └── main/ │ │ │ │ └── java/ │ │ │ │ └── com/ │ │ │ │ └── test/ │ │ │ │ └── Empty.java │ │ │ ├── multi/ │ │ │ │ ├── complex-service/ │ │ │ │ │ ├── fake-remote-repo/ │ │ │ │ │ │ └── com/ │ │ │ │ │ │ └── google/ │ │ │ │ │ │ └── cloud/ │ │ │ │ │ │ └── tools/ │ │ │ │ │ │ └── tiny-test-lib/ │ │ │ │ │ │ ├── 0.0.1-SNAPSHOT/ │ │ │ │ │ │ │ ├── maven-metadata-local.xml │ │ │ │ │ │ │ ├── tiny-test-lib-0.0.1-SNAPSHOT.jar │ │ │ │ │ │ │ └── tiny-test-lib-0.0.1-SNAPSHOT.pom │ │ │ │ │ │ └── maven-metadata-local.xml │ │ │ │ │ ├── pom.xml │ │ │ │ │ └── src/ │ │ │ │ │ └── main/ │ │ │ │ │ ├── extra-resources-1/ │ │ │ │ │ │ └── resource1.txt │ │ │ │ │ ├── extra-resources-2/ │ │ │ │ │ │ └── resource2.txt │ │ │ │ │ ├── java/ │ │ │ │ │ │ └── com/ │ │ │ │ │ │ └── test/ │ │ │ │ │ │ └── HelloWorld.java │ │ │ │ │ ├── jib1/ │ │ │ │ │ │ └── foo │ │ │ │ │ └── jib2/ │ │ │ │ │ └── bar │ │ │ │ ├── lib/ │ │ │ │ │ ├── pom.xml │ │ │ │ │ └── src/ │ │ │ │ │ ├── main/ │ │ │ │ │ │ └── java/ │ │ │ │ │ │ └── com/ │ │ │ │ │ │ └── lib/ │ │ │ │ │ │ └── Lib.java │ │ │ │ │ └── test/ │ │ │ │ │ └── java/ │ │ │ │ │ └── com/ │ │ │ │ │ └── lib/ │ │ │ │ │ └── LibTest.java │ │ │ │ ├── pom.xml │ │ │ │ └── simple-service/ │ │ │ │ ├── pom.xml │ │ │ │ └── src/ │ │ │ │ └── main/ │ │ │ │ └── java/ │ │ │ │ └── com/ │ │ │ │ └── test/ │ │ │ │ └── HelloWorld.java │ │ │ ├── simple/ │ │ │ │ ├── libs/ │ │ │ │ │ └── dependency-1.0.0.jar │ │ │ │ ├── mock-docker.sh │ │ │ │ ├── pom-complex-properties.xml │ │ │ │ ├── pom-complex.xml │ │ │ │ ├── pom-cred-helper-1.xml │ │ │ │ ├── pom-cred-helper-2.xml │ │ │ │ ├── pom-dockerclient.xml │ │ │ │ ├── pom-extra-dirs-filtering.xml │ │ │ │ ├── pom-extra-dirs.xml │ │ │ │ ├── pom-jar-containerization.xml │ │ │ │ ├── pom-java11-incompatible.xml │ │ │ │ ├── pom-java11.xml │ │ │ │ ├── pom-localbase.xml │ │ │ │ ├── pom-multiplatform-build.xml │ │ │ │ ├── pom-no-to-image.xml │ │ │ │ ├── pom-skaffold-config.xml │ │ │ │ ├── pom-timestamps-custom.xml │ │ │ │ ├── pom.xml │ │ │ │ └── src/ │ │ │ │ └── main/ │ │ │ │ ├── java/ │ │ │ │ │ └── com/ │ │ │ │ │ └── test/ │ │ │ │ │ └── HelloWorld.java │ │ │ │ ├── jib-custom/ │ │ │ │ │ ├── bar/ │ │ │ │ │ │ └── cat │ │ │ │ │ └── foo │ │ │ │ ├── jib-custom-2/ │ │ │ │ │ └── baz │ │ │ │ ├── jib-custom-3/ │ │ │ │ │ ├── cat.json │ │ │ │ │ ├── cat.txt │ │ │ │ │ └── sub/ │ │ │ │ │ ├── a.json │ │ │ │ │ └── a.txt │ │ │ │ ├── jib-custom-4/ │ │ │ │ │ ├── bar │ │ │ │ │ └── foo │ │ │ │ └── resources/ │ │ │ │ └── world │ │ │ ├── spring-boot/ │ │ │ │ ├── pom.xml │ │ │ │ └── src/ │ │ │ │ └── main/ │ │ │ │ └── java/ │ │ │ │ └── hello/ │ │ │ │ ├── Application.java │ │ │ │ └── HelloController.java │ │ │ ├── spring-boot-multi/ │ │ │ │ ├── pom.xml │ │ │ │ ├── service-1/ │ │ │ │ │ ├── pom.xml │ │ │ │ │ └── src/ │ │ │ │ │ └── main/ │ │ │ │ │ └── java/ │ │ │ │ │ └── com/ │ │ │ │ │ └── test/ │ │ │ │ │ └── HelloWorld.java │ │ │ │ └── service-2/ │ │ │ │ ├── pom.xml │ │ │ │ └── src/ │ │ │ │ └── main/ │ │ │ │ └── java/ │ │ │ │ └── com/ │ │ │ │ └── test/ │ │ │ │ └── HelloWorld.java │ │ │ └── war_servlet25/ │ │ │ ├── pom-tomcat.xml │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ ├── extra_js/ │ │ │ │ └── bogus.js │ │ │ ├── extra_static/ │ │ │ │ └── bogus.html │ │ │ └── main/ │ │ │ ├── java/ │ │ │ │ └── example/ │ │ │ │ └── HelloWorld.java │ │ │ ├── resources/ │ │ │ │ └── world │ │ │ └── webapp/ │ │ │ ├── META-INF/ │ │ │ │ └── MANIFEST.MF │ │ │ ├── WEB-INF/ │ │ │ │ └── web.xml │ │ │ └── index.html │ │ ├── settings/ │ │ │ ├── bad-encrypted-proxy-settings.xml │ │ │ ├── encrypted-proxy-settings.xml │ │ │ ├── http-only-proxy-settings.xml │ │ │ ├── https-only-proxy-settings.xml │ │ │ ├── no-active-proxy-settings.xml │ │ │ ├── readme │ │ │ ├── settings-security.empty.xml │ │ │ ├── settings-security.xml │ │ │ └── settings.xml │ │ ├── testM2/ │ │ │ └── com/ │ │ │ └── test/ │ │ │ ├── dependency/ │ │ │ │ ├── 1.0.0/ │ │ │ │ │ ├── _remote.repositories │ │ │ │ │ ├── dependency-1.0.0.jar │ │ │ │ │ └── dependency-1.0.0.pom │ │ │ │ └── maven-metadata-local.xml │ │ │ └── dependencyX/ │ │ │ ├── 1.0.0-SNAPSHOT/ │ │ │ │ ├── _remote.repositories │ │ │ │ ├── dependencyX-1.0.0-SNAPSHOT.jar │ │ │ │ ├── dependencyX-1.0.0-SNAPSHOT.pom │ │ │ │ └── maven-metadata-local.xml │ │ │ └── maven-metadata-local.xml │ │ └── webapp/ │ │ └── final-name/ │ │ ├── META-INF/ │ │ │ └── context.xml │ │ ├── Test.jsp │ │ └── WEB-INF/ │ │ ├── classes/ │ │ │ └── package/ │ │ │ └── test.properties │ │ ├── lib/ │ │ │ ├── dependency-1.0.0.jar │ │ │ └── dependencyX-1.0.0-SNAPSHOT.jar │ │ └── web.xml │ └── mockito-extensions/ │ └── org.mockito.plugins.MockMaker ├── jib-maven-plugin-extension-api/ │ ├── CHANGELOG.md │ ├── build.gradle │ ├── gradle.properties │ ├── kokoro/ │ │ └── release_build.sh │ └── src/ │ └── main/ │ └── java/ │ └── com/ │ └── google/ │ └── cloud/ │ └── tools/ │ └── jib/ │ └── maven/ │ └── extension/ │ ├── JibMavenPluginExtension.java │ └── MavenData.java ├── jib-plugins-common/ │ ├── README.md │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── google/ │ │ └── cloud/ │ │ └── tools/ │ │ └── jib/ │ │ └── plugins/ │ │ └── common/ │ │ ├── AuthProperty.java │ │ ├── BuildStepsExecutionException.java │ │ ├── ConfigurationPropertyValidator.java │ │ ├── ContainerizingMode.java │ │ ├── DefaultCredentialRetrievers.java │ │ ├── ExtraDirectoryNotFoundException.java │ │ ├── HelpfulSuggestions.java │ │ ├── ImageMetadataOutput.java │ │ ├── IncompatibleBaseImageJavaVersionException.java │ │ ├── InferredAuthException.java │ │ ├── InferredAuthProvider.java │ │ ├── InvalidAppRootException.java │ │ ├── InvalidContainerVolumeException.java │ │ ├── InvalidContainerizingModeException.java │ │ ├── InvalidCreationTimeException.java │ │ ├── InvalidFilesModificationTimeException.java │ │ ├── InvalidPlatformException.java │ │ ├── InvalidWorkingDirectoryException.java │ │ ├── JavaContainerBuilderHelper.java │ │ ├── JibBuildRunner.java │ │ ├── MainClassInferenceException.java │ │ ├── MainClassResolver.java │ │ ├── PluginConfigurationProcessor.java │ │ ├── PluginExtensionLogger.java │ │ ├── ProjectProperties.java │ │ ├── PropertyNames.java │ │ ├── RawConfiguration.java │ │ ├── SkaffoldFilesOutput.java │ │ ├── SkaffoldInitOutput.java │ │ ├── SkaffoldSyncMapTemplate.java │ │ ├── TimerEventHandler.java │ │ ├── UpdateChecker.java │ │ ├── VersionChecker.java │ │ ├── ZipUtil.java │ │ ├── globalconfig/ │ │ │ ├── GlobalConfig.java │ │ │ ├── GlobalConfigTemplate.java │ │ │ ├── InvalidGlobalConfigException.java │ │ │ └── RegistryMirrorsTemplate.java │ │ └── logging/ │ │ ├── AnsiLoggerWithFooter.java │ │ ├── ConsoleLogger.java │ │ ├── ConsoleLoggerBuilder.java │ │ ├── PlainConsoleLogger.java │ │ ├── ProgressDisplayGenerator.java │ │ └── SingleThreadedExecutor.java │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── google/ │ │ └── cloud/ │ │ └── tools/ │ │ └── jib/ │ │ ├── api/ │ │ │ ├── HttpRequestTester.java │ │ │ └── JibContainerBuilderTestHelper.java │ │ └── plugins/ │ │ └── common/ │ │ ├── ConfigurationPropertyValidatorTest.java │ │ ├── ContainerizingModeTest.java │ │ ├── DefaultCredentialRetrieversTest.java │ │ ├── HelpfulSuggestionsTest.java │ │ ├── ImageMetadataOutputTest.java │ │ ├── JavaContainerBuilderHelperTest.java │ │ ├── JibBuildRunnerTest.java │ │ ├── MainClassResolverTest.java │ │ ├── PluginConfigurationProcessorTest.java │ │ ├── SkaffoldFilesOutputTest.java │ │ ├── SkaffoldSyncMapTemplateTest.java │ │ ├── TimerEventHandlerTest.java │ │ ├── UpdateCheckerTest.java │ │ ├── VersionCheckerTest.java │ │ ├── ZipUtilTest.java │ │ ├── globalconfig/ │ │ │ └── GlobalConfigTest.java │ │ └── logging/ │ │ ├── AnsiLoggerWithFooterTest.java │ │ ├── ConsoleLoggerBuilderTest.java │ │ ├── PlainConsoleLoggerTest.java │ │ ├── ProgressDisplayGeneratorTest.java │ │ └── SingleThreadedExecutorTest.java │ └── resources/ │ └── plugins-common/ │ └── exploded-war/ │ ├── META-INF/ │ │ └── context.xml │ ├── Test.jsp │ └── WEB-INF/ │ ├── classes/ │ │ └── package/ │ │ └── test.properties │ ├── lib/ │ │ ├── dependency-1.0.0.jar │ │ └── dependencyX-1.0.0-SNAPSHOT.jar │ └── web.xml ├── jib-plugins-extension-common/ │ ├── build.gradle │ ├── gradle.properties │ ├── kokoro/ │ │ └── release_build.sh │ └── src/ │ └── main/ │ └── java/ │ └── com/ │ └── google/ │ └── cloud/ │ └── tools/ │ └── jib/ │ └── plugins/ │ └── extension/ │ ├── ExtensionLogger.java │ ├── JibPluginExtension.java │ ├── JibPluginExtensionException.java │ └── NullExtension.java ├── kokoro/ │ ├── continuous.bat │ ├── continuous.sh │ ├── docker_setup_macos.sh │ ├── docker_setup_ubuntu.sh │ ├── presubmit.bat │ └── presubmit.sh ├── proposals/ │ ├── README.md │ ├── archives/ │ │ ├── README.md │ │ ├── build_tarball.md │ │ ├── cache_v2.md │ │ ├── docker_build.md │ │ ├── events.md │ │ ├── java_agents_and_more_layers.md │ │ ├── jib_core_library.md │ │ ├── log_output_v2.md │ │ ├── maven_configuration_v2.md │ │ ├── output_files.md │ │ ├── progress_output.md │ │ ├── reproducible_base_image.md │ │ ├── reproducible_jars.md │ │ └── skaffold_config.md │ ├── buildfile.md │ ├── cli-jar-processing.md │ ├── container-build-plan-spec.md │ ├── jib-cli-surface.md │ └── tags-on-existing-images.md └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .allstar/binary_artifacts.yaml ================================================ # 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 ================================================ FILE: .gitattributes ================================================ *.bat eol=crlf *.cmd eol=crlf ================================================ FILE: .github/ISSUE_TEMPLATE/issue_report.md ================================================ --- 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**: ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ 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 # 🛠️ ================================================ FILE: .github/RELEASE_TEMPLATES/cli_release_checklist.md ================================================ --- 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 }}) ================================================ FILE: .github/RELEASE_TEMPLATES/core_release_checklist.md ================================================ --- 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 }}) ================================================ FILE: .github/RELEASE_TEMPLATES/plugin_release_checklist.md ================================================ --- 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) ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "gradle" directory: "/" schedule: interval: "daily" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" ================================================ FILE: .github/workflows/gradle-wrapper-validation.yml ================================================ 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 ================================================ FILE: .github/workflows/jib-cli-release.yml ================================================ 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 ================================================ FILE: .github/workflows/prepare-release.yml ================================================ 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 ================================================ FILE: .github/workflows/sonar.yml ================================================ 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 ================================================ FILE: .github/workflows/unit-tests.yml ================================================ 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 ================================================ FILE: .gitignore ================================================ build/ !jib-gradle-plugin/src/test/resources/gradle/application/build/ target/ out bin/ *.iml *.ipr *.iws .idea .gradle/ # https://github.com/takari/maven-wrapper#usage-without-binary-jar **/.mvn/wrapper/maven-wrapper.jar .settings/ .classpath .project .DS_Store ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Code of Conduct As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery * Personal attacks * Trolling or insulting/derogatory comments * Public or private harassment * Publishing other's private information, such as physical or electronic addresses, without explicit permission * Other unethical or unprofessional conduct. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team. This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0, available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/) ================================================ FILE: CONTRIBUTING.md ================================================ 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 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; 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. You generally only need to submit a CLA once, so if you've already submitted one (even if it was for a different project), you probably don't need to do it again. ## Code Reviews 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 (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' ``` ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ ![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 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. Jib works well with Google Cloud Build. For details, see [how to use Jib on Google Cloud Build](docs/google-cloud-build.md). 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 * **Fast** - Deploy your changes fast. Jib separates your application into multiple layers, splitting dependencies from classes. Now you don’t have to wait for Docker to rebuild your entire Java application - just deploy the layers that changed. * **Reproducible** - Rebuilding your container image with the same contents always generates the same image. Never trigger an unnecessary update again. * **Daemonless** - Reduce your CLI dependencies. Build your Docker image from within Maven or Gradle and push to any registry of your choice. *No more writing Dockerfiles and calling docker build/push.* ## Quickstart * **Maven** - See the jib-maven-plugin [Quickstart](jib-maven-plugin#quickstart). * **Gradle** - See the jib-gradle-plugin [Quickstart](jib-gradle-plugin#quickstart). * **Jib Core** - See the Jib Core [Quickstart](jib-core#adding-jib-core-to-your-build). * **Jib CLI** - See the Jib CLI [doc](jib-cli). ## 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 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). ## 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). ## Get involved with the 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) * 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. ================================================ FILE: SECURITY.md ================================================ # 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. ================================================ FILE: STYLE_GUIDE.md ================================================ # 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` ================================================ FILE: build.gradle ================================================ // 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.15.2', 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 */ ================================================ FILE: config/checkstyle/checkstyle-suppressions.xml ================================================ ================================================ FILE: config/checkstyle/copyright-java.header ================================================ ^/\*$ ^ \* 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 ^ \*$ ^ \* http://www\.apache\.org/licenses/LICENSE-2\.0$ ^ \*$ ^ \* Unless required by applicable law or agreed to in writing, software$ ^ \* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT$ ^ \* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied\. See the$ ^ \* License for the specific language governing permissions and limitations under$ ^ \* the License\.$ ^ \*/$ ================================================ FILE: docs/configure-gcp-credentials.md ================================================ # 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 registry. ## Using the Docker credential helper The easiest way is to install the [docker-credential-gcr](https://github.com/GoogleCloudPlatform/docker-credential-gcr). ### Installation If you have [`gcloud` (Cloud SDK)](https://cloud.google.com/sdk/gcloud/) installed, you can run: ```shell gcloud components install docker-credential-gcr ``` Alternatively, if you have `go get` installed, you can run: ```shell go get -u github.com/GoogleCloudPlatform/docker-credential-gcr ``` Alternatively, you can download `docker-credential-gcr` from its [Github Releases](https://github.com/GoogleCloudPlatform/docker-credential-gcr/releases). ### Enable the Container Registry API If you have not already done so, make sure you [enable the Container Registry API](https://console.cloud.google.com/flows/enableapi?apiid=containerregistry.googleapis.com&redirect=https://github.com/GoogleContainerRegistry/jib) for the Google Cloud Platform account you wish to use. ### Log in Log in to the account you with to use with: ```shell docker-credential-gcr gcr-login ``` This stores the credentials in `docker-credential-gcr`'s private credential store. Now, you can use Jib to pull and push from images in the form `gcr.io/your-gcp-project/your-image-name`. ================================================ FILE: docs/default_base_image.md ================================================ # 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`. ================================================ FILE: docs/faq.md ================================================ ## 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 ``` ================================================ FILE: docs/google-cloud-build.md ================================================ # 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 ``` ================================================ FILE: docs/privacy.md ================================================ 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` ================================================ FILE: docs/self_sign_cert.md ================================================ # 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. ================================================ FILE: examples/README.md ================================================ # 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) ================================================ FILE: examples/dropwizard/.mvn/wrapper/MavenWrapperDownloader.java ================================================ /* 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(); } } ================================================ FILE: examples/dropwizard/.mvn/wrapper/maven-wrapper.properties ================================================ distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.0/apache-maven-3.6.0-bin.zip ================================================ FILE: examples/dropwizard/README.md ================================================ # 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) ================================================ FILE: examples/dropwizard/mvnw ================================================ #!/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.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 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 "$@" ================================================ FILE: examples/dropwizard/mvnw.cmd ================================================ @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% ================================================ FILE: examples/dropwizard/pom.xml ================================================ 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.1 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} ================================================ FILE: examples/dropwizard/src/main/java/example/JibExampleApplication.java ================================================ /* * 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)); } } ================================================ FILE: examples/dropwizard/src/main/java/example/JibExampleConfiguration.java ================================================ /* * 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 com.fasterxml.jackson.annotation.JsonProperty; import io.dropwizard.Configuration; import example.config.HelloWorldConfiguration; import javax.validation.Valid; import javax.validation.constraints.NotNull; @SuppressWarnings("unused") public class JibExampleConfiguration extends Configuration { @Valid @NotNull @JsonProperty private HelloWorldConfiguration hello; @JsonProperty("hello") public HelloWorldConfiguration getHelloConfiguration() { return hello; } } ================================================ FILE: examples/dropwizard/src/main/java/example/api/Saying.java ================================================ /* * 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; } } ================================================ FILE: examples/dropwizard/src/main/java/example/config/HelloWorldConfiguration.java ================================================ /* * 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.config; import io.dropwizard.Configuration; import org.hibernate.validator.constraints.NotEmpty; 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; } @JsonProperty public String getDefaultName() { return defaultName; } } ================================================ FILE: examples/dropwizard/src/main/java/example/health/TemplateHealthCheck.java ================================================ /* * 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.health; import com.codahale.metrics.health.HealthCheck; public class TemplateHealthCheck extends HealthCheck { private final String template; public TemplateHealthCheck(String template) { this.template = template; } @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(); } } ================================================ FILE: examples/dropwizard/src/main/java/example/resources/HelloWorldResource.java ================================================ /* * 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); } } ================================================ FILE: examples/dropwizard/src/main/resources/banner.txt ================================================ ================================================================================ Dropwizard Jib Example ================================================================================ ================================================ FILE: examples/dropwizard/src/main/resources/dropwizard.yml ================================================ <#-- 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 ================================================ FILE: examples/helloworld/README.md ================================================ Builds a container image that outputs `Hello World` when run. To build the image: 1. In `pom.xml` or `build.gradle`, replace `REPLACE-WITH-YOUR-GCP-PROJECT` with your GCP project. 1. Run `mvn compile jib:build` or `./gradlew jib`. ================================================ FILE: examples/helloworld/build.gradle ================================================ plugins { id 'java' id 'com.google.cloud.tools.jib' version '3.5.3' } sourceCompatibility = 1.8 targetCompatibility = 1.8 repositories { mavenCentral() } dependencies { implementation 'com.google.guava:guava:23.6-jre' } jib.to.image = 'gcr.io/REPLACE-WITH-YOUR-GCP-PROJECT/image-built-with-jib' ================================================ FILE: examples/helloworld/gradle/wrapper/gradle-wrapper.properties ================================================ 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 ================================================ FILE: examples/helloworld/gradlew ================================================ #!/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" "$@" ================================================ FILE: examples/helloworld/gradlew.bat ================================================ @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 ================================================ FILE: examples/helloworld/pom.xml ================================================ 4.0.0 example helloworld 1 UTF-8 3.5.1 3.8.0 com.google.guava guava 32.0.0-jre org.apache.maven.plugins maven-compiler-plugin ${maven-compiler-plugin.version} 1.8 1.8 com.google.cloud.tools jib-maven-plugin ${jib-maven-plugin.version} gcr.io/REPLACE-WITH-YOUR-GCP-PROJECT/image-built-with-jib package build ================================================ FILE: examples/helloworld/settings.gradle ================================================ ================================================ FILE: examples/helloworld/src/main/java/example/HelloWorld.java ================================================ /* * 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 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; public class HelloWorld { public static void main(String[] args) throws URISyntaxException, IOException { try (Reader reader = new InputStreamReader( HelloWorld.class.getResourceAsStream("/world"), StandardCharsets.UTF_8)) { String world = CharStreams.toString(reader); System.out.println("Hello " + world); } } } ================================================ FILE: examples/helloworld/src/main/resources/world ================================================ world ================================================ FILE: examples/java-agent/README.md ================================================ 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) ================================================ FILE: examples/java-agent/build.gradle ================================================ plugins { id 'java' id 'com.google.cloud.tools.jib' version '3.5.3' 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' ================================================ FILE: examples/java-agent/gradle/wrapper/gradle-wrapper.properties ================================================ 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 ================================================ FILE: examples/java-agent/gradle.properties ================================================ version = 0.0.1-SNAPSHOT ================================================ FILE: examples/java-agent/gradlew ================================================ #!/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" "$@" ================================================ FILE: examples/java-agent/gradlew.bat ================================================ @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 ================================================ FILE: examples/java-agent/pom.xml ================================================ 4.0.0 example java-agent 1 UTF-8 3.5.1 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} ================================================ FILE: examples/java-agent/settings.gradle ================================================ ================================================ FILE: examples/java-agent/src/main/java/example/HelloWorld.java ================================================ /* * 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"); } } ================================================ FILE: examples/java-agent/src/main/resources/world ================================================ world ================================================ FILE: examples/ktor/README.md ================================================ # 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 ================================================ FILE: examples/ktor/build.gradle.kts ================================================ plugins { application kotlin("jvm") version "1.3.10" id("com.google.cloud.tools.jib") version "3.5.3" } 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() } ================================================ FILE: examples/ktor/gradle/wrapper/gradle-wrapper.properties ================================================ 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 ================================================ FILE: examples/ktor/gradle.properties ================================================ kotlin.code.style=official ================================================ FILE: examples/ktor/gradlew ================================================ #!/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" "$@" ================================================ FILE: examples/ktor/gradlew.bat ================================================ @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 ================================================ FILE: examples/ktor/settings.gradle.kts ================================================ rootProject.name = "ktor-jib-example" ================================================ FILE: examples/ktor/src/main/kotlin/example/ktor/App.kt ================================================ /* * 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() } } } ================================================ FILE: examples/ktor/src/main/resources/application.conf ================================================ # 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 ] } } ================================================ FILE: examples/ktor/src/main/resources/logback.xml ================================================ %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n ================================================ FILE: examples/micronaut/README.md ================================================ # 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). ================================================ FILE: examples/micronaut/build.gradle ================================================ plugins { id "groovy" id "com.github.johnrengelman.shadow" version "5.2.0" id "application" id 'com.google.cloud.tools.jib' version '3.5.3' } 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') } ================================================ FILE: examples/micronaut/gradle/wrapper/gradle-wrapper.properties ================================================ 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 ================================================ FILE: examples/micronaut/gradle.properties ================================================ micronautVersion=2.0.0.M3 ================================================ FILE: examples/micronaut/gradlew ================================================ #!/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" "$@" ================================================ FILE: examples/micronaut/gradlew.bat ================================================ @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 ================================================ FILE: examples/micronaut/settings.gradle ================================================ rootProject.name = 'micronaut-jib' ================================================ FILE: examples/micronaut/src/main/groovy/example/micronaut/Application.groovy ================================================ package example.micronaut import groovy.transform.CompileStatic import io.micronaut.runtime.Micronaut @CompileStatic class Application { static void main(String[] args) { Micronaut.run(Application.class) } } ================================================ FILE: examples/micronaut/src/main/groovy/example/micronaut/HelloController.groovy ================================================ 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> } } ================================================ FILE: examples/micronaut/src/main/resources/application.yml ================================================ micronaut: application: name: examples ================================================ FILE: examples/micronaut/src/main/resources/logback.xml ================================================ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n ================================================ FILE: examples/micronaut/src/test/groovy/example/micronaut/HelloControllerSpec.groovy ================================================ 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" } } ================================================ FILE: examples/multi-module/.mvn/wrapper/MavenWrapperDownloader.java ================================================ /* 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(); } } ================================================ FILE: examples/multi-module/.mvn/wrapper/maven-wrapper.properties ================================================ distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.5.4/apache-maven-3.5.4-bin.zip ================================================ FILE: examples/multi-module/README.md ================================================ # 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' ``` ================================================ FILE: examples/multi-module/build.gradle ================================================ // 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.3' apply false } ================================================ FILE: examples/multi-module/gradle/wrapper/gradle-wrapper.properties ================================================ 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 ================================================ FILE: examples/multi-module/gradle-build.sh ================================================ #!/bin/sh set -ex export PROJECT_ID=$(gcloud config list --format 'value(core.project)') ./gradlew jib ================================================ FILE: examples/multi-module/gradlew ================================================ #!/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" "$@" ================================================ FILE: examples/multi-module/gradlew.bat ================================================ @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 ================================================ FILE: examples/multi-module/hello-service/build.gradle ================================================ 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}" ================================================ FILE: examples/multi-module/hello-service/gradle.properties ================================================ version = 0.1.0 ================================================ FILE: examples/multi-module/hello-service/pom.xml ================================================ 4.0.0 hello-service 0.1.0 com.example jib-multimodule 0.1.0 ================================================ FILE: examples/multi-module/hello-service/src/main/java/hello/Application.java ================================================ 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); } } ================================================ FILE: examples/multi-module/hello-service/src/main/java/hello/HelloController.java ================================================ 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; } } ================================================ FILE: examples/multi-module/kubernetes.yaml ================================================ 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 ================================================ FILE: examples/multi-module/maven-build.sh ================================================ #!/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 ================================================ FILE: examples/multi-module/mvnw ================================================ #!/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 "$@" ================================================ FILE: examples/multi-module/mvnw.cmd ================================================ @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% ================================================ FILE: examples/multi-module/name-service/build.gradle ================================================ 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}" ================================================ FILE: examples/multi-module/name-service/gradle.properties ================================================ version = 0.1.0 ================================================ FILE: examples/multi-module/name-service/pom.xml ================================================ 4.0.0 name-service 0.1.0 com.example jib-multimodule 0.1.0 com.example shared-library 0.1.0 ================================================ FILE: examples/multi-module/name-service/src/main/java/name/Application.java ================================================ 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); } } ================================================ FILE: examples/multi-module/name-service/src/main/java/name/NameController.java ================================================ 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(); } } ================================================ FILE: examples/multi-module/pom.xml ================================================ 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.1 org.springframework.boot spring-boot-starter-web ================================================ FILE: examples/multi-module/settings.gradle ================================================ rootProject.name = 'jib-multimodule' include 'name-service', 'hello-service', 'shared-library' ================================================ FILE: examples/multi-module/shared-library/build.gradle ================================================ 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 } ================================================ FILE: examples/multi-module/shared-library/gradle.properties ================================================ version = 0.1.0 ================================================ FILE: examples/multi-module/shared-library/pom.xml ================================================ 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 ================================================ FILE: examples/multi-module/shared-library/src/main/java/common/SharedUtils.java ================================================ package common; public class SharedUtils { public static String getText() { return "A string from 'shared-library'"; } } ================================================ FILE: examples/spring-boot/.mvn/wrapper/MavenWrapperDownloader.java ================================================ /* 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(); } } ================================================ FILE: examples/spring-boot/.mvn/wrapper/maven-wrapper.properties ================================================ distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.5.4/apache-maven-3.5.4-bin.zip ================================================ FILE: examples/spring-boot/README.md ================================================ # 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) ================================================ FILE: examples/spring-boot/build.gradle ================================================ 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.3' } repositories { mavenCentral() } sourceCompatibility = 1.8 targetCompatibility = 1.8 dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' } ================================================ FILE: examples/spring-boot/gradle/wrapper/gradle-wrapper.properties ================================================ 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 ================================================ FILE: examples/spring-boot/gradlew ================================================ #!/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" "$@" ================================================ FILE: examples/spring-boot/gradlew.bat ================================================ @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 ================================================ FILE: examples/spring-boot/mvnw ================================================ #!/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 "$@" ================================================ FILE: examples/spring-boot/mvnw.cmd ================================================ @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% ================================================ FILE: examples/spring-boot/pom.xml ================================================ 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.1 ================================================ FILE: examples/spring-boot/settings.gradle ================================================ ================================================ FILE: examples/spring-boot/src/main/java/hello/Application.java ================================================ 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); } } ================================================ FILE: examples/spring-boot/src/main/java/hello/HelloController.java ================================================ 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!"; } } ================================================ FILE: examples/vertx/README.md ================================================ # 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). ================================================ FILE: examples/vertx/build.gradle ================================================ plugins { id 'io.vertx.vertx-plugin' version '0.1.0' id 'com.google.cloud.tools.jib' version '3.5.3' } 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'] } } ================================================ FILE: examples/vertx/gradle/wrapper/gradle-wrapper.properties ================================================ 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 ================================================ FILE: examples/vertx/gradlew ================================================ #!/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" "$@" ================================================ FILE: examples/vertx/gradlew.bat ================================================ @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 ================================================ FILE: examples/vertx/settings.gradle ================================================ rootProject.name = 'vertx-jib-example' ================================================ FILE: examples/vertx/src/main/java/example/vertx/MainVerticle.java ================================================ 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()); } } ================================================ FILE: examples/vertx/src/main/resources/logback.xml ================================================ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ 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 ================================================ FILE: gradle.properties ================================================ org.gradle.jvmargs=-Xmx1024m org.gradle.caching=true ================================================ FILE: gradlew ================================================ #!/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" "$@" ================================================ FILE: gradlew.bat ================================================ @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 ================================================ FILE: jib-build-plan/CHANGELOG.md ================================================ # 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)) ================================================ FILE: jib-build-plan/build.gradle ================================================ 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 */ ================================================ FILE: jib-build-plan/gradle.properties ================================================ version = 0.4.1-SNAPSHOT ================================================ FILE: jib-build-plan/kokoro/release_build.sh ================================================ #!/bin/bash # Fail on any error. set -o errexit # Display commands to stderr. set -o xtrace cd github/jib ./gradlew :jib-build-plan:prepareRelease ================================================ FILE: jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/AbsoluteUnixPath.java ================================================ /* * 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(); } } ================================================ FILE: jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/ContainerBuildPlan.java ================================================ /* * 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); } } ================================================ FILE: jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/FileEntriesLayer.java ================================================ /* * 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); } } ================================================ FILE: jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/FileEntry.java ================================================ /* * 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 + "}"; } } ================================================ FILE: jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/FilePermissions.java ================================================ /* * 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(); } } ================================================ FILE: jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/FilePermissionsProvider.java ================================================ /* * 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); } ================================================ FILE: jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/ImageFormat.java ================================================ /* * 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 } ================================================ FILE: jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/LayerObject.java ================================================ /* * 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(); } ================================================ FILE: jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/ModificationTimeProvider.java ================================================ /* * 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); } ================================================ FILE: jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/OwnershipProvider.java ================================================ /* * 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); } ================================================ FILE: jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/Platform.java ================================================ /* * 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); } } ================================================ FILE: jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/Port.java ================================================ /* * 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; } } ================================================ FILE: jib-build-plan/src/main/java/com/google/cloud/tools/jib/api/buildplan/RelativeUnixPath.java ================================================ /* * 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); } } ================================================ FILE: jib-build-plan/src/main/java/com/google/cloud/tools/jib/buildplan/UnixPathParser.java ================================================ /* * 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() {} } ================================================ FILE: jib-build-plan/src/test/java/com/google/cloud/tools/jib/api/buildplan/AbsoluteUnixPathTest.java ================================================ /* * 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()); } } ================================================ FILE: jib-build-plan/src/test/java/com/google/cloud/tools/jib/api/buildplan/ContainerBuildPlanTest.java ================================================ /* * 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(); } } ================================================ FILE: jib-build-plan/src/test/java/com/google/cloud/tools/jib/api/buildplan/FileEntriesLayerTest.java ================================================ /* * 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())); } } ================================================ FILE: jib-build-plan/src/test/java/com/google/cloud/tools/jib/api/buildplan/FileEntryTest.java ================================================ /* * 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()); } } ================================================ FILE: jib-build-plan/src/test/java/com/google/cloud/tools/jib/api/buildplan/FilePermissionsTest.java ================================================ /* * 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()))); } } ================================================ FILE: jib-build-plan/src/test/java/com/google/cloud/tools/jib/api/buildplan/PortTest.java ================================================ /* * 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, "")); } } ================================================ FILE: jib-build-plan/src/test/java/com/google/cloud/tools/jib/api/buildplan/RelativeUnixPathTest.java ================================================ /* * 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()); } } ================================================ FILE: jib-build-plan/src/test/java/com/google/cloud/tools/jib/buildplan/UnixPathParserTest.java ================================================ /* * 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")); } } ================================================ FILE: jib-build-plan/src/test/resources/core/fileA ================================================ Crepe cakes are good. ================================================ FILE: jib-build-plan/src/test/resources/core/layer/a/b/bar ================================================ bar ================================================ FILE: jib-build-plan/src/test/resources/core/layer/c/cat ================================================ cat ================================================ FILE: jib-build-plan/src/test/resources/core/layer/foo ================================================ foo ================================================ FILE: jib-cli/CHANGELOG.md ================================================ # 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)) ================================================ FILE: jib-cli/README.md ================================================ # 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) * [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 ``` ## 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. ================================================ FILE: jib-cli/build.gradle ================================================ 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 */ ================================================ FILE: jib-cli/gradle.properties ================================================ version = 0.13.1-SNAPSHOT ================================================ FILE: jib-cli/scripts/update_gcs_latest.sh ================================================ #!/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 gsutil cp jib-cli-temp $destination gsutil acl ch -u allUsers:READ $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 ================================================ FILE: jib-cli/src/integration-test/java/com/google/cloud/tools/jib/cli/JarCommandTest.java ================================================ /* * 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(); } } } ================================================ FILE: jib-cli/src/integration-test/java/com/google/cloud/tools/jib/cli/TestProject.java ================================================ /* * 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; } } ================================================ FILE: jib-cli/src/integration-test/java/com/google/cloud/tools/jib/cli/WarCommandTest.java ================================================ /* * 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")); } } ================================================ FILE: jib-cli/src/integration-test/resources/jarTest/spring-boot/build-layered.gradle ================================================ 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() } ================================================ FILE: jib-cli/src/integration-test/resources/jarTest/spring-boot/build.gradle ================================================ 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() } ================================================ FILE: jib-cli/src/integration-test/resources/jarTest/spring-boot/gradle/wrapper/gradle-wrapper.properties ================================================ 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 ================================================ FILE: jib-cli/src/integration-test/resources/jarTest/spring-boot/gradlew ================================================ #!/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" "$@" ================================================ FILE: jib-cli/src/integration-test/resources/jarTest/spring-boot/gradlew.bat ================================================ @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 ================================================ FILE: jib-cli/src/integration-test/resources/jarTest/spring-boot/settings-layered.gradle ================================================ pluginManagement { repositories { mavenCentral() gradlePluginPortal() } } rootProject.name = 'spring-boot' rootProject.buildFileName = 'build-layered.gradle' ================================================ FILE: jib-cli/src/integration-test/resources/jarTest/spring-boot/settings.gradle ================================================ pluginManagement { repositories { mavenCentral() gradlePluginPortal() } } rootProject.name = 'spring-boot' ================================================ FILE: jib-cli/src/integration-test/resources/jarTest/spring-boot/src/main/java/hello/Application.java ================================================ 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); } } ================================================ FILE: jib-cli/src/integration-test/resources/jarTest/spring-boot/src/main/java/hello/HelloController.java ================================================ 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"; } } ================================================ FILE: jib-cli/src/integration-test/resources/jarTest/standard/HelloWorld.java ================================================ public class HelloWorld { public static void main(String[] args) { System.out.print("Hello World"); } } ================================================ FILE: jib-cli/src/integration-test/resources/jarTest/standard/dep/A.java ================================================ package dep; public class A { public static void getResult() { System.out.print("Hello "); } } ================================================ FILE: jib-cli/src/integration-test/resources/jarTest/standard/dep2/B.java ================================================ package dep2; public class B { public static void getResult() { System.out.print("World"); } } ================================================ FILE: jib-cli/src/integration-test/resources/warTest/build.gradle ================================================ 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 } ================================================ FILE: jib-cli/src/integration-test/resources/warTest/gradle/wrapper/gradle-wrapper.properties ================================================ 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 ================================================ FILE: jib-cli/src/integration-test/resources/warTest/gradlew ================================================ #!/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" "$@" ================================================ FILE: jib-cli/src/integration-test/resources/warTest/gradlew.bat ================================================ @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 ================================================ FILE: jib-cli/src/integration-test/resources/warTest/settings.gradle ================================================ rootProject.name = 'standard-war' ================================================ FILE: jib-cli/src/integration-test/resources/warTest/src/extra_js/bogus.js ================================================ // nothing inside ================================================ FILE: jib-cli/src/integration-test/resources/warTest/src/extra_static/bogus.html ================================================ nothing inside ================================================ FILE: jib-cli/src/integration-test/resources/warTest/src/main/java/example/HelloWorld.java ================================================ /* * 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); } } } ================================================ FILE: jib-cli/src/integration-test/resources/warTest/src/main/resources/world ================================================ world ================================================ FILE: jib-cli/src/integration-test/resources/warTest/src/main/webapp/META-INF/MANIFEST.MF ================================================ Manifest-Version: 1.0 Class-Path: ================================================ FILE: jib-cli/src/integration-test/resources/warTest/src/main/webapp/WEB-INF/web.xml ================================================ HelloWorld example.HelloWorld HelloWorld /hello ================================================ FILE: jib-cli/src/integration-test/resources/warTest/src/main/webapp/index.html ================================================ Hello World

Hello World!

Available Servlets:
The HelloWorld servlet
================================================ FILE: jib-cli/src/java-templates/com/google/cloud/tools/jib/cli/VersionInfo.java.template ================================================ /* * 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}"; } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/ArtifactLayers.java ================================================ /* * 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(); } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/ArtifactProcessor.java ================================================ /* * 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(); } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/ArtifactProcessors.java ================================================ /* * 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; } } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/Build.java ================================================ /* * 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; } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/CacheDirectories.java ================================================ /* * 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); } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/CommonCliOptions.java ================================================ /* * 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://...."); } } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/CommonContainerConfigCliOptions.java ================================================ /* * 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; } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/ContainerBuilders.java ================================================ /* * 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; } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/Containerizers.java ================================================ /* * 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); })); } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/Credentials.java ================================================ /* * 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(); } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/Instants.java ================================================ /* * 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"); } } } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/Jar.java ================================================ /* * 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; } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/JibCli.java ================================================ /* * 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); } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/ShortErrorMessageHandler.java ================================================ /* * 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(); } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/War.java ================================================ /* * 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)); } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/ArchiveLayerSpec.java ================================================ /* * 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); } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/BaseImageSpec.java ================================================ /* * 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; } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/BuildFileSpec.java ================================================ /* * 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); } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/BuildFiles.java ================================================ /* * 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); } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/CopySpec.java ================================================ /* * 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); } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/FileLayerSpec.java ================================================ /* * 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); } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/FilePropertiesSpec.java ================================================ /* * 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); } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/FilePropertiesStack.java ================================================ /* * 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; } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/LayerSpec.java ================================================ /* * 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"); } } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/Layers.java ================================================ /* * 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)); } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/LayersSpec.java ================================================ /* * 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; } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/PlatformSpec.java ================================================ /* * 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; } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/buildfile/Validator.java ================================================ /* * 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 + "'"); } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/jar/JarFiles.java ================================================ /* * 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"; } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/jar/JarLayers.java ================================================ /* * 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); } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/jar/ProcessingMode.java ================================================ /* * 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 } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/jar/SpringBootExplodedProcessor.java ================================================ /* * 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); } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/jar/SpringBootPackagedProcessor.java ================================================ /* * 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; } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/jar/StandardExplodedProcessor.java ================================================ /* * 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; } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/jar/StandardPackagedProcessor.java ================================================ /* * 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; } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/logging/CliLogger.java ================================================ /* * 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; } } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/logging/ConsoleOutput.java ================================================ /* * 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 } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/logging/HttpTraceLevel.java ================================================ /* * 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); } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/logging/Verbosity.java ================================================ /* * 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; } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/war/StandardWarExplodedProcessor.java ================================================ /* * 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."); } } ================================================ FILE: jib-cli/src/main/java/com/google/cloud/tools/jib/cli/war/WarFiles.java ================================================ /* * 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; } } ================================================ FILE: jib-cli/src/test/java/com/google/cloud/tools/jib/ArtifactProcessorsTest.java ================================================ /* * 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); } } ================================================ FILE: jib-cli/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTestProxy.java ================================================ /* * 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(); } } ================================================ FILE: jib-cli/src/test/java/com/google/cloud/tools/jib/api/HttpRequestTester.java ================================================ /* * 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() {} } ================================================ FILE: jib-cli/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTestHelper.java ================================================ /* * 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() {} } ================================================ FILE: jib-cli/src/test/java/com/google/cloud/tools/jib/cli/BuildTest.java ================================================ /* * 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 } } ================================================ FILE: jib-cli/src/test/java/com/google/cloud/tools/jib/cli/CacheDirectoriesTest.java ================================================ /* * 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"))); } } ================================================ FILE: jib-cli/src/test/java/com/google/cloud/tools/jib/cli/ContainerBuildersTest.java ================================================ /* * 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"))); } } ================================================ FILE: jib-cli/src/test/java/com/google/cloud/tools/jib/cli/ContainerizersTest.java ================================================ /* * 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(); } } ================================================ FILE: jib-cli/src/test/java/com/google/cloud/tools/jib/cli/CredentialsTest.java ================================================ /* * 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); } } ================================================ FILE: jib-cli/src/test/java/com/google/cloud/tools/jib/cli/InstantsTest.java ================================================ /* * 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()); } } } ================================================ FILE: jib-cli/src/test/java/com/google/cloud/tools/jib/cli/JarTest.java ================================================ /* * 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(); } } ================================================ FILE: jib-cli/src/test/java/com/google/cloud/tools/jib/cli/JibCliTest.java ================================================ /* * 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"); } } ================================================ FILE: jib-cli/src/test/java/com/google/cloud/tools/jib/cli/WarTest.java ================================================ /* * 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(); } } ================================================ FILE: jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/ArchiveLayerSpecTest.java ================================================ /* * 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")); } } } ================================================ FILE: jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/BaseImageSpecTest.java ================================================ /* * 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")); } } } ================================================ FILE: jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/BuildFileSpecTest.java ================================================ /* * 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(); } } ================================================ FILE: jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/BuildFilesTest.java ================================================ /* * 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()); } } ================================================ FILE: jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/CopySpecTest.java ================================================ /* * 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(); } } ================================================ FILE: jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/FileLayerSpecTest.java ================================================ /* * 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")); } } } ================================================ FILE: jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/FilePropertiesSpecTest.java ================================================ /* * 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(); } } ================================================ FILE: jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/FilePropertiesStackTest.java ================================================ /* * 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()); } } ================================================ FILE: jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/LayerSpecTest.java ================================================ /* * 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'")); } } } ================================================ FILE: jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/LayersSpecTest.java ================================================ /* * 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")); } } } ================================================ FILE: jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/LayersTest.java ================================================ /* * 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, ""))); } } ================================================ FILE: jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/PlatformSpecTest.java ================================================ /* * 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")); } } } ================================================ FILE: jib-cli/src/test/java/com/google/cloud/tools/jib/cli/buildfile/ValidatorTest.java ================================================ /* * 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()); } } } ================================================ FILE: jib-cli/src/test/java/com/google/cloud/tools/jib/cli/jar/JarFilesTest.java ================================================ /* * 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)); } } ================================================ FILE: jib-cli/src/test/java/com/google/cloud/tools/jib/cli/jar/SpringBootExplodedProcessorTest.java ================================================ /* * 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); } } ================================================ FILE: jib-cli/src/test/java/com/google/cloud/tools/jib/cli/jar/SpringBootPackagedProcessorTest.java ================================================ /* * 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); } } ================================================ FILE: jib-cli/src/test/java/com/google/cloud/tools/jib/cli/jar/StandardExplodedProcessorTest.java ================================================ /* * 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); } } ================================================ FILE: jib-cli/src/test/java/com/google/cloud/tools/jib/cli/jar/StandardPackagedProcessorTest.java ================================================ /* * 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); } } ================================================ FILE: jib-cli/src/test/java/com/google/cloud/tools/jib/cli/logging/CliLoggerTest.java ================================================ /* * 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(); } } ================================================ FILE: jib-cli/src/test/java/com/google/cloud/tools/jib/cli/war/StandardWarExplodedProcessorTest.java ================================================ /* * 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; } } ================================================ FILE: jib-cli/src/test/java/com/google/cloud/tools/jib/cli/war/WarFilesTest.java ================================================ /* * 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(); } } ================================================ FILE: jib-cli/src/test/resources/buildfiles/layers/archiveLayerTest/layers.yaml ================================================ entries: - name: "default" archive: "path/to/archive.tgz" ================================================ FILE: jib-cli/src/test/resources/buildfiles/layers/fileTest/default/layers.yaml ================================================ 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/" ================================================ FILE: jib-cli/src/test/resources/buildfiles/layers/fileTest/default/toDir.txt ================================================ ================================================ FILE: jib-cli/src/test/resources/buildfiles/layers/fileTest/default/toFile.txt ================================================ ================================================ FILE: jib-cli/src/test/resources/buildfiles/layers/fileTest/failWithExcludes/layers.yaml ================================================ entries: - name: "default" files: - src: "toFile.txt" dest: "/target/toFile.txt" excludes: - "**/*" ================================================ FILE: jib-cli/src/test/resources/buildfiles/layers/fileTest/failWithExcludes/toFile.txt ================================================ ================================================ FILE: jib-cli/src/test/resources/buildfiles/layers/fileTest/failWithIncludes/layers.yaml ================================================ entries: - name: "default" files: - src: "toFile.txt" dest: "/target/toFile.txt" includes: - "**/*" ================================================ FILE: jib-cli/src/test/resources/buildfiles/layers/fileTest/failWithIncludes/toFile.txt ================================================ ================================================ FILE: jib-cli/src/test/resources/buildfiles/layers/includesExcludesTest/layers.yaml ================================================ 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" ================================================ FILE: jib-cli/src/test/resources/buildfiles/layers/includesExcludesTest/project/excludedDir/exclude.me ================================================ ================================================ FILE: jib-cli/src/test/resources/buildfiles/layers/includesExcludesTest/project/includedDir/include.me ================================================ ================================================ FILE: jib-cli/src/test/resources/buildfiles/layers/includesExcludesTest/project/wild.card ================================================ ================================================ FILE: jib-cli/src/test/resources/buildfiles/layers/pathDoesNotExist/layers.yaml ================================================ entries: - name: "default" files: - src: "something-that-does-not-exist" dest: "/target/" ================================================ FILE: jib-cli/src/test/resources/buildfiles/layers/propertiesTest/dir/file.txt ================================================ ================================================ FILE: jib-cli/src/test/resources/buildfiles/layers/propertiesTest/layers.yaml ================================================ 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" ================================================ FILE: jib-cli/src/test/resources/buildfiles/layers/writeToRoot/dir/file.txt ================================================ ================================================ FILE: jib-cli/src/test/resources/buildfiles/layers/writeToRoot/layers.yaml ================================================ entries: - name: "root writer" files: - src: "dir" dest: "/" - name: "root parent fill" files: - src: "." dest: "/" includes: - "**/file.txt" ================================================ FILE: jib-cli/src/test/resources/buildfiles/projects/allDefaults/jib.yaml ================================================ apiVersion: jib/v1alpha1 kind: BuildFile ================================================ FILE: jib-cli/src/test/resources/buildfiles/projects/allProperties/altYamls/alt-jib.yaml ================================================ # 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" ================================================ FILE: jib-cli/src/test/resources/buildfiles/projects/allProperties/jib.yaml ================================================ # 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" ================================================ FILE: jib-cli/src/test/resources/buildfiles/projects/allProperties/project/script.sh ================================================ env echo "string from file" ================================================ FILE: jib-cli/src/test/resources/buildfiles/projects/templating/missingVar.yaml ================================================ apiVersion: jib/v1alpha1 kind: BuildFile labels: "label1": "${missingVar}" ================================================ FILE: jib-cli/src/test/resources/buildfiles/projects/templating/multiLine.yaml ================================================ apiVersion: jib/v1alpha1 kind: BuildFile ${replace this} ================================================ FILE: jib-cli/src/test/resources/buildfiles/projects/templating/valid.yaml ================================================ apiVersion: jib/v1alpha1 kind: BuildFile labels: "${key}": "${value}" "label1": "${repeated}" "label2": "${repeated}" "label3": "$${escaped}" "label4": "free$" "unmatched": "${" ================================================ FILE: jib-cli/src/test/resources/jar/standard/dependency1 ================================================ ================================================ FILE: jib-cli/src/test/resources/jar/standard/dependency2 ================================================ ================================================ FILE: jib-cli/src/test/resources/jar/standard/dependency3-SNAPSHOT-1.jar ================================================ ================================================ FILE: jib-cli/src/test/resources/jar/standard/directory/dependency4 ================================================ ================================================ FILE: jib-cli/src/test/resources/war/standard/allLayers/META-INF/context.xml ================================================ ================================================ FILE: jib-cli/src/test/resources/war/standard/allLayers/Test.jsp ================================================ ================================================ FILE: jib-cli/src/test/resources/war/standard/allLayers/WEB-INF/classes/package/test.properties ================================================ ================================================ FILE: jib-cli/src/test/resources/war/standard/allLayers/WEB-INF/web.xml ================================================ ================================================ FILE: jib-cli/src/test/resources/war/standard/noWebInfClasses/META-INF/context.xml ================================================ ================================================ FILE: jib-cli/src/test/resources/war/standard/noWebInfLib/META-INF/context.xml ================================================ ================================================ FILE: jib-core/CHANGELOG.md ================================================ # Change Log All notable changes to this project will be documented in this file. ## [unreleased] ### Added ### Changed ### Fixed ## 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)) ================================================ FILE: jib-core/README.md ================================================ ![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 - 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.1 ``` Add Jib Core as a dependency using Gradle: ```groovy dependencies { compile 'com.google.cloud.tools:jib-core:0.28.1' } ``` ## 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). ================================================ FILE: jib-core/build.gradle ================================================ plugins { id 'net.researchgate.release' id 'maven-publish' id 'eclipse' } 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 { 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': 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.*' } } /* RELEASE */ configureMavenRelease() publishing { publications { mavenJava(MavenPublication) { pom { name = 'Jib Core' description = 'Build container images.' } from components.java } } } // Release plugin (git release commits and version updates) release { tagTemplate = 'v$version-core' git { requireBranch = /^core-release-v\d+.*$/ //regex } } /* RELEASE */ /* ECLIPSE */ eclipse.classpath.plusConfigurations += [configurations.integrationTestImplementation] /* ECLIPSE */ ================================================ FILE: jib-core/examples/README.md ================================================ # 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. ================================================ FILE: jib-core/examples/build.gradle/README.md ================================================ # 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.27.0' } } 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' } } ``` ================================================ FILE: jib-core/gradle.properties ================================================ version = 0.28.2-SNAPSHOT ================================================ FILE: jib-core/kokoro/release_build.sh ================================================ #!/bin/bash # Fail on any error. set -o errexit # Display commands to stderr. set -o xtrace cd github/jib ./gradlew :jib-core:prepareRelease ================================================ FILE: jib-core/src/integration-test/java/com/google/cloud/tools/jib/Command.java ================================================ /* * 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.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.List; import javax.annotation.Nullable; /** Test utility to run shell commands for integration tests. */ 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); } /** Runs the command and pipes in {@code stdin}. */ public String run(@Nullable byte[] stdin) throws IOException, InterruptedException { ProcessBuilder processBuilder = new ProcessBuilder(command); if (workingDir != null) { processBuilder.directory(workingDir.toFile()); } Process process = processBuilder.start(); if (stdin != null) { // Write out stdin. try (OutputStream outputStream = process.getOutputStream()) { outputStream.write(stdin); } } // Read in stdout. try (InputStreamReader inputStreamReader = new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)) { String output = CharStreams.toString(inputStreamReader); if (process.waitFor() != 0) { String stderr = CharStreams.toString( new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8)); throw new RuntimeException("Command '" + String.join(" ", command) + "' failed: " + stderr); } return output; } } } ================================================ FILE: jib-core/src/integration-test/java/com/google/cloud/tools/jib/IntegrationTestingConfiguration.java ================================================ /* * 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() {} } ================================================ FILE: jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ContainerizerIntegrationTest.java ================================================ /* * 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); } } ================================================ FILE: jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibIntegrationTest.java ================================================ /* * 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 } } ================================================ FILE: jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/JibMultiPlatformIntegrationTest.java ================================================ /* * 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; } } ================================================ FILE: jib-core/src/integration-test/java/com/google/cloud/tools/jib/api/ReproducibleImageTest.java ================================================ /* * 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); } } ================================================ FILE: jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BearerAuthenticationIntegrationTest.java ================================================ /* * 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"); } } ================================================ FILE: jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobCheckerIntegrationTest.java ================================================ /* * 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.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.Test; /** Integration tests for {@link BlobChecker}. */ public class BlobCheckerIntegrationTest { private final FailoverHttpClient httpClient = new FailoverHttpClient(true, false, ignored -> {}); @Test public void testCheck_exists() throws IOException, RegistryException { RegistryClient registryClient = RegistryClient.factory(EventHandlers.NONE, "gcr.io", "distroless/base", httpClient) .newRegistryClient(); V22ManifestTemplate manifestTemplate = registryClient .pullManifest( ManifestPullerIntegrationTest.KNOWN_MANIFEST_V22_SHA, V22ManifestTemplate.class) .getManifest(); DescriptorDigest blobDigest = manifestTemplate.getLayers().get(0).getDigest(); Assert.assertEquals(blobDigest, registryClient.checkBlob(blobDigest).get().getDigest()); } @Test public void testCheck_doesNotExist() throws IOException, RegistryException, DigestException { RegistryClient registryClient = RegistryClient.factory(EventHandlers.NONE, "gcr.io", "distroless/base", httpClient) .newRegistryClient(); DescriptorDigest fakeBlobDigest = DescriptorDigest.fromHash( "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); Assert.assertFalse(registryClient.checkBlob(fakeBlobDigest).isPresent()); } } ================================================ FILE: jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPullerIntegrationTest.java ================================================ /* * 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.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.security.DigestException; import java.util.concurrent.atomic.LongAdder; import org.hamcrest.CoreMatchers; import org.hamcrest.MatcherAssert; import org.junit.Assert; import org.junit.Test; /** Integration tests for {@link BlobPuller}. */ public class BlobPullerIntegrationTest { private final FailoverHttpClient httpClient = new FailoverHttpClient(true, false, ignored -> {}); @Test public void testPull() throws IOException, RegistryException { RegistryClient registryClient = RegistryClient.factory(EventHandlers.NONE, "gcr.io", "distroless/base", httpClient) .newRegistryClient(); V22ManifestTemplate manifestTemplate = registryClient .pullManifest( ManifestPullerIntegrationTest.KNOWN_MANIFEST_V22_SHA, V22ManifestTemplate.class) .getManifest(); DescriptorDigest realDigest = manifestTemplate.getLayers().get(0).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 IOException, DigestException { DescriptorDigest nonexistentDigest = DescriptorDigest.fromHash( "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); RegistryClient registryClient = RegistryClient.factory(EventHandlers.NONE, "gcr.io", "distroless/base", httpClient) .newRegistryClient(); try { registryClient .pullBlob(nonexistentDigest, ignored -> {}, ignored -> {}) .writeTo(ByteStreams.nullOutputStream()); Assert.fail("Trying to pull nonexistent blob should have errored"); } catch (IOException ex) { if (!(ex.getCause() instanceof RegistryErrorException)) { throw ex; } MatcherAssert.assertThat( ex.getMessage(), CoreMatchers.containsString( "pull BLOB for gcr.io/distroless/base with digest " + nonexistentDigest)); } } } ================================================ FILE: jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/BlobPusherIntegrationTest.java ================================================ /* * 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.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.event.EventHandlers; import com.google.cloud.tools.jib.http.FailoverHttpClient; import java.io.IOException; import java.security.DigestException; import org.junit.Assert; import org.junit.ClassRule; import org.junit.Test; /** Integration tests for {@link BlobPusher}. */ public class BlobPusherIntegrationTest { @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 { Blob testBlob = Blobs.from("crepecake"); // Known digest for 'crepecake' DescriptorDigest testBlobDigest = DescriptorDigest.fromHash( "52a9e4d4ba4333ce593707f98564fee1e6d898db0d3602408c0b2a6a424d357c"); RegistryClient registryClient = RegistryClient.factory(EventHandlers.NONE, dockerHost + ":5000", "testimage", httpClient) .newRegistryClient(); Assert.assertFalse(registryClient.pushBlob(testBlobDigest, testBlob, null, ignored -> {})); } } ================================================ FILE: jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/LocalRegistry.java ================================================ /* * 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.Command; import com.google.common.collect.Lists; import java.io.IOException; 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.mindrot.jbcrypt.BCrypt; /** Runs a local registry. */ public class LocalRegistry extends ExternalResource { 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 IOException, InterruptedException { start(); } @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 { logout(); new Command("docker", "stop", containerName).run(); } catch (InterruptedException | IOException ex) { throw new RuntimeException("Could not stop local registry fully: " + containerName, ex); } } /** * 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)); } } 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); } } } ================================================ FILE: jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestCheckerIntegrationTest.java ================================================ /* * 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); } } ================================================ FILE: jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPullerIntegrationTest.java ================================================ /* * 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 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.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(EventHandlers.NONE, dockerHost + ":5000", "busybox", httpClient) .newRegistryClient(); V21ManifestTemplate manifestTemplate = registryClient.pullManifest("latest", V21ManifestTemplate.class).getManifest(); assertThat(manifestTemplate.getSchemaVersion()).isEqualTo(1); assertThat(manifestTemplate.getFsLayers()).isNotEmpty(); } @Test public void testPull_v22() throws IOException, RegistryException { RegistryClient registryClient = 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(); // 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(EventHandlers.NONE, dockerHost + ":5000", "busybox", httpClient) .newRegistryClient(); registryClient.pullManifest("nonexistent-tag"); Assert.fail("Trying to pull nonexistent image should have errored"); } catch (RegistryErrorException ex) { assertThat(ex) .hasMessageThat() .contains("pull image manifest for " + dockerHost + ":5000/busybox:nonexistent-tag"); } } } ================================================ FILE: jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/ManifestPusherIntegrationTest.java ================================================ /* * 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.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.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; import java.security.DigestException; import org.junit.Assert; import org.junit.ClassRule; import org.junit.Test; /** Integration tests for {@link ManifestPusher}. */ public class ManifestPusherIntegrationTest { @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(EventHandlers.NONE, "gcr.io", "distroless/java", httpClient) .newRegistryClient(); ManifestTemplate manifestTemplate = registryClient.pullManifest("latest").getManifest(); registryClient = RegistryClient.factory(EventHandlers.NONE, dockerHost + ":5000", "ignored", httpClient) .newRegistryClient(); try { registryClient.pushManifest(manifestTemplate, "latest"); Assert.fail("Pushing manifest without its BLOBs should fail"); } catch (RegistryErrorException ex) { ResponseException responseException = (ResponseException) ex.getCause(); Assert.assertEquals( HttpStatusCodes.STATUS_CODE_BAD_REQUEST, responseException.getStatusCode()); } } /** Tests manifest pushing. This test is a comprehensive test of push and pull. */ @Test public void testPush() throws DigestException, IOException, RegistryException { Blob testLayerBlob = Blobs.from("crepecake"); // Known digest for 'crepecake' DescriptorDigest testLayerBlobDigest = DescriptorDigest.fromHash( "52a9e4d4ba4333ce593707f98564fee1e6d898db0d3602408c0b2a6a424d357c"); Blob testContainerConfigurationBlob = Blobs.from("12345"); DescriptorDigest testContainerConfigurationBlobDigest = DescriptorDigest.fromHash( "5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5"); // Creates a valid image manifest. V22ManifestTemplate expectedManifestTemplate = new V22ManifestTemplate(); expectedManifestTemplate.addLayer(9, testLayerBlobDigest); expectedManifestTemplate.setContainerConfiguration(5, testContainerConfigurationBlobDigest); // Pushes the BLOBs. RegistryClient registryClient = RegistryClient.factory(EventHandlers.NONE, dockerHost + ":5000", "testimage", httpClient) .newRegistryClient(); Assert.assertFalse( registryClient.pushBlob(testLayerBlobDigest, testLayerBlob, null, ignored -> {})); Assert.assertFalse( registryClient.pushBlob( testContainerConfigurationBlobDigest, testContainerConfigurationBlob, null, ignored -> {})); // Pushes the manifest. DescriptorDigest imageDigest = registryClient.pushManifest(expectedManifestTemplate, "latest"); // Pulls the manifest. V22ManifestTemplate manifestTemplate = 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)); } } ================================================ FILE: jib-core/src/integration-test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperIntegrationTest.java ================================================ /* * 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.credentials; import com.google.cloud.tools.jib.Command; 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, CredentialHelperUnhandledServerUrlException, CredentialHelperNotFoundException, URISyntaxException, InterruptedException { new Command(GCR_CREDENTIAL_HELPER, "store") .run(Files.readAllBytes(Paths.get(Resources.getResource("credentials.json").toURI()))); DockerCredentialHelper dockerCredentialHelper = new DockerCredentialHelper("myregistry", Paths.get(GCR_CREDENTIAL_HELPER)); Credential credentials = dockerCredentialHelper.retrieve(); Assert.assertEquals("myusername", credentials.getUsername()); Assert.assertEquals("mysecret", credentials.getPassword()); } @Test public void testRetrieve_nonexistentCredentialHelper() throws IOException, CredentialHelperUnhandledServerUrlException { try { DockerCredentialHelper fakeDockerCredentialHelper = new DockerCredentialHelper("", Paths.get("non-existing-helper")); fakeDockerCredentialHelper.retrieve(); Assert.fail("Retrieve should have failed for nonexistent credential helper"); } catch (CredentialHelperNotFoundException ex) { Assert.assertEquals("The system does not have non-existing-helper CLI", ex.getMessage()); } } @Test public void testRetrieve_nonexistentServerUrl() throws IOException, CredentialHelperNotFoundException { try { DockerCredentialHelper fakeDockerCredentialHelper = new DockerCredentialHelper("fake.server.url", Paths.get(GCR_CREDENTIAL_HELPER)); fakeDockerCredentialHelper.retrieve(); Assert.fail("Retrieve should have failed for nonexistent server URL"); } catch (CredentialHelperUnhandledServerUrlException ex) { MatcherAssert.assertThat( ex.getMessage(), CoreMatchers.containsString( "The credential helper (docker-credential-gcr) has nothing for server URL: fake.server.url")); } } } ================================================ FILE: jib-core/src/integration-test/resources/core/hello ================================================ Hello World ================================================ FILE: jib-core/src/integration-test/resources/credentials.json ================================================ { "ServerURL": "myregistry", "Username": "myusername", "Secret": "mysecret" } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/ProjectInfo.java ================================================ /* * 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; /** Constants relating to the Jib project. */ public class ProjectInfo { /** Link to the GitHub repository. */ public static final String GITHUB_URL = "https://github.com/GoogleContainerTools/jib"; /** Link to file an issue against the GitHub repository. */ public static final String GITHUB_NEW_ISSUE_URL = GITHUB_URL + "/issues/new"; private ProjectInfo() {} } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/api/CacheDirectoryCreationException.java ================================================ /* * 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; /** 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"; public CacheDirectoryCreationException(Throwable cause) { super(MESSAGE, cause); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/api/Containerizer.java ================================================ /* * 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(); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/api/Credential.java ================================================ /* * 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; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/api/CredentialRetriever.java ================================================ /* * 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; } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/api/DescriptorDigest.java ================================================ /* * 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 com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.google.cloud.tools.jib.image.json.DescriptorDigestDeserializer; import com.google.cloud.tools.jib.image.json.DescriptorDigestSerializer; import java.security.DigestException; /** * Represents a SHA-256 content descriptor digest as defined by the Registry HTTP API v2 reference. * * @see https://docs.docker.com/registry/spec/api/#content-digests * @see OCI * Content Descriptor Digest */ @JsonSerialize(using = DescriptorDigestSerializer.class) @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 = String.format("[a-f0-9]{%d}", HASH_LENGTH); /** The algorithm prefix for the digest string. */ private static final String DIGEST_PREFIX = "sha256:"; /** Pattern matches a SHA-256 digest - a SHA-256 hash prefixed with "sha256:". */ static final String DIGEST_REGEX = DIGEST_PREFIX + HASH_REGEX; private final String hash; /** * Creates a new instance from a valid hash string. * * @param hash the hash to generate the {@link DescriptorDigest} from * @return a new {@link DescriptorDigest} created from the hash * @throws DigestException if the hash is invalid */ public static DescriptorDigest fromHash(String hash) throws DigestException { if (!hash.matches(HASH_REGEX)) { throw new DigestException("Invalid hash: " + hash); } return new DescriptorDigest(hash); } /** * Creates a new instance from a valid digest string. * * @param digest the digest to generate the {@link DescriptorDigest} from * @return a new {@link DescriptorDigest} created from the digest * @throws DigestException if the digest is invalid */ public static DescriptorDigest fromDigest(String digest) throws DigestException { if (!digest.matches(DIGEST_REGEX)) { throw new DigestException("Invalid digest: " + digest); } // Extracts the hash portion of the digest. String hash = digest.substring(DIGEST_PREFIX.length()); return new DescriptorDigest(hash); } private DescriptorDigest(String hash) { this.hash = hash; } public String getHash() { return hash; } @Override public String toString() { return DIGEST_PREFIX + hash; } /** Pass-through hash code of the digest string. */ @Override public int hashCode() { return hash.hashCode(); } /** Two digest objects are equal if their digest strings are equal. */ @Override public boolean equals(Object obj) { if (obj instanceof DescriptorDigest) { return hash.equals(((DescriptorDigest) obj).hash); } return false; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerClient.java ================================================ /* * 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(); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerDaemonImage.java ================================================ /* * 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; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/api/DockerInfoDetails.java ================================================ /* * 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; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/api/ImageDetails.java ================================================ /* * 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; } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/api/ImageReference.java ================================================ /* * 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); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/api/InsecureRegistryException.java ================================================ /* * 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.net.URL; /** * Throw when attempting to access an insecure registry when only secure connections are allowed. */ public class InsecureRegistryException extends RegistryException { /** * 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); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/api/InvalidImageReferenceException.java ================================================ /* * 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; /** 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; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java ================================================ /* * 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); }); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/api/Jib.java ================================================ /* * 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() {} } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainer.java ================================================ /* * 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; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerBuilder.java ================================================ /* * 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())); } } } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/api/JibContainerDescription.java ================================================ /* * 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; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/api/JibEvent.java ================================================ /* * 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 {} ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/api/LayerConfiguration.java ================================================ /* * 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; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/api/LayerEntry.java ================================================ /* * 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; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/api/LogEvent.java ================================================ /* * 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 + "]"; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/api/MainClassFinder.java ================================================ /* * 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); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/api/Ports.java ================================================ /* * 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.Port; import com.google.common.base.Strings; 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 Docker/OCI ports from text representations. */ public class Ports { /** * Pattern used for parsing information out of exposed port configurations. * *

Example matches: 100, 200-210, 1000/tcp, 2000/udp, 500-600/tcp */ private static final Pattern portPattern = Pattern.compile("(\\d+)(?:-(\\d+))?(?:/(tcp|udp))?"); /** * Converts/validates a list of strings representing port ranges to an expanded list of {@link * Port}s. * *

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, 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 Set parse(List ports) throws NumberFormatException { Set result = new HashSet<>(); for (String port : ports) { Matcher matcher = portPattern.matcher(port); if (!matcher.matches()) { throw new NumberFormatException( "Invalid port configuration: '" + port + "'. Make sure the port is a single number or a range of two numbers separated " + "with a '-', with or without protocol specified (e.g. '/tcp' or " + "'/udp')."); } // Parse protocol int min = Integer.parseInt(matcher.group(1)); int max = min; if (!Strings.isNullOrEmpty(matcher.group(2))) { max = Integer.parseInt(matcher.group(2)); } String protocol = matcher.group(3); // Error if configured as 'max-min' instead of 'min-max' if (min > max) { throw new NumberFormatException( "Invalid port range '" + port + "'; smaller number must come first."); } // Warn for possibly invalid port numbers if (min < 1 || max > 65535) { throw new NumberFormatException( "Port number '" + port + "' is out of usual range (1-65535)."); } for (int portNumber = min; portNumber <= max; portNumber++) { result.add(Port.parseProtocol(portNumber, protocol)); } } return result; } private Ports() {} } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryAuthenticationFailedException.java ================================================ /* * 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; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryException.java ================================================ /* * 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 javax.annotation.Nullable; /** Thrown when interacting with a registry. */ public class RegistryException extends Exception { public RegistryException(String message, @Nullable Throwable cause) { super(message, cause); } public RegistryException(String message) { super(message); } public RegistryException(Throwable cause) { super(cause); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryImage.java ================================================ /* * 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; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/api/RegistryUnauthorizedException.java ================================================ /* * 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 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 { private final String registry; private final String repository; /** * Identifies the image registry and repository that denied access. * * @param registry the image registry * @param repository the image repository * @param cause the cause */ public RegistryUnauthorizedException( String registry, String repository, ResponseException cause) { super("Unauthorized for " + registry + "/" + repository, cause); this.registry = registry; this.repository = repository; } public String getImageReference() { return registry + "/" + repository; } public HttpResponseException getHttpResponseException() { return (HttpResponseException) getCause().getCause(); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/api/TarImage.java ================================================ /* * 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); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blob.java ================================================ /* * 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 java.io.IOException; import java.io.OutputStream; /** Holds a BLOB source for writing to an {@link OutputStream}. */ public interface Blob { /** * Writes the BLOB to an {@link OutputStream}. Does not close the {@code outputStream}. * * @param outputStream the {@link OutputStream} to write to * @return the {@link BlobDescriptor} of the written 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(); } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/blob/BlobDescriptor.java ================================================ /* * 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.blob; 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 { private final DescriptorDigest digest; /** The size of the BLOB (in bytes). Negative if unknown. */ private final long size; public BlobDescriptor(long size, DescriptorDigest digest) { this.size = size; this.digest = digest; } /** * Initialize with just digest. * * @param digest the digest to initialize the {@link BlobDescriptor} from */ public BlobDescriptor(DescriptorDigest digest) { this(-1, digest); } public boolean hasSize() { return size >= 0; } public DescriptorDigest getDigest() { return digest; } public long getSize() { return size; } /** * Checks if two {@link BlobDescriptor}s are equal. * *

Two blobs are equal if their: * *

    *
  1. {@code digest}s are not null and equal, and *
  2. {@code size}s are non-negative and equal *
*/ @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (size < 0 || !(obj instanceof BlobDescriptor)) { return false; } BlobDescriptor other = (BlobDescriptor) obj; return size == other.getSize() && digest.equals(other.getDigest()); } @Override public int hashCode() { int result = digest.hashCode(); result = 31 * result + (int) (size ^ (size >>> 32)); return result; } @Override public String toString() { return "digest: " + digest + ", size: " + size; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/blob/Blobs.java ================================================ /* * 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.WritableContents; import com.google.cloud.tools.jib.json.JsonTemplate; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Path; /** Static methods for {@link Blob}. */ public class Blobs { public static Blob from(InputStream inputStream) { return new InputStreamBlob(inputStream); } 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(WritableContents writable, boolean retryable) { return new WritableContentsBlob(writable, retryable); } /** * Writes the BLOB to a string with UTF-8 decoding. * * @param blob the BLOB to write * @return the BLOB contents as a string * @throws IOException if writing out the BLOB contents fails */ public static String writeToString(Blob blob) throws IOException { return new String(writeToByteArray(blob), StandardCharsets.UTF_8); } /** * Writes the BLOB to a byte array. * * @param blob the BLOB to write * @return the BLOB contents as a byte array * @throws IOException if writing out the BLOB contents fails */ public static byte[] writeToByteArray(Blob blob) throws IOException { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); blob.writeTo(byteArrayOutputStream); return byteArrayOutputStream.toByteArray(); } private Blobs() {} } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/blob/FileBlob.java ================================================ /* * 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 java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; /** A {@link Blob} that holds a {@link Path}. */ class FileBlob implements Blob { private final Path file; FileBlob(Path file) { this.file = file; } @Override public BlobDescriptor writeTo(OutputStream outputStream) throws IOException { try (InputStream fileIn = new BufferedInputStream(Files.newInputStream(file))) { return Digests.computeDigest(fileIn, outputStream); } } @Override public boolean isRetryable() { return true; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/blob/InputStreamBlob.java ================================================ /* * 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 java.io.IOException; import java.io.InputStream; import java.io.OutputStream; /** A {@link Blob} that holds an {@link InputStream}. */ class InputStreamBlob implements Blob { private final InputStream inputStream; /** Indicates if the {@link Blob} has already been written or not. */ private boolean isWritten = false; InputStreamBlob(InputStream inputStream) { this.inputStream = inputStream; } @Override public BlobDescriptor writeTo(OutputStream outStream) throws IOException { // Cannot rewrite. if (isWritten) { throw new IllegalStateException("Cannot rewrite Blob backed by an InputStream"); } try (InputStream inStream = this.inputStream) { return Digests.computeDigest(inStream, outStream); } finally { isWritten = true; } } @Override public boolean isRetryable() { return false; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/blob/JsonBlob.java ================================================ /* * 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.blob; 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 holds {@link JsonTemplate}. */ class JsonBlob implements Blob { private final JsonTemplate template; JsonBlob(JsonTemplate template) { this.template = template; } @Override public BlobDescriptor writeTo(OutputStream outputStream) throws IOException { return Digests.computeDigest(template, outputStream); } @Override public boolean isRetryable() { return true; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/blob/StringBlob.java ================================================ /* * 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 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}. Encodes in UTF-8 when writing in bytes. */ class StringBlob implements Blob { private final String content; StringBlob(String content) { this.content = content; } @Override public BlobDescriptor writeTo(OutputStream outputStream) throws IOException { try (InputStream stringIn = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))) { return Digests.computeDigest(stringIn, outputStream); } } @Override public boolean isRetryable() { return true; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/blob/WritableContentsBlob.java ================================================ /* * 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; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcher.java ================================================ /* * 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; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/builder/Timer.java ================================================ /* * 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()); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/builder/TimerEventDispatcher.java ================================================ /* * 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)); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/AuthenticatePushStep.java ================================================ /* * 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.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; /** * Authenticates push to a target registry using Docker Token Authentication. * * @see https://docs.docker.com/registry/spec/auth/token/ */ class AuthenticatePushStep implements Callable { @SuppressWarnings("InlineFormatString") private static final String DESCRIPTION = "Authenticating push to %s"; private final BuildContext buildContext; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; AuthenticatePushStep( BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory) { this.buildContext = buildContext; this.progressEventDispatcherFactory = progressEventDispatcherFactory; } @Override 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); 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 registryClient; } } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStep.java ================================================ /* * 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.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.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 java.io.IOException; import java.util.List; import java.util.Optional; import java.util.concurrent.Callable; /** Builds and caches application layers. */ class BuildAndCacheApplicationLayerStep implements Callable { @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( 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( buildContext, progressEventDispatcher.newChildProducer(), layerConfiguration.getName(), layerConfiguration)) .collect(ImmutableList.toImmutableList()); } } private final BuildContext buildContext; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; private final String layerName; private final FileEntriesLayer layerConfiguration; private BuildAndCacheApplicationLayerStep( BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory, String layerName, FileEntriesLayer layerConfiguration) { this.buildContext = buildContext; this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.layerName = layerName; this.layerConfiguration = layerConfiguration; } @Override public PreparedLayer call() throws IOException, CacheCorruptedException { String description = String.format(DESCRIPTION, layerName); EventHandlers eventHandlers = buildContext.getEventHandlers(); eventHandlers.dispatch(LogEvent.progress(description + "...")); try (ProgressEventDispatcher ignored = progressEventDispatcherFactory.create("building " + layerName + " layer", 1); TimerEventDispatcher ignored2 = new TimerEventDispatcher(eventHandlers, description)) { Cache cache = buildContext.getApplicationLayersCache(); ImmutableList layerEntries = ImmutableList.copyOf(layerConfiguration.getEntries()); // Don't build the layer if it exists already. Optional optionalCachedLayer = cache.retrieve(layerEntries); if (optionalCachedLayer.isPresent()) { return new PreparedLayer.Builder(optionalCachedLayer.get()).setName(layerName).build(); } Blob layerBlob = new ReproducibleLayerBuilder(layerEntries).build(); CachedLayer cachedLayer = cache.writeUncompressedLayer(layerBlob, layerEntries); eventHandlers.dispatch(LogEvent.debug(description + " built " + cachedLayer.getDigest())); return new PreparedLayer.Builder(cachedLayer).setName(layerName).build(); } } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildImageStep.java ================================================ /* * 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.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.collect.UnmodifiableIterator; import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; import javax.annotation.Nullable; /** Builds a model {@link Image}. */ class BuildImageStep implements Callable { private static final String DESCRIPTION = "Building container configuration"; @VisibleForTesting static String truncateLongClasspath(ImmutableList imageEntrypoint) { List truncated = new ArrayList<>(); UnmodifiableIterator iterator = imageEntrypoint.iterator(); while (iterator.hasNext()) { String element = iterator.next(); truncated.add(element); 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( 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 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()); // Base image layers baseImageLayers.forEach(imageBuilder::addLayer); // 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()); 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()); } // 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()); } // 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; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildManifestListOrSingleManifestStep.java ================================================ /* * 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()); } } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/BuildResult.java ================================================ /* * 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); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/CheckManifestStep.java ================================================ /* * 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()); } } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LoadDockerStep.java ================================================ /* * 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.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.cloud.tools.jib.image.ImageTarball; import java.io.IOException; import java.util.concurrent.Callable; /** Adds image layers to a tarball and loads into Docker daemon. */ class LoadDockerStep implements Callable { private final BuildContext buildContext; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; private final DockerClient dockerClient; private final Image builtImage; LoadDockerStep( BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory, DockerClient dockerClient, Image builtImage) { this.buildContext = buildContext; this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.dockerClient = dockerClient; this.builtImage = builtImage; } @Override 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()); // 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))); return BuildResult.fromImage(builtImage, buildContext.getTargetFormat()); } } } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageSteps.java ================================================ /* * 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(); } } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStep.java ================================================ /* * 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(); } } } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PlatformChecker.java ================================================ /* * 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); } } } } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PreparedLayer.java ================================================ /* * 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(); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStep.java ================================================ /* * 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.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.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.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.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.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.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 javax.annotation.Nullable; /** 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 ImagesAndRegistryClient { final List images; @Nullable final RegistryClient registryClient; ImagesAndRegistryClient(List images, @Nullable RegistryClient registryClient) { this.images = images; this.registryClient = registryClient; } } private final BuildContext buildContext; private final ProgressEventDispatcher.Factory progressDispatcherFactory; PullBaseImageStep( BuildContext buildContext, ProgressEventDispatcher.Factory progressDispatcherFactory) { this.buildContext = buildContext; this.progressDispatcherFactory = progressDispatcherFactory; } @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)) { // 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()); 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); } eventHandlers.dispatch( LogEvent.progress("Getting manifest for base image " + imageReference + "...")); 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(); } try { // 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) { 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. } } 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 images specified in the platforms list. * * @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 * information * @throws LayerPropertyNotFoundException if adding image layers fails * @throws BadContainerConfigurationFormatException if the container configuration is in a bad * format */ private List pullBaseImages( RegistryClient registryClient, ProgressEventDispatcher.Factory progressDispatcherFactory) throws IOException, RegistryException, LayerPropertyNotFoundException, LayerCountMismatchException, BadContainerConfigurationFormatException { 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)); } } 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); 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)); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java ================================================ /* * 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.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.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 java.io.IOException; import java.util.concurrent.Callable; /** Pushes a BLOB to the target registry. */ class PushBlobStep implements Callable { private static final String DESCRIPTION = "Pushing BLOB "; private final BuildContext buildContext; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; private final RegistryClient registryClient; private final BlobDescriptor blobDescriptor; private final Blob blob; private final boolean forcePush; PushBlobStep( BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory, RegistryClient registryClient, BlobDescriptor blobDescriptor, Blob blob, boolean forcePush) { this.buildContext = buildContext; this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.registryClient = registryClient; this.blobDescriptor = blobDescriptor; this.blob = blob; this.forcePush = forcePush; } @Override 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)) { // 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; } // 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; } } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushContainerConfigurationStep.java ================================================ /* * 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.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.cloud.tools.jib.json.JsonTemplate; import com.google.cloud.tools.jib.registry.RegistryClient; import java.io.IOException; import java.util.concurrent.Callable; /** Pushes the container configuration. */ class PushContainerConfigurationStep implements Callable { private static final String DESCRIPTION = "Pushing container configuration"; private final BuildContext buildContext; private final ProgressEventDispatcher.Factory progressEventDispatcherFactory; private final RegistryClient registryClient; private final Image builtImage; PushContainerConfigurationStep( BuildContext buildContext, ProgressEventDispatcher.Factory progressEventDispatcherFactory, RegistryClient registryClient, Image builtImage) { this.buildContext = buildContext; this.progressEventDispatcherFactory = progressEventDispatcherFactory; this.registryClient = registryClient; this.builtImage = builtImage; } @Override 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( buildContext, progressEventDispatcher.newChildProducer(), registryClient, Digests.computeDigest(containerConfiguration), Blobs.from(containerConfiguration), false) .call(); } } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushImageStep.java ================================================ /* * 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.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.common.collect.ImmutableList; import java.io.IOException; import java.util.Collections; import java.util.Set; import java.util.concurrent.Callable; import java.util.stream.Collectors; /** * 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()); } } 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; } if (buildContext.getEnablePlatformTags()) { String architecture = builtImage.getArchitecture(); return tags.stream().map(tag -> tag + "-" + architecture).collect(Collectors.toSet()); } return Collections.singleton(manifestDigest.toString()); } 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()); } } 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; 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); } } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushLayerStep.java ================================================ /* * 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(); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/RegistryCredentialRetriever.java ================================================ /* * 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(); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/StepsRunner.java ================================================ /* * 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.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.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.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 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 { /** 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(); } /** * 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); } private static List realizeFutures(Collection> futures) throws InterruptedException, ExecutionException { List values = new ArrayList<>(); for (Future future : futures) { values.add(future.get()); } return values; } 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; } /** * 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; } /** * 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; } /** * 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; } /** * 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(); } } 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)); } } private void authenticateBearerPush(ProgressEventDispatcher.Factory progressDispatcherFactory) { results.targetRegistryClient = executorService.submit(new AuthenticatePushStep(buildContext, progressDispatcherFactory)); } 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))); } 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))); } 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)); } @VisibleForTesting void pullBaseImages(ProgressEventDispatcher.Factory progressDispatcherFactory) { results.baseImagesAndRegistryClient = executorService.submit(new PullBaseImageStep(buildContext, progressDispatcherFactory)); } 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; } }); } // 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; } } 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 results.baseImagesAndBuiltImages.get().values().iterator().next().get(); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/ThrottledProgressEventDispatcherWrapper.java ================================================ /* * 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); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/WriteTarFileStep.java ================================================ /* * 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()); } } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/cache/Cache.java ================================================ /* * 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.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.nio.file.Files; import java.nio.file.Path; import java.util.Collections; import java.util.List; import java.util.Optional; import javax.annotation.concurrent.Immutable; /** * 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 the cache using {@code cacheDirectory} for storage. * * @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 withDirectory(Path cacheDirectory) throws CacheDirectoryCreationException { try { Files.createDirectories(cacheDirectory); } catch (IOException ex) { throw new CacheDirectoryCreationException(ex); } return new Cache(new CacheStorageFiles(cacheDirectory)); } private final CacheStorageWriter cacheStorageWriter; private final CacheStorageReader cacheStorageReader; private Cache(CacheStorageFiles cacheStorageFiles) { cacheStorageWriter = new CacheStorageWriter(cacheStorageFiles); cacheStorageReader = new CacheStorageReader(cacheStorageFiles); } /** * 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); } /** * 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)); } /** * 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)); } /** * 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); } /** * 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}. * * @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 */ public CachedLayer writeUncompressedLayer( Blob uncompressedLayerBlob, ImmutableList layerEntries) throws IOException { return cacheStorageWriter.writeUncompressed( uncompressedLayerBlob, LayerEntriesSelector.generateSelector(layerEntries)); } /** * Caches a layer that was extracted from a local base image, and names the file using the * provided diff id. * * @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 CachedLayer writeTarLayer(DescriptorDigest diffId, Blob compressedBlob) throws IOException { return cacheStorageWriter.writeTarLayer(diffId, compressedBlob); } /** * 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}. * *

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 writeLocalConfig( DescriptorDigest imageId, ContainerConfigurationTemplate containerConfiguration) throws IOException { cacheStorageWriter.writeLocalConfig(imageId, containerConfiguration); } /** * 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); } /** * 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); } /** * 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()); } /** * 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); } /** * 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); } /** * 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); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheCorruptedException.java ================================================ /* * 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 java.nio.file.Path; /** Thrown if the the cache was found to be corrupted. */ public class CacheCorruptedException extends Exception { CacheCorruptedException(Path cacheDirectory, String message, Throwable cause) { super( message + ". You may need to clear the cache by deleting the '" + cacheDirectory + "' directory", cause); } CacheCorruptedException(Path cacheDirectory, String message) { super( message + ". You may need to clear the cache by deleting the '" + cacheDirectory + "' directory"); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageFiles.java ================================================ /* * 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; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageReader.java ================================================ /* * 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); } } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageWriter.java ================================================ /* * 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: * *

    *
  • The {@code uncompressedLayerBlob} is written to the layer directory under the layers * directory corresponding to the layer blob. *
  • The {@code selector} is written to the selector file under the selectors directory. *
* * @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); } } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/cache/CachedLayer.java ================================================ /* * 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.blob.Blob; import com.google.cloud.tools.jib.blob.BlobDescriptor; import com.google.cloud.tools.jib.image.Layer; import com.google.common.base.Preconditions; import javax.annotation.Nullable; /** A reference to an image layer that is in the Cache. */ public class CachedLayer implements Layer { /** 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")); } } /** * Creates a new {@link Builder} for a {@link CachedLayer}. * * @return the new {@link Builder} */ public static Builder builder() { return new Builder(); } 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 DescriptorDigest getDiffId() { return layerDiffId; } @Override public Blob getBlob() { return layerBlob; } @Override public BlobDescriptor getBlobDescriptor() { return blobDescriptor; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/cache/LayerEntriesSelector.java ================================================ /* * 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() {} } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/cache/Retry.java ================================================ /* * 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; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildContext.java ================================================ /* * 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(); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ContainerConfiguration.java ================================================ /* * 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); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/configuration/DockerHealthCheck.java ================================================ /* * 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); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/configuration/ImageConfiguration.java ================================================ /* * 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); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/docker/CliDockerClient.java ================================================ /* * 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); } } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/docker/DockerClientResolver.java ================================================ /* * 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(); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/docker/json/DockerManifestEntryTemplate.java ================================================ /* * 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.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.common.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.List; /** * 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". * *

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": [
 *     "eb05f3dbdb543cc610527248690575bacbbcebabe6ecf665b189cf18b541e3ca.tar.gz",
 *     "ba7c544469e514f1a9a4dec59ab640540d50992b288adbb34a1a63c45bf19a24.tar.gz",
 *     "15705ab016593987662839b40f5a22fd1032996c90808d4a1371eb46974017d5.tar.gz"
 *   ]
 * }
 * }
* * @see Docker load * source */ @JsonIgnoreProperties(ignoreUnknown = true) public class DockerManifestEntryTemplate implements JsonTemplate { @JsonProperty("Config") private String config = "config.json"; @JsonProperty("RepoTags") private final List repoTags = new ArrayList<>(); @JsonProperty("Layers") private final List layers = new ArrayList<>(); public void setConfig(String config) { this.config = config; } public void addRepoTag(String repoTag) { repoTags.add(repoTag); } public void addLayerFile(String layer) { layers.add(layer); } public String getConfig() { return config; } public List getLayerFiles() { return layers; } @VisibleForTesting public List getRepoTags() { return repoTags; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/event/EventHandlers.java ================================================ /* * 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); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/event/Handler.java ================================================ /* * 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)); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/event/events/ProgressEvent.java ================================================ /* * 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; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/event/events/TimerEvent.java ================================================ /* * 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; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/Allocation.java ================================================ /* * 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; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/AllocationCompletionTracker.java ================================================ /* * 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())); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandler.java ================================================ /* * 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())); } } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/event/progress/ThrottledAccumulatingConsumer.java ================================================ /* * 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); } } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/DirectoryWalker.java ================================================ /* * 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.ImmutableList; import java.io.IOException; import java.nio.file.Files; import java.nio.file.NotDirectoryException; import java.nio.file.Path; import java.util.function.Predicate; import java.util.stream.Stream; /** Recursively applies a function to each file in a directory. */ public class DirectoryWalker { private final Path rootDir; private Predicate pathFilter = path -> true; /** * Initialize with a root directory to walk. * * @param rootDir the root directory. * @throws NotDirectoryException if the root directory is not a directory. */ public DirectoryWalker(Path rootDir) throws NotDirectoryException { if (!Files.isDirectory(rootDir)) { throw new NotDirectoryException(rootDir + " is not a directory"); } this.rootDir = rootDir; } /** * Adds a filter to the walked paths. * * @param pathFilter the filter. {@code pathFilter} returns {@code true} if the path should be * accepted and {@code false} otherwise. * @return this */ public DirectoryWalker filter(Predicate pathFilter) { this.pathFilter = this.pathFilter.and(pathFilter); return this; } /** * Filters away the {@code rootDir}. * * @return this */ public DirectoryWalker filterRoot() { filter(path -> !path.equals(rootDir)); return this; } /** * Walks {@link #rootDir} and applies {@code pathConsumer} to each file. Note that {@link * #rootDir} itself is visited as well. * * @param pathConsumer the consumer that is applied to each file. * @return a list of Paths that were walked. * @throws IOException if the walk fails. */ public ImmutableList walk(PathConsumer pathConsumer) throws IOException { ImmutableList files = walk(); for (Path path : files) { pathConsumer.accept(path); } return files; } /** * Walks {@link #rootDir}. * * @return the walked files. * @throws IOException if walking the files fails. */ public ImmutableList walk() throws IOException { try (Stream fileStream = Files.walk(rootDir)) { return fileStream.filter(pathFilter).sorted().collect(ImmutableList.toImmutableList()); } } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/FileOperations.java ================================================ /* * 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.base.Verify; import com.google.common.collect.ImmutableList; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; /** Static methods for operating on the filesystem. */ public class FileOperations { /** * Copies {@code sourceFiles} to the {@code destDir} directory. * * @param sourceFiles the list of source files. * @param destDir the directory to copy the files to. * @throws IOException if the copy fails. */ public static void copy(ImmutableList sourceFiles, Path destDir) throws IOException { for (Path sourceFile : sourceFiles) { PathConsumer copyPathConsumer = path -> { // Creates the same path in the destDir. Path parent = Verify.verifyNotNull(sourceFile.getParent()); Path destPath = destDir.resolve(parent.relativize(path)); if (Files.isDirectory(path)) { Files.createDirectories(destPath); } else { Files.copy(path, destPath); } }; if (Files.isDirectory(sourceFile)) { new DirectoryWalker(sourceFile).walk(copyPathConsumer); } else { copyPathConsumer.accept(sourceFile); } } } private FileOperations() {} } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/LockFile.java ================================================ /* * 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(); } } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/PathConsumer.java ================================================ /* * 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 java.io.IOException; import java.nio.file.Path; @FunctionalInterface public interface PathConsumer { void accept(Path path) throws IOException; } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/TempDirectoryProvider.java ================================================ /* * 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(); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/XdgDirectories.java ================================================ /* * 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() {} } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactory.java ================================================ /* * 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)); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java ================================================ /* * 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() {} } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStream.java ================================================ /* * 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 java.io.IOException; import java.io.OutputStream; import java.security.DigestException; import java.security.DigestOutputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; /** A {@link DigestOutputStream} that also keeps track of the total number of bytes written. */ public class CountingDigestOutputStream extends DigestOutputStream { private static final String SHA_256_ALGORITHM = "SHA-256"; private long bytesSoFar = 0; /** * Wraps the {@code outputStream}. * * @param outputStream the {@link OutputStream} to wrap. */ public CountingDigestOutputStream(OutputStream outputStream) { super(outputStream, null); try { setMessageDigest(MessageDigest.getInstance(SHA_256_ALGORITHM)); } catch (NoSuchAlgorithmException ex) { throw new RuntimeException( "SHA-256 algorithm implementation not found - might be a broken JVM"); } } /** * 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 computed hash and the size of the bytes consumed */ public BlobDescriptor computeDigest() { try { byte[] hashedBytes = digest.digest(); // Encodes each hashed byte into 2-character hexadecimal representation. StringBuilder stringBuilder = new StringBuilder(2 * hashedBytes.length); for (byte b : hashedBytes) { stringBuilder.append(String.format("%02x", b)); } String hash = stringBuilder.toString(); 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); } } @Override public void write(byte[] data, int offset, int length) throws IOException { super.write(data, offset, length); bytesSoFar += length; } @Override public void write(int singleByte) throws IOException { super.write(singleByte); bytesSoFar++; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/hash/Digests.java ================================================ /* * 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(); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/hash/WritableContents.java ================================================ /* * 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.hash; import java.io.IOException; import java.io.OutputStream; /** * 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 WritableContents { void writeTo(OutputStream outputStream) throws IOException; } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java ================================================ /* * 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 java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Objects; /** * Holds the credentials for an HTTP {@code Authorization} header. * *

The HTTP {@code Authorization} header is in the format: * *

{@code Authorization:  }
*/ 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; private Authorization(String scheme, String token) { this.scheme = scheme; this.token = token; } public String getScheme() { return scheme; } public String getToken() { return token; } /** Return the HTTP {@link Authorization} header value. */ @Override 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); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/http/BlobHttpContent.java ================================================ /* * 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.HttpContent; 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 public long getLength() { // Returns negative value for unknown length. return -1; } @Override public String getType() { return contentType; } @Override public boolean retrySupported() { return blob.isRetryable(); } @Override public void writeTo(OutputStream outputStream) throws IOException { blob.writeTo(new NotifyingOutputStream(outputStream, writtenByteCountListener)); outputStream.flush(); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/http/FailoverHttpClient.java ================================================ /* * 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; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/http/NotifyingOutputStream.java ================================================ /* * 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; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/http/Request.java ================================================ /* * 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.HttpContent; import com.google.api.client.http.HttpHeaders; import java.util.List; import javax.annotation.Nullable; /** Holds an HTTP request. */ public class Request { /** The HTTP request headers. */ private final HttpHeaders headers; /** The HTTP request 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 HttpContent body; @Nullable private Integer httpTimeout; public Request build() { return new Request(this); } /** * Sets the {@code Authorization} header. * * @param authorization the authorization * @return this */ public Builder setAuthorization(@Nullable Authorization authorization) { headers.setAuthorization(authorization == null ? null : authorization.toString()); return this; } /** * Sets the {@code Accept} header. * * @param mimeTypes the items to pass into the accept header * @return this */ public Builder setAccept(List mimeTypes) { headers.setAccept(String.join(",", mimeTypes)); return this; } /** * Sets the {@code User-Agent} header. * * @param userAgent the user agent * @return this */ 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 httpContent the body content * @return this */ public Builder setBody(@Nullable HttpContent httpContent) { this.body = httpContent; return this; } } public static Builder builder() { return new Builder(); } private Request(Builder builder) { this.headers = builder.headers; this.body = builder.body; this.httpTimeout = builder.httpTimeout; } HttpHeaders getHeaders() { return headers; } @Nullable HttpContent getHttpContent() { return body; } @Nullable Integer getHttpTimeout() { return httpTimeout; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/http/Response.java ================================================ /* * 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.HttpResponse; 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 implements Closeable { private final HttpResponse httpResponse; Response(HttpResponse httpResponse) { this.httpResponse = httpResponse; } /** * 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 */ public List getHeader(String headerName) { return httpResponse.getHeaders().getHeaderStringValues(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 */ public long getContentLength() throws NumberFormatException { String contentLengthHeader = httpResponse.getHeaders().getFirstHeaderStringValue(HttpHeaders.CONTENT_LENGTH); if (contentLengthHeader == null) { return -1; } try { return Long.parseLong(contentLengthHeader); } catch (NumberFormatException ex) { return -1; } } /** * 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 InputStream getBody() throws IOException { return httpResponse.getContent(); } /** * 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(); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/http/ResponseException.java ================================================ /* * 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; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/image/DigestOnlyLayer.java ================================================ /* * 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.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; /** A {@link Layer} reference that only has its {@link DescriptorDigest}. */ public class DigestOnlyLayer implements Layer { /** The {@link BlobDescriptor} of the compressed layer content. */ private final BlobDescriptor blobDescriptor; /** * Instantiate with a {@link DescriptorDigest}. * * @param digest the digest to instantiate the {@link DigestOnlyLayer} from */ public DigestOnlyLayer(DescriptorDigest digest) { blobDescriptor = new BlobDescriptor(digest); } @Override public Blob getBlob() throws LayerPropertyNotFoundException { throw new LayerPropertyNotFoundException("Blob not available for digest-only layer"); } @Override public BlobDescriptor getBlobDescriptor() { return blobDescriptor; } @Override public DescriptorDigest getDiffId() throws LayerPropertyNotFoundException { throw new LayerPropertyNotFoundException("Diff ID not available for digest-only layer"); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java ================================================ /* * 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.image; 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 { /** Builds the immutable {@link Image}. */ public static class Builder { private final Class imageFormat; private final ImmutableList.Builder imageLayersBuilder = ImmutableList.builder(); private final ImmutableList.Builder historyBuilder = ImmutableList.builder(); // 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 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 addEnvironment(@Nullable Map environment) { if (environment != null) { environmentBuilder.putAll(environment); } return this; } /** * Adds an environment variable with a given name and value. * * @param name the name of the variable * @param value the value to set it to * @return this */ public Builder addEnvironmentVariable(String name, String value) { environmentBuilder.put(name, value); return this; } /** * Sets the entrypoint of the image. * * @param entrypoint the list of entrypoint tokens * @return this */ public Builder setEntrypoint(@Nullable List entrypoint) { this.entrypoint = (entrypoint == null) ? null : ImmutableList.copyOf(entrypoint); return this; } /** * Sets the user/group to run the container as. * * @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 addLabels(@Nullable Map labels) { if (labels != null) { labelsBuilder.putAll(labels); } return this; } /** * Adds an item to the "Labels" field in the container configuration. * * @param name the name of the label * @param value the value of the label * @return this */ public Builder addLabel(String name, String value) { labelsBuilder.put(name, value); return this; } /** * Sets the item in the "WorkingDir" field in the container configuration. * * @param workingDirectory the working directory * @return this */ public Builder setWorkingDirectory(@Nullable String workingDirectory) { this.workingDirectory = workingDirectory; return this; } /** * Adds a layer to the image. * * @param layer the layer to add * @return this * @throws LayerPropertyNotFoundException if adding the layer fails */ public Builder addLayer(Layer layer) throws LayerPropertyNotFoundException { imageLayersBuilder.add(layer); return this; } /** * 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(), historyBuilder.build(), ImmutableMap.copyOf(environmentBuilder), entrypoint, programArguments, healthCheck, ImmutableSet.copyOf(exposedPortsBuilder), ImmutableSet.copyOf(volumesBuilder), ImmutableMap.copyOf(labelsBuilder), workingDirectory, user); } } 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 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}. */ @Nullable private final ImmutableMap environment; /** Initial command to run when running the image. */ @Nullable private final ImmutableList entrypoint; /** Arguments to append to the image entrypoint when running the image. */ @Nullable private final ImmutableList programArguments; /** Healthcheck configuration. */ @Nullable private final DockerHealthCheck healthCheck; /** Ports that the container listens on. */ @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( 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.history = history; this.environment = environment; this.entrypoint = entrypoint; 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; } @Nullable public ImmutableMap getEnvironment() { return environment; } @Nullable public ImmutableList getEntrypoint() { return entrypoint; } @Nullable public ImmutableList getProgramArguments() { return programArguments; } @Nullable public DockerHealthCheck getHealthCheck() { return healthCheck; } @Nullable public ImmutableSet getExposedPorts() { return exposedPorts; } @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; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/image/ImageTarball.java ================================================ /* * 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; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/image/Layer.java ================================================ /* * 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.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; /** * Represents a layer in an image. Implementations represent the various types of layers. * *

An image layer consists of: * *

    *
  • Content BLOB *
  • *
      *
    • The compressed archive (tarball gzip) of the partial filesystem changeset. *
    *
  • Content Digest *
  • *
      *
    • The SHA-256 hash of the content BLOB. *
    *
  • Content Size *
  • *
      *
    • The size (in bytes) of the content BLOB. *
    *
  • Diff ID *
  • *
      *
    • The SHA-256 hash of the uncompressed archive (tarball) of the partial filesystem * changeset. *
    *
*/ 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 */ DescriptorDigest getDiffId() throws LayerPropertyNotFoundException; } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerCountMismatchException.java ================================================ /* * 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.image; /** Exception thrown when the number of layers found did not match expectations. */ public class LayerCountMismatchException extends Exception { public LayerCountMismatchException(String message) { super(message); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/image/LayerPropertyNotFoundException.java ================================================ /* * 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.image; /** Exception thrown when accessing non-existent properties of layers. */ public class LayerPropertyNotFoundException extends RuntimeException { LayerPropertyNotFoundException(String message) { super(message); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/image/ReferenceLayer.java ================================================ /* * 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.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; /** * A {@link Layer} reference that does not have the underlying content. It references the * layer with its digest, size, and diff ID. */ public class ReferenceLayer implements Layer { /** The {@link BlobDescriptor} of the compressed layer content. */ private final BlobDescriptor blobDescriptor; /** The digest of the uncompressed layer content. */ private final DescriptorDigest diffId; /** * Instantiate with a {@link BlobDescriptor} and diff ID. * * @param blobDescriptor the blob descriptor * @param diffId the diff ID */ public ReferenceLayer(BlobDescriptor blobDescriptor, DescriptorDigest diffId) { this.blobDescriptor = blobDescriptor; this.diffId = diffId; } @Override public Blob getBlob() throws LayerPropertyNotFoundException { throw new LayerPropertyNotFoundException("Blob not available for reference layer"); } @Override public BlobDescriptor getBlobDescriptor() { return blobDescriptor; } @Override public DescriptorDigest getDiffId() { return diffId; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/image/ReferenceNoDiffIdLayer.java ================================================ /* * 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.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; /** * A {@link Layer} reference that does not have the underlying content. It references the * layer with its digest and size, but not its diff ID. */ public class ReferenceNoDiffIdLayer implements Layer { /** The {@link BlobDescriptor} of the compressed layer content. */ private final BlobDescriptor blobDescriptor; /** * Instantiate with a {@link BlobDescriptor} and no diff ID. * * @param blobDescriptor the blob descriptor */ public ReferenceNoDiffIdLayer(BlobDescriptor blobDescriptor) { this.blobDescriptor = blobDescriptor; } @Override public Blob getBlob() throws LayerPropertyNotFoundException { throw new LayerPropertyNotFoundException( "Blob not available for reference layer without diff ID"); } @Override public BlobDescriptor getBlobDescriptor() { return blobDescriptor; } @Override public DescriptorDigest getDiffId() throws LayerPropertyNotFoundException { throw new LayerPropertyNotFoundException( "Diff ID not available for reference layer without diff ID"); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilder.java ================================================ /* * 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.image; 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.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 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 { /** * Holds a list of {@link TarArchiveEntry}s with unique extraction paths. The list also includes * all parent directories for each extraction path. */ 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()); } private List getSortedEntries() { List sortedEntries = new ArrayList<>(entries); sortedEntries.sort(Comparator.comparing(TarArchiveEntry::getName)); return sortedEntries; } } private static void clearTimeHeaders(TarArchiveEntry entry, Instant modTime) { entry.setModTime(modTime.toEpochMilli()); 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); } 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 {@link Blob}. * * @return the new layer * @throws IOException if an I/O error occurs */ public Blob build() throws IOException { UniqueTarArchiveEntries uniqueTarArchiveEntries = new UniqueTarArchiveEntries(); // Adds all the layer entries as tar entries. 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); } // 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 : sortedFilesystemEntries) { Verify.verify(!names.contains(entry.getName())); names.add(entry.getName()); tarStreamBuilder.addTarArchiveEntry(entry); } return Blobs.from(tarStreamBuilder::writeAsTarArchiveTo, false); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BadContainerConfigurationFormatException.java ================================================ /* * 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; /** Exception thrown when trying to parse a bad image configuration format. */ public class BadContainerConfigurationFormatException extends Exception { // TODO: Potentially provide Path or source object to problem configuration file BadContainerConfigurationFormatException(String message) { super(message); } BadContainerConfigurationFormatException(String message, Throwable cause) { super(message, cause); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/image/json/BuildableManifestTemplate.java ================================================ /* * 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.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 Docker V2.2 format * @see OciManifestTemplate OCI format */ public interface BuildableManifestTemplate extends ManifestTemplate { /** * Template for inner JSON object representing content descriptor for a layer or container * configuration. * * @see OCI * Content Descriptors */ class ContentDescriptorTemplate implements JsonTemplate { @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; this.size = size; this.digest = digest; } /** Necessary for Jackson to create from JSON. */ @SuppressWarnings("unused") protected ContentDescriptorTemplate() {} public long getSize() { return size; } void setSize(long size) { this.size = size; } @Nullable public DescriptorDigest getDigest() { return digest; } 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); } } /** * 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(); /** * Returns the content descriptor of the container configuration. * * @return the content descriptor of the container configuration */ @Nullable ContentDescriptorTemplate getContainerConfiguration(); /** * Returns an unmodifiable view of the layers. * * @return an unmodifiable view of the layers */ List getLayers(); /** * Sets the content descriptor of the container configuration. * * @param size the size of the container configuration. * @param digest the container configuration content descriptor digest. */ void setContainerConfiguration(long size, DescriptorDigest digest); /** * Adds a layer to the manifest. * * @param size the size of the layer. * @param digest the layer descriptor digest. */ void addLayer(long size, DescriptorDigest digest); } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplate.java ================================================ /* * 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.image.json; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.common.base.Preconditions; import java.util.ArrayList; import java.util.List; import java.util.Map; import javax.annotation.Nullable; /** * JSON Template for Docker Container Configuration referenced in Docker Manifest Schema V2.2 * *

Example container config JSON: * *

{@code
 * {
 *   "created": "1970-01-01T00:00:00Z",
 *   "architecture": "amd64",
 *   "os": "linux",
 *   "config": {
 *     "Env": ["/usr/bin/java"],
 *     "Entrypoint": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],
 *     "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",
 *       "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef",
 *     ],
 *     "type": "layers"
 *   }
 * }
 * }
* * @see Image Manifest Version 2, * Schema 2 */ @JsonIgnoreProperties(ignoreUnknown = true) public class ContainerConfigurationTemplate implements JsonTemplate { /** 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"; /** The operating system to run the container on. */ private String os = "linux"; /** 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(); /** Template for inner JSON object representing the configuration for running the container. */ @JsonIgnoreProperties(ignoreUnknown = true) private static class ConfigurationObjectTemplate implements JsonTemplate { /** Environment variables in the format {@code VARNAME=VARVALUE}. */ @Nullable private List Env; /** Command to run when container starts. */ @Nullable private List Entrypoint; /** Arguments to pass into main. */ @Nullable private List Cmd; /** Healthcheck. */ @Nullable private HealthCheckObjectTemplate Healthcheck; /** Network ports the container exposes. */ @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; } /** * Template for inner JSON object representing the filesystem changesets used to build the * container filesystem. */ private static class RootFilesystemObjectTemplate implements JsonTemplate { /** The type must always be {@code "layers"}. */ @SuppressWarnings("unused") private final String type = "layers"; /** * The in-order list of layer content digests (hashes of the uncompressed partial filesystem * changeset). */ private final List diff_ids = new ArrayList<>(); } 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(@Nullable List command) { config.Entrypoint = command; } public void setContainerCmd(@Nullable List cmd) { config.Cmd = cmd; } /** * 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; } @Nullable List getContainerEntrypoint() { return config.Entrypoint; } @Nullable List getContainerCmd() { return config.Cmd; } @Nullable 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; } @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(); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/image/json/DescriptorDigestDeserializer.java ================================================ /* * 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.image.json; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import com.google.cloud.tools.jib.api.DescriptorDigest; import java.io.IOException; import java.security.DigestException; /** Deserializes a JSON element into a {@link DescriptorDigest} object. */ public class DescriptorDigestDeserializer extends JsonDeserializer { @Override public DescriptorDigest deserialize(JsonParser jsonParser, DeserializationContext ignored) throws IOException { try { return DescriptorDigest.fromDigest(jsonParser.getValueAsString()); } catch (DigestException ex) { throw new IOException(ex); } } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/image/json/DescriptorDigestSerializer.java ================================================ /* * 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.image.json; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import com.google.cloud.tools.jib.api.DescriptorDigest; import java.io.IOException; /** Serializes a {@link DescriptorDigest} into JSON element. */ public class DescriptorDigestSerializer extends JsonSerializer { @Override public void serialize( DescriptorDigest value, JsonGenerator jsonGenerator, SerializerProvider ignored) throws IOException { jsonGenerator.writeString(value.toString()); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/image/json/HistoryEntry.java ================================================ /* * 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; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageMetadataTemplate.java ================================================ /* * 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; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslator.java ================================================ /* * 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.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.DockerHealthCheck; import com.google.cloud.tools.jib.image.Image; 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.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. */ public class ImageToJsonTranslator { /** * 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 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, or {@code null} if {@code exposedPorts} is {@code null} */ @VisibleForTesting @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 set.stream() .collect( ImmutableSortedMap.toImmutableSortedMap( String::compareTo, keyMapper, ignored -> Collections.emptyMap())); } private final Image image; /** * Instantiate with an {@link Image}. * * @param image the image to translate */ public ImageToJsonTranslator(Image image) { this.image = image; } /** * Gets the container configuration. * * @return the container configuration */ public JsonTemplate getContainerConfiguration() { // Set up the JSON template. ContainerConfigurationTemplate template = new ContainerConfigurationTemplate(); // Adds the layer diff IDs. for (Layer layer : image.getLayers()) { template.addLayerDiffId(layer.getDiffId()); } // Adds the history. for (HistoryEntry historyObject : image.getHistory()) { template.addHistoryEntry(historyObject); } 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)); } return template; } /** * Gets the manifest as a JSON template. The {@code containerConfigurationBlobDescriptor} must be * 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. * @param containerConfigurationBlobDescriptor the container configuration descriptor. * @return the image contents serialized as JSON. */ public T getManifestTemplate( Class manifestTemplateClass, BlobDescriptor containerConfigurationBlobDescriptor) { try { // Set up the JSON template. T template = manifestTemplateClass.getDeclaredConstructor().newInstance(); // Adds the container configuration reference. DescriptorDigest containerConfigurationDigest = containerConfigurationBlobDescriptor.getDigest(); long containerConfigurationSize = containerConfigurationBlobDescriptor.getSize(); template.setContainerConfiguration(containerConfigurationSize, containerConfigurationDigest); // Adds the layers. for (Layer layer : image.getLayers()) { template.addLayer( layer.getBlobDescriptor().getSize(), layer.getBlobDescriptor().getDigest()); } return template; } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException ex) { throw new IllegalArgumentException(manifestTemplateClass + " cannot be instantiated", ex); } } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslator.java ================================================ /* * 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.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.DockerHealthCheck; import com.google.cloud.tools.jib.image.DigestOnlyLayer; 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.ReferenceLayer; import com.google.cloud.tools.jib.image.ReferenceNoDiffIdLayer; import com.google.common.annotations.VisibleForTesting; 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; /** Translates {@link V21ManifestTemplate} and {@link V22ManifestTemplate} into {@link Image}. */ public class JsonToImageTranslator { /** * Pattern used for parsing information out of exposed port configurations. Only accepts single * ports with protocol. * *

Example matches: 100, 1000/tcp, 2000/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}. * * @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, BadContainerConfigurationFormatException { Image.Builder imageBuilder = Image.builder(V21ManifestTemplate.class); // 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(); } /** * Translates {@link BuildableManifestTemplate} to {@link Image}. Uses the corresponding {@link * ContainerConfigurationTemplate} to get the layer diff IDs. * * @param manifestTemplate the template containing the image layers. * @param containerConfigurationTemplate the template containing the diff IDs and container * configuration properties. * @return the translated {@link Image}. * @throws LayerCountMismatchException if the manifest and configuration contain conflicting layer * information. * @throws LayerPropertyNotFoundException if adding image layers fails. * @throws BadContainerConfigurationFormatException if the container configuration is in a bad * format */ public static Image toImage( BuildableManifestTemplate manifestTemplate, ContainerConfigurationTemplate containerConfigurationTemplate) throws LayerCountMismatchException, LayerPropertyNotFoundException, BadContainerConfigurationFormatException { List layers = new ArrayList<>(); for (BuildableManifestTemplate.ContentDescriptorTemplate layerObjectTemplate : manifestTemplate.getLayers()) { if (layerObjectTemplate.getDigest() == null) { throw new IllegalArgumentException( "All layers in the manifest template must have digest set"); } layers.add( new ReferenceNoDiffIdLayer( new BlobDescriptor(layerObjectTemplate.getSize(), layerObjectTemplate.getDigest()))); } List diffIds = containerConfigurationTemplate.getDiffIds(); if (layers.size() != diffIds.size()) { throw new LayerCountMismatchException( "Mismatch between image manifest and container configuration"); } Image.Builder imageBuilder = Image.builder(manifestTemplate.getClass()); for (int layerIndex = 0; layerIndex < layers.size(); layerIndex++) { ReferenceNoDiffIdLayer noDiffIdLayer = layers.get(layerIndex); DescriptorDigest diffId = diffIds.get(layerIndex); imageBuilder.addLayer(new ReferenceLayer(noDiffIdLayer.getBlobDescriptor(), diffId)); } 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.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.addExposedPorts( portMapToSet(containerConfigurationTemplate.getContainerExposedPorts())); } if (containerConfigurationTemplate.getContainerVolumes() != null) { imageBuilder.addVolumes(volumeMapToSet(containerConfigurationTemplate.getContainerVolumes())); } if (containerConfigurationTemplate.getContainerEnvironment() != null) { for (String environmentVariable : containerConfigurationTemplate.getContainerEnvironment()) { 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")); } } imageBuilder.addLabels(containerConfigurationTemplate.getContainerLabels()); imageBuilder.setWorkingDirectory(containerConfigurationTemplate.getContainerWorkingDir()); imageBuilder.setUser(containerConfigurationTemplate.getContainerUser()); } /** * 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 set of {@link Port}s */ @VisibleForTesting static ImmutableSet portMapToSet(@Nullable Map> portMap) throws BadContainerConfigurationFormatException { if (portMap == null) { return ImmutableSet.of(); } ImmutableSet.Builder ports = new ImmutableSet.Builder<>(); for (Map.Entry> entry : portMap.entrySet()) { String port = entry.getKey(); Matcher matcher = PORT_PATTERN.matcher(port); if (!matcher.matches()) { throw new BadContainerConfigurationFormatException( "Invalid port configuration: '" + port + "'."); } 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() {} } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ManifestAndConfigTemplate.java ================================================ /* * 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; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ManifestListGenerator.java ================================================ /* * 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; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ManifestListTemplate.java ================================================ /* * 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); } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/image/json/ManifestTemplate.java ================================================ /* * 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.image.json; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.google.cloud.tools.jib.json.JsonTemplate; /** Parent class for image manifest and manifest list JSON templates. */ @JsonIgnoreProperties(ignoreUnknown = true) public interface ManifestTemplate extends JsonTemplate { int getSchemaVersion(); String getManifestMediaType(); } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/image/json/OciIndexTemplate.java ================================================ /* * 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 */ 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; } } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/image/json/OciManifestTemplate.java ================================================ /* * 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.image.json; 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. * *

Example manifest JSON: * *

{@code
 * {
 *   "schemaVersion": 2,
 *   "mediaType": "application/vnd.oci.image.manifest.v1+json",
 *   "config": {
 *     "mediaType": "application/vnd.oci.image.config.v1+json",
 *     "size": 631,
 *     "digest": "sha256:26b84ca5b9050d32e68f66ad0f3e2bbcd247198a6e6e09a7effddf126eb8d873"
 *   },
 *   "layers": [
 *     {
 *       "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
 *       "size": 1991435,
 *       "digest": "sha256:b56ae66c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a4647"
 *     },
 *     {
 *       "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
 *       "size": 32,
 *       "digest": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
 *     }
 *   ]
 * }
 * }
* * @see OCI Image * Manifest Specification */ public class OciManifestTemplate implements BuildableManifestTemplate { /** The OCI manifest media type. */ public static final String MANIFEST_MEDIA_TYPE = "application/vnd.oci.image.manifest.v1+json"; /** The OCI container configuration media type. */ private static final String CONTAINER_CONFIGURATION_MEDIA_TYPE = "application/vnd.oci.image.config.v1+json"; /** The OCI layer media type. */ 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. */ @Nullable private ContentDescriptorTemplate config; /** The list of layer references. */ private final List layers = new ArrayList<>(); @Override public int getSchemaVersion() { return schemaVersion; } @Override public String getManifestMediaType() { return MANIFEST_MEDIA_TYPE; } @Override @Nullable public ContentDescriptorTemplate getContainerConfiguration() { return config; } @Override public List getLayers() { return Collections.unmodifiableList(layers); } @Override public void setContainerConfiguration(long size, DescriptorDigest digest) { config = new ContentDescriptorTemplate(CONTAINER_CONFIGURATION_MEDIA_TYPE, size, digest); } @Override public void addLayer(long size, DescriptorDigest digest) { layers.add(new ContentDescriptorTemplate(LAYER_MEDIA_TYPE, size, digest)); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/image/json/PlatformNotFoundInBaseImageException.java ================================================ /* * 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); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/image/json/UnknownManifestFormatException.java ================================================ /* * 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.image.json; import com.google.cloud.tools.jib.api.RegistryException; /** Exception thrown when trying to parse an unknown image manifest format. */ public class UnknownManifestFormatException extends RegistryException { public UnknownManifestFormatException(String message) { super(message); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/image/json/UnlistedPlatformInManifestListException.java ================================================ /* * 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); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplate.java ================================================ /* * 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.image.json; 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; /** * JSON template for Docker Manifest Schema V2.1 * *

This is only for parsing manifests in the older V2.1 schema. Generated manifests should be in * the V2.2 schema using the {@link V22ManifestTemplate}. * *

Example manifest JSON (only the {@code fsLayers} and {@code history} fields are relevant for * parsing): * *

{@code
 * {
 *   ...
 *   "fsLayers": {
 *     {
 *       "blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
 *     },
 *     {
 *       "blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
 *     }
 *   },
 *   "history": [
 *     {
 *       "v1Compatibility": ""
 *     }
 *   ]
 *   ...
 * }
 * }
* * @see Image Manifest Version 2, * Schema 1 */ 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<>(); /** * 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; @Nullable DescriptorDigest getDigest() { return blobSum; } } /** Template for inner JSON object representing history for a layer. */ private static class HistoryObjectTemplate implements JsonTemplate { // 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<>(); for (LayerObjectTemplate layerObjectTemplate : fsLayers) { layerDigests.add(layerObjectTemplate.blobSum); } return layerDigests; } @Override public int getSchemaVersion() { return schemaVersion; } @Override public String getManifestMediaType() { return mediaType; } public List getFsLayers() { return Collections.unmodifiableList(fsLayers); } /** * 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(); } } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V22ManifestListTemplate.java ================================================ /* * 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. */ 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; } } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplate.java ================================================ /* * 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.image.json; 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 Docker Manifest Schema V2.2 * *

Example manifest JSON: * *

{@code
 * {
 *   "schemaVersion": 2,
 *   "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
 *   "config": {
 *     "mediaType": "application/vnd.docker.container.image.v1+json",
 *     "size": 631,
 *     "digest": "sha256:26b84ca5b9050d32e68f66ad0f3e2bbcd247198a6e6e09a7effddf126eb8d873"
 *   },
 *   "layers": [
 *     {
 *       "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
 *       "size": 1991435,
 *       "digest": "sha256:b56ae66c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a4647"
 *     },
 *     {
 *       "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
 *       "size": 32,
 *       "digest": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
 *     }
 *   ]
 * }
 * }
* * @see Image Manifest Version 2, * Schema 2 */ public class V22ManifestTemplate implements BuildableManifestTemplate { /** The Docker V2.2 manifest media type. */ public static final String MANIFEST_MEDIA_TYPE = "application/vnd.docker.distribution.manifest.v2+json"; /** The Docker V2.2 container configuration media type. */ private static final String CONTAINER_CONFIGURATION_MEDIA_TYPE = "application/vnd.docker.container.image.v1+json"; /** The Docker V2.2 layer media type. */ private static final String LAYER_MEDIA_TYPE = "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. */ @Nullable private ContentDescriptorTemplate config; /** The list of layer references. */ private final List layers = new ArrayList<>(); @Override public int getSchemaVersion() { return schemaVersion; } @Override public String getManifestMediaType() { return MANIFEST_MEDIA_TYPE; } @Override @Nullable public ContentDescriptorTemplate getContainerConfiguration() { return config; } @Override public List getLayers() { return Collections.unmodifiableList(layers); } @Override public void setContainerConfiguration(long size, DescriptorDigest digest) { config = new ContentDescriptorTemplate(CONTAINER_CONFIGURATION_MEDIA_TYPE, size, digest); } @Override public void addLayer(long size, DescriptorDigest digest) { layers.add(new ContentDescriptorTemplate(LAYER_MEDIA_TYPE, size, digest)); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplate.java ================================================ /* * 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.json; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonInclude; /** * All JSON templates to be used with {@link JsonTemplateMapper} must extend this class. * *

Json fields should be private fields and fields that are {@code null} will not be serialized. */ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonAutoDetect( fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE, creatorVisibility = JsonAutoDetect.Visibility.NONE) public interface JsonTemplate {} ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java ================================================ /* * 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.json; import com.fasterxml.jackson.databind.ObjectMapper; 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. /** * Helper class for serializing and deserializing JSON. * *

The interface uses Jackson as the JSON parser. Some useful annotations to include on classes * used as templates for JSON are: * *

{@code @JsonInclude(JsonInclude.Include.NON_NULL)} * *

    *
  • Does not serialize fields that are {@code null}. *
* * {@code @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)} * *
    *
  • Fields that are private are also accessible for serialization/deserialization. *
* * @see https://github.com/FasterXML/jackson */ public class JsonTemplateMapper { private static final ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule()); /** * Deserializes a JSON file via a JSON object template. * * @param child type of {@link JsonTemplate} * @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 readJsonFromFile(Path jsonFile, Class templateClass) throws IOException { try (InputStream fileIn = Files.newInputStream(jsonFile)) { return objectMapper.readValue(fileIn, templateClass); } } /** * Deserializes a JSON file via a JSON object template with a shared lock on the file. * * @param child type of {@link JsonTemplate} * @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(InputStream jsonStream, Class templateClass) throws IOException { return objectMapper.readValue(jsonStream, templateClass); } /** * Deserializes a JSON object from a 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 T readJson(String jsonString, Class templateClass) throws IOException { return objectMapper.readValue(jsonString, templateClass); } /** * Deserializes a JSON object from a JSON byte array. * * @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 T readJson(byte[] jsonBytes, Class templateClass) throws IOException { return objectMapper.readValue(jsonBytes, templateClass); } /** * Deserializes a JSON object list from a 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 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 void writeTo(Object template, OutputStream out) throws IOException { objectMapper.writeValue(out, template); } private JsonTemplateMapper() {} } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/registry/AbstractManifestPuller.java ================================================ /* * 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; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetriever.java ================================================ /* * 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.HttpMethods; 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> { private final RegistryEndpointRequestProperties registryEndpointRequestProperties; @Nullable private final String userAgent; private final FailoverHttpClient httpClient; AuthenticationMethodRetriever( RegistryEndpointRequestProperties registryEndpointRequestProperties, @Nullable String userAgent, FailoverHttpClient httpClient) { this.registryEndpointRequestProperties = registryEndpointRequestProperties; this.userAgent = userAgent; this.httpClient = httpClient; } @Nullable @Override public BlobHttpContent getContent() { return null; } @Override public List getAccept() { return Collections.emptyList(); } /** * The request did not error, meaning that the registry does not require authentication. * * @param response ignored * @return {@link Optional#empty()} */ @Override public Optional handleResponse(Response response) { return Optional.empty(); } @Override public URL getApiRoute(String apiRouteBase) throws MalformedURLException { return new URL(apiRouteBase); } @Override public String getHttpMethod() { return HttpMethods.GET; } @Override public String getActionDescription() { return "retrieve authentication method for " + registryEndpointRequestProperties.getServerUrl(); } @Override public Optional handleHttpResponseException( ResponseException responseException) throws ResponseException, RegistryErrorException { // Only valid for status code of '401 Unauthorized'. if (responseException.getStatusCode() != HttpStatusCodes.STATUS_CODE_UNAUTHORIZED) { throw responseException; } // Checks if the 'WWW-Authenticate' header is present. String authenticationMethod = responseException.getHeaders().getAuthenticate(); if (authenticationMethod == null) { throw new RegistryErrorExceptionBuilder(getActionDescription(), responseException) .addReason("'WWW-Authenticate' header not found") .build(); } // Parses the header to retrieve the components. try { return RegistryAuthenticator.fromAuthenticationMethod( authenticationMethod, registryEndpointRequestProperties, userAgent, httpClient); } catch (RegistryAuthenticationFailedException ex) { throw new RegistryErrorExceptionBuilder(getActionDescription(), ex) .addReason("Failed get authentication method from 'WWW-Authenticate' header") .build(); } } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobChecker.java ================================================ /* * 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.HttpMethods; 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.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> { private final RegistryEndpointRequestProperties registryEndpointRequestProperties; private final DescriptorDigest blobDigest; BlobChecker( RegistryEndpointRequestProperties registryEndpointRequestProperties, DescriptorDigest blobDigest) { this.registryEndpointRequestProperties = registryEndpointRequestProperties; this.blobDigest = blobDigest; } /** Returns the BLOB's content descriptor. */ @Override public Optional handleResponse(Response response) throws RegistryErrorException { long contentLength = response.getContentLength(); if (contentLength < 0) { throw new RegistryErrorExceptionBuilder(getActionDescription()) .addReason("Did not receive Content-Length header") .build(); } return Optional.of(new BlobDescriptor(contentLength, blobDigest)); } @Override public Optional handleHttpResponseException(ResponseException responseException) throws ResponseException { if (responseException.getStatusCode() != HttpStatusCodes.STATUS_CODE_NOT_FOUND) { throw responseException; } 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 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 responseException; } @Override public URL getApiRoute(String apiRouteBase) throws MalformedURLException { return new URL( apiRouteBase + registryEndpointRequestProperties.getImageName() + "/blobs/" + blobDigest); } @Nullable @Override public BlobHttpContent getContent() { return null; } @Override public List getAccept() { return Collections.emptyList(); } @Override public String getHttpMethod() { return HttpMethods.HEAD; } @Override public String getActionDescription() { return "check BLOB exists for " + registryEndpointRequestProperties.getServerUrl() + "/" + registryEndpointRequestProperties.getImageName() + " with digest " + blobDigest; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPuller.java ================================================ /* * 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.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.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). */ class BlobPuller implements RegistryEndpointProvider { private final RegistryEndpointRequestProperties registryEndpointRequestProperties; /** The digest of the BLOB to pull. */ private final DescriptorDigest blobDigest; /** * The {@link OutputStream} to write the BLOB to. Closes the {@link OutputStream} after writing. */ private final OutputStream destinationOutputStream; private final Consumer blobSizeListener; private final Consumer writtenByteCountListener; BlobPuller( RegistryEndpointRequestProperties registryEndpointRequestProperties, DescriptorDigest blobDigest, 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 { 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( "The pulled BLOB has digest '" + receivedBlobDescriptor.getDigest() + "', but the request digest was '" + blobDigest + "'"); } } return null; } @Override @Nullable public BlobHttpContent getContent() { return null; } @Override public List getAccept() { return Collections.emptyList(); } @Override public URL getApiRoute(String apiRouteBase) throws MalformedURLException { return new URL( apiRouteBase + registryEndpointRequestProperties.getImageName() + "/blobs/" + blobDigest); } @Override public String getHttpMethod() { return HttpMethods.GET; } @Override public String getActionDescription() { return "pull BLOB for " + registryEndpointRequestProperties.getServerUrl() + "/" + registryEndpointRequestProperties.getImageName() + " with digest " + blobDigest; } @Override public Void handleHttpResponseException(ResponseException responseException) throws ResponseException, RegistryErrorException { throw responseException; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/registry/BlobPusher.java ================================================ /* * 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.GenericUrl; import com.google.api.client.http.HttpMethods; 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.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; /** * Pushes an image's BLOB (layer or container configuration). * *

The BLOB is pushed in three stages: * *

    *
  1. Initialize - Gets a location back to write the BLOB content to *
  2. Write BLOB - Write the BLOB content to the received location *
  3. Commit BLOB - Commits the BLOB with its digest *
*/ 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> { @Nullable @Override public BlobHttpContent getContent() { return null; } @Override public List getAccept() { return Collections.emptyList(); } /** * Returns a URL to continue pushing the BLOB to, or {@link Optional#empty()} if the BLOB * already exists on the registry. */ @Override public Optional handleResponse(Response response) throws RegistryErrorException { switch (response.getStatusCode()) { case HttpURLConnection.HTTP_CREATED: // The BLOB exists in the registry. return Optional.empty(); case HttpURLConnection.HTTP_ACCEPTED: return Optional.of(getRedirectLocation(response)); default: throw buildRegistryErrorException( "Received unrecognized status code " + response.getStatusCode()); } } @Override public URL getApiRoute(String apiRouteBase) throws MalformedURLException { 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 public String getHttpMethod() { return HttpMethods.POST; } @Override 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(), writtenByteCountListener); } @Override public List getAccept() { return Collections.emptyList(); } /** Returns a URL to continue pushing the BLOB to. */ @Override public URL handleResponse(Response response) throws RegistryException { // TODO: Handle 204 No Content return getRedirectLocation(response); } @Override public URL getApiRoute(String apiRouteBase) { return location; } @Override public String getHttpMethod() { return HttpMethods.PATCH; } @Override public String getActionDescription() { return BlobPusher.this.getActionDescription(); } private Writer(URL location, Consumer writtenByteCountListener) { this.location = location; this.writtenByteCountListener = writtenByteCountListener; } @Override public URL handleHttpResponseException(ResponseException responseException) throws ResponseException, RegistryErrorException { throw responseException; } } /** Commits the written BLOB. */ private class Committer implements RegistryEndpointProvider { private final URL location; @Nullable @Override public BlobHttpContent getContent() { return null; } @Override public List getAccept() { return Collections.emptyList(); } @Override public Void handleResponse(Response response) { return null; } /** 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(); } @Override public String getHttpMethod() { return HttpMethods.PUT; } @Override public String getActionDescription() { return BlobPusher.this.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, @Nullable String sourceRepository) { this.registryEndpointRequestProperties = registryEndpointRequestProperties; this.blobDigest = blobDigest; this.blob = blob; this.sourceRepository = sourceRepository; } /** * Returns a {@link RegistryEndpointProvider} for initializing the BLOB upload with an existence * check. */ 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, 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 */ RegistryEndpointProvider committer(URL location) { return new Committer(location); } private RegistryErrorException buildRegistryErrorException(String reason) { RegistryErrorExceptionBuilder registryErrorExceptionBuilder = new RegistryErrorExceptionBuilder(getActionDescription()); registryErrorExceptionBuilder.addReason(reason); return registryErrorExceptionBuilder.build(); } /** * Returns the common action description for {@link Initializer}, {@link Writer}, and {@link * Committer}. */ private String getActionDescription() { return "push BLOB for " + registryEndpointRequestProperties.getServerUrl() + "/" + registryEndpointRequestProperties.getImageName() + " with digest " + blobDigest; } /** * Extract the {@code Location} header from the response to get the new location for the next * request. * *

The {@code Location} header can be relative or absolute. * * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location#Directives * @param response the response to extract the 'Location' header from * @return the new location for the next request * @throws RegistryErrorException if there was not a single 'Location' header */ private URL getRedirectLocation(Response response) throws RegistryErrorException { // Extracts and returns the 'Location' header. List locationHeaders = response.getHeader("Location"); if (locationHeaders.size() != 1) { throw buildRegistryErrorException( "Expected 1 'Location' header, but found " + locationHeaders.size()); } String locationHeader = locationHeaders.get(0); return response.getRequestUrl().toURL(locationHeader); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/registry/ErrorCodes.java ================================================ /* * 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; /** * Enumerated errors that can be received from the Registry API. * *

Descriptions are from: * * @see https://docs.docker.com/registry/spec/api/#errors-2 */ enum ErrorCodes { /** * This error may be returned when a blob is unknown to the registry in a specified repository. * This can be returned with a standard get or if a manifest references an unknown layer during * upload. */ BLOB_UNKNOWN, /** The blob upload encountered an error and can no longer proceed. */ BLOB_UPLOAD_INVALID, /** If a blob upload has been cancelled or was never started, this error code may be returned. */ BLOB_UPLOAD_UNKNOWN, /** * When a blob is uploaded, the registry will check that the content matches the digest provided * by the client. The error may include a detail structure with the key "digest", including the * invalid digest string. This error may also be returned when a manifest includes an invalid * layer digest. */ DIGEST_INVALID, /** This error may be returned when a manifest blob is unknown to the registry. */ MANIFEST_BLOB_UNKNOWN, /** * During upload, manifests undergo several checks ensuring validity. If those checks fail, this * error may be returned, unless a more specific error is included. The detail will contain * information the failed validation. */ MANIFEST_INVALID, /** * This error is returned when the manifest, identified by name and tag is unknown to the * repository. */ MANIFEST_UNKNOWN, /** * During manifest upload, if the manifest fails signature verification, this error will be * returned. */ MANIFEST_UNVERIFIED, /** Invalid repository name encountered either during manifest validation or any API operation. */ NAME_INVALID, /** This is returned if the name used during an operation is unknown to the registry. */ NAME_UNKNOWN, /** * When a layer is uploaded, the provided size will be checked against the uploaded content. If * they do not match, this error will be returned. */ SIZE_INVALID, /** * During a manifest upload, if the tag in the manifest does not match the uri tag, this error * will be returned. */ TAG_INVALID, /** * The access controller was unable to authenticate the client. Often this will be accompanied by * a Www-Authenticate HTTP response header indicating how to authenticate. */ UNAUTHORIZED, /** The access controller denied access for the operation on a resource. */ DENIED, /** The operation was unsupported due to a missing implementation or invalid set of parameters. */ UNSUPPORTED } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/registry/ErrorResponseUtil.java ================================================ /* * 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() {} } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestAndDigest.java ================================================ /* * 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; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestChecker.java ================================================ /* * 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); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPuller.java ================================================ /* * 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.google.cloud.tools.jib.image.json.ManifestTemplate; /** Pulls an image's manifest. */ class ManifestPuller extends AbstractManifestPuller> { ManifestPuller( RegistryEndpointRequestProperties registryEndpointRequestProperties, String imageTag, Class manifestTemplateClass) { super(registryEndpointRequestProperties, imageTag, manifestTemplateClass); } @Override ManifestAndDigest computeReturn(ManifestAndDigest manifestAndDigest) { return manifestAndDigest; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/registry/ManifestPusher.java ================================================ /* * 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.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.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 { /** 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 ManifestTemplate manifestTemplate; private final String imageTag; private final EventHandlers eventHandlers; ManifestPusher( RegistryEndpointRequestProperties registryEndpointRequestProperties, 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( Blobs.from(manifestTemplate), manifestTemplate.getManifestMediaType()); } @Override public List getAccept() { return Collections.emptyList(); } @Override 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 public URL getApiRoute(String apiRouteBase) throws MalformedURLException { return new URL( apiRouteBase + registryEndpointRequestProperties.getImageName() + "/manifests/" + imageTag); } @Override public String getHttpMethod() { return HttpMethods.PUT; } @Override public String getActionDescription() { return "push image manifest for " + registryEndpointRequestProperties.getServerUrl() + "/" + registryEndpointRequestProperties.getImageName() + ":" + imageTag; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAliasGroup.java ================================================ /* * 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); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java ================================================ /* * 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.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.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; /** * Authenticates push/pull access with a registry service. * * @see https://docs.docker.com/registry/spec/auth/token/ */ public class RegistryAuthenticator { // 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 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 */ 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 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( registryUrl, imageName, authenticationMethod, "Bearer"); } Pattern realmPattern = Pattern.compile("realm=\"(.*?)\""); Matcher realmMatcher = realmPattern.matcher(authenticationMethod); if (!realmMatcher.find()) { throw newRegistryAuthenticationFailedException( registryUrl, imageName, authenticationMethod, "realm"); } String realm = realmMatcher.group(1); Pattern servicePattern = Pattern.compile("service=\"(.*?)\""); Matcher serviceMatcher = servicePattern.matcher(authenticationMethod); // use the provided registry location when missing service (e.g., for OpenShift) String service = serviceMatcher.find() ? serviceMatcher.group(1) : registryUrl; return Optional.of( new RegistryAuthenticator( realm, service, registryEndpointRequestProperties, userAgent, httpClient)); } private static RegistryAuthenticationFailedException newRegistryAuthenticationFailedException( 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: " + authenticationMethod); } /** Template for the authentication response JSON. */ @VisibleForTesting @JsonIgnoreProperties(ignoreUnknown = true) static class AuthenticationResponseTemplate implements JsonTemplate { @Nullable private String token; /** * {@code access_token} is accepted as an alias for {@code token}. * * @see https://docs.docker.com/registry/spec/auth/token/#token-response-fields */ @Nullable @JsonProperty("access_token") private String accessToken; /** Returns {@link #token} if not null, or {@link #accessToken}. */ @Nullable @VisibleForTesting String getToken() { if (token != null) { return token; } return accessToken; } } private final RegistryEndpointRequestProperties registryEndpointRequestProperties; private final String realm; private final String service; @Nullable private final String userAgent; private final FailoverHttpClient httpClient; 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(@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(@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(@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(@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 url = getAuthenticationUrl(credential, repositoryScopes); 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(response.getBody(), AuthenticationResponseTemplate.class); if (responseJson.getToken() == null) { throw new RegistryAuthenticationFailedException( registryUrl, imageName, "Did not get token in authentication response from " + getAuthenticationUrl(credential, repositoryScopes) + "; parameters: " + getAuthRequestParameters(credential, repositoryScopes)); } 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(registryUrl, imageName, ex); } } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java ================================================ /* * 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.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.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.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.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.Verify; import com.google.common.collect.ImmutableSetMultimap; import com.google.common.collect.Multimap; import java.io.IOException; 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. Thread-safe. */ @ThreadSafe public class RegistryClient { /** Factory for creating {@link RegistryClient}s. */ public static class Factory { private final EventHandlers eventHandlers; private final RegistryEndpointRequestProperties registryEndpointRequestProperties; private final FailoverHttpClient httpClient; @Nullable private String userAgent; @Nullable private Credential credential; private Factory( EventHandlers eventHandlers, RegistryEndpointRequestProperties registryEndpointRequestProperties, FailoverHttpClient httpClient) { this.eventHandlers = eventHandlers; this.registryEndpointRequestProperties = registryEndpointRequestProperties; this.httpClient = httpClient; } /** * Sets the authentication credentials to use to authenticate with the registry. * * @param credential the {@link Credential} to access the registry/repository * @return this */ public Factory setCredential(@Nullable Credential credential) { this.credential = credential; return this; } /** * Sets the value of {@code User-Agent} in headers for registry requests. * * @param userAgent user agent string * @return this */ public Factory setUserAgent(@Nullable String userAgent) { this.userAgent = userAgent; return this; } /** * Creates a new {@link RegistryClient}. * * @return the new {@link RegistryClient} */ public RegistryClient newRegistryClient() { return new RegistryClient( eventHandlers, credential, registryEndpointRequestProperties, userAgent, httpClient); } } 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( EventHandlers eventHandlers, String serverUrl, String imageName, FailoverHttpClient httpClient) { return new Factory( eventHandlers, new RegistryEndpointRequestProperties(serverUrl, imageName), httpClient); } /** * 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 sourceImageName additional source image to request pull permission from the registry * @param httpClient HTTP client * @return the new {@link Factory} */ public static Factory factory( EventHandlers eventHandlers, String serverUrl, String imageName, String sourceImageName, FailoverHttpClient httpClient) { return new Factory( eventHandlers, new RegistryEndpointRequestProperties(serverUrl, imageName, sourceImageName), httpClient); } /** * A simple class representing the payload of a Docker Registry v2 Bearer Token * which lists the set of access claims granted. * *

   * {"access":[{"type": "repository","name": "library/openjdk","actions":["push","pull"]}]}
   * 
* * @see AccessClaim */ @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 { @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; } 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; } } private final EventHandlers eventHandlers; @Nullable private final Credential credential; private final RegistryEndpointRequestProperties registryEndpointRequestProperties; @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 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 userAgent {@code User-Agent} header to send with the request, can be {@code null} * @param httpClient HTTP client */ private RegistryClient( EventHandlers eventHandlers, @Nullable Credential credential, RegistryEndpointRequestProperties registryEndpointRequestProperties, @Nullable String userAgent, FailoverHttpClient httpClient) { this.eventHandlers = eventHandlers; this.credential = credential; this.registryEndpointRequestProperties = registryEndpointRequestProperties; 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)); } /** * 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 */ 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 and digest for a specific tag. * * @param child type of ManifestTemplate * @param imageQualifier the tag or digest to pull on * @param manifestTemplateClass the specific version of manifest template to pull, or {@link * 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 ManifestAndDigest pullManifest( String imageQualifier, Class manifestTemplateClass) throws IOException, RegistryException { ManifestPuller manifestPuller = new ManifestPuller<>( registryEndpointRequestProperties, imageQualifier, manifestTemplateClass); return callRegistryEndpoint(manifestPuller); } public ManifestAndDigest pullManifest(String imageQualifier) throws IOException, RegistryException { return pullManifest(imageQualifier, ManifestTemplate.class); } /** * Pushes the image manifest for a specific tag. * * @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 DescriptorDigest pushManifest(ManifestTemplate manifestTemplate, String imageTag) throws IOException, RegistryException { 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 {@link * Optional#empty()} if it doesn't * @throws IOException if communicating with the endpoint fails * @throws RegistryException if communicating with the endpoint fails */ public Optional checkBlob(DescriptorDigest blobDigest) throws IOException, RegistryException { BlobChecker blobChecker = new BlobChecker(registryEndpointRequestProperties, blobDigest); return callRegistryEndpoint(blobChecker); } /** * 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 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 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); } /** * 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, @Nullable String sourceRepository, Consumer writtenByteCountListener) throws IOException, RegistryException { if (isBearerAuth(authorization.get()) && readOnlyBearerAuth) { throw new IllegalStateException("push may fail with pull-only bearer auth token"); } 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; } timerEventDispatcher2.lap("pushBlob PATCH " + blobDigest); // PATCH with BLOB URL putLocation = callRegistryEndpoint(blobPusher.writer(patchLocation.get(), writtenByteCountListener)); timerEventDispatcher2.lap("pushBlob PUT " + blobDigest); // PUT ?digest={blob.digest} callRegistryEndpoint(blobPusher.committer(putLocation)); return false; } } } /** * 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 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; } /** * Calls the registry endpoint. * * @param registryEndpointProvider the {@link RegistryEndpointProvider} to the endpoint * @throws IOException if communicating with the endpoint fails * @throws RegistryException if communicating with the endpoint fails */ private T callRegistryEndpoint(RegistryEndpointProvider registryEndpointProvider) throws IOException, RegistryException { 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)); } } } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryCredentialsNotSentException.java ================================================ /* * 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"); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointCaller.java ================================================ /* * 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.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.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.URL; import java.util.Locale; import javax.annotation.Nullable; import javax.net.ssl.SSLException; /** * Makes requests to a registry endpoint. * * @param the type returned by calling the endpoint */ class RegistryEndpointCaller { /** * https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/308. */ @VisibleForTesting static final int STATUS_CODE_PERMANENT_REDIRECT = 308; // 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; } exception = exception.getCause(); if (exception == original) { // just in case if there's a circular chain return false; } } return false; } private final EventHandlers eventHandlers; @Nullable private final String userAgent; private final RegistryEndpointProvider registryEndpointProvider; @Nullable private final Authorization authorization; private final RegistryEndpointRequestProperties registryEndpointRequestProperties; 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 registryEndpointProvider the {@link RegistryEndpointProvider} to the endpoint * @param authorization optional authentication credentials to use * @param registryEndpointRequestProperties properties of the registry endpoint request * @param httpClient HTTP client */ RegistryEndpointCaller( EventHandlers eventHandlers, @Nullable String userAgent, RegistryEndpointProvider registryEndpointProvider, @Nullable Authorization authorization, RegistryEndpointRequestProperties registryEndpointRequestProperties, FailoverHttpClient httpClient) { this.eventHandlers = eventHandlers; this.userAgent = userAgent; this.registryEndpointProvider = registryEndpointProvider; this.authorization = authorization; this.registryEndpointRequestProperties = registryEndpointRequestProperties; this.httpClient = httpClient; } /** * Makes the request to the endpoint. * * @return an object representing the response, or {@code null} * @throws IOException for most I/O exceptions when making the request * @throws RegistryException for known exceptions when interacting with the registry */ T call() throws IOException, RegistryException { String apiRouteBase = "https://" + registryEndpointRequestProperties.getServerUrl() + "/v2/"; URL initialRequestUrl = registryEndpointProvider.getApiRoute(apiRouteBase); return call(initialRequestUrl); } /** * Calls the registry endpoint with a certain {@link URL}. * * @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 */ private T call(URL url) throws IOException, RegistryException { String serverUrl = registryEndpointRequestProperties.getServerUrl(); String imageName = registryEndpointRequestProperties.getImageName(); 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 (ResponseException ex) { // First, see if the endpoint provider handles an exception as an expected response. try { return registryEndpointProvider.handleHttpResponseException(ex); } 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. throw newRegistryErrorException(responseException); } else if (responseException.getStatusCode() == HttpStatusCodes.STATUS_CODE_FORBIDDEN) { throw new RegistryUnauthorizedException(serverUrl, imageName, responseException); } 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 responseException; } } } 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")); } 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."); } } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointProvider.java ================================================ /* * 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.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; import java.util.List; import javax.annotation.Nullable; /** * Provides implementations for a registry endpoint. Implementations should be immutable. * * @param the type returned from handling the endpoint response */ interface RegistryEndpointProvider { /** 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; /** Returns the {@link BlobHttpContent} to send as the request body. */ @Nullable BlobHttpContent getContent(); /** Returns a list of MIME types to pass as an HTTP {@code Accept} header. */ List getAccept(); /** Handles the response specific to the registry action. */ T handleResponse(Response response) throws IOException, RegistryException; /** * Handles an {@link ResponseException} that occurs. Implementation must re-throw the given * exception if it did not conclusively handled the response exception. * * @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 */ T handleHttpResponseException(ResponseException responseException) throws ResponseException, RegistryErrorException; /** * Returns description of the registry action performed, used in error messages to describe the * action that failed. */ String getActionDescription(); } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointRequestProperties.java ================================================ /* * 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 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() { return serverUrl; } String getImageName() { return imageName; } @Nullable String getSourceImageName() { return sourceImageName; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryErrorException.java ================================================ /* * 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.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}. */ class RegistryErrorException extends RegistryException { RegistryErrorException(String message, @Nullable Throwable cause) { super(message, cause); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryErrorExceptionBuilder.java ================================================ /* * 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.google.cloud.tools.jib.registry.json.ErrorEntryTemplate; import javax.annotation.Nullable; /** Builds a {@link RegistryErrorException} with multiple causes. */ class RegistryErrorExceptionBuilder { @Nullable private final Throwable cause; private final StringBuilder errorMessageBuilder = new StringBuilder(); private boolean firstErrorReason = true; /** * Gets the reason for certain errors. * * @param errorCodeString string form of {@link ErrorCodes} * @param message the original received error message, which may or may not be used depending on * the {@code errorCode} */ private static String getReason(@Nullable String errorCodeString, @Nullable String message) { if (message == null) { message = "no details"; } if (errorCodeString == null) { return "unknown: " + message; } try { 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; } } catch (IllegalArgumentException ex) { return "unknown error code: " + errorCodeString + " (" + message + ")"; } } /** Creates a new builder with information about the method that errored. */ RegistryErrorExceptionBuilder(String method, @Nullable Throwable cause) { this.cause = cause; errorMessageBuilder.append("Tried to "); errorMessageBuilder.append(method); errorMessageBuilder.append(" but failed because: "); } /** Creates a new builder with information about the method that errored. */ RegistryErrorExceptionBuilder(String method) { this(method, null); } // TODO: Don't use a JsonTemplate as a data object to pass around. /** * Builds an entry to the error reasons from an {@link ErrorEntryTemplate}. * * @param errorEntry the {@link ErrorEntryTemplate} to add */ RegistryErrorExceptionBuilder addReason(ErrorEntryTemplate errorEntry) { String reason = getReason(errorEntry.getCode(), errorEntry.getMessage()); addReason(reason); return this; } /** Adds an entry to the error reasons. */ RegistryErrorExceptionBuilder addReason(String reason) { if (!firstErrorReason) { errorMessageBuilder.append(", "); } errorMessageBuilder.append(reason); firstErrorReason = false; return this; } RegistryErrorException build() { return new RegistryErrorException(errorMessageBuilder.toString(), cause); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/registry/UnexpectedBlobDigestException.java ================================================ /* * 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 a pulled BLOB did not have the same digest as requested. */ class UnexpectedBlobDigestException extends RegistryException { UnexpectedBlobDigestException(String message) { super(message); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialHelperNotFoundException.java ================================================ /* * 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.credentials; import java.nio.file.Path; /** Thrown because the requested credential helper CLI does not exist. */ public class CredentialHelperNotFoundException extends CredentialRetrievalException { CredentialHelperNotFoundException(Path credentialHelper, Throwable cause) { super("The system does not have " + credentialHelper + " CLI", cause); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialHelperUnhandledServerUrlException.java ================================================ /* * 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.credentials; import java.nio.file.Path; /** Thrown because the credential helper does not have credentials for the specified server URL. */ public class CredentialHelperUnhandledServerUrlException extends CredentialRetrievalException { CredentialHelperUnhandledServerUrlException( Path credentialHelper, String serverUrl, String credentialHelperOutput) { super( "The credential helper (" + credentialHelper + ") has nothing for server URL: " + serverUrl + "\n\nGot output:\n\n" + credentialHelperOutput); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/CredentialRetrievalException.java ================================================ /* * 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); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfig.java ================================================ /* * 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); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetriever.java ================================================ /* * 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.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.util.Base64; import java.util.Map; import java.util.Optional; import java.util.function.Consumer; /** * Retrieves registry credentials from the Docker config. * *

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. *
* * @see https://docs.docker.com/engine/reference/commandline/login/ */ public class DockerConfigCredentialRetriever { private final String registry; private final Path dockerConfigFile; private final boolean legacyConfigFormat; public static DockerConfigCredentialRetriever create(String registry, Path dockerConfigFile) { return new DockerConfigCredentialRetriever(registry, dockerConfigFile, false); } public static DockerConfigCredentialRetriever createForLegacyFormat( String registry, Path dockerConfigFile) { return new DockerConfigCredentialRetriever(registry, dockerConfigFile, true); } private DockerConfigCredentialRetriever( String registry, Path dockerConfigFile, boolean legacyConfigFormat) { this.registry = registry; this.dockerConfigFile = dockerConfigFile; this.legacyConfigFormat = legacyConfigFormat; } public Path getDockerConfigFile() { return dockerConfigFile; } /** * 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 */ public Optional retrieve(Consumer logger) throws IOException { if (!Files.exists(dockerConfigFile)) { return Optional.empty(); } 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); } DockerConfig dockerConfig = new DockerConfig(objectMapper.readValue(fileIn, DockerConfigTemplate.class)); return retrieve(dockerConfig, logger); } } /** * Retrieves credentials for a registry alias from a {@link DockerConfig}. * * @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 */ @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())); } } } } // 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(); } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelper.java ================================================ /* * 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.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.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; /** * Retrieves Docker credentials with a Docker credential helper. * * @see https://github.com/docker/docker-credential-helpers */ public class DockerCredentialHelper { private final String serverUrl; 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) static class DockerCredentialsTemplate implements JsonTemplate { @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()); } /** * 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 * @param environment environment variables used in configuring the credential helper */ 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.credentialHelper = credentialHelper; this.systemProperties = systemProperties; this.processBuilderFactory = processBuilderFactory; this.environment = environment; } /** * Calls the credential helper CLI. * *

Calls occur in the form: * *

{@code
   * echo -n  | docker-credential- get
   * }
* * @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 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 { ProcessBuilder processBuilder = processBuilderFactory.apply(credentialHelperCommand); processBuilder.environment().putAll(environment); Process process = processBuilder.start(); try (OutputStream processStdin = process.getOutputStream()) { processStdin.write(serverUrl.getBytes(StandardCharsets.UTF_8)); } try (InputStreamReader processStdoutReader = new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)) { String output = CharStreams.toString(processStdoutReader); // Throws an exception if the credential store does not have credentials for serverUrl. if (output.contains("credentials not found in native keychain")) { 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 CredentialHelperUnhandledServerUrlException( credentialHelper, serverUrl, errorOutput); } } try { DockerCredentialsTemplate dockerCredentials = JsonTemplateMapper.readJson(output, DockerCredentialsTemplate.class); if (Strings.isNullOrEmpty(dockerCredentials.username) || Strings.isNullOrEmpty(dockerCredentials.secret)) { throw new CredentialHelperUnhandledServerUrlException( credentialHelper, serverUrl, output); } return Credential.from(dockerCredentials.username, dockerCredentials.secret); } catch (JsonProcessingException ex) { throw new CredentialHelperUnhandledServerUrlException( credentialHelper, serverUrl, output); } } } catch (IOException ex) { if (ex.getMessage() == null) { throw ex; } // 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") || ex.getMessage().contains("error=2")) /* errno=2 (ENOENT) */ { throw new CredentialHelperNotFoundException(credentialHelper, ex); } throw ex; } } Path getCredentialHelper() { return credentialHelper; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/json/DockerConfigTemplate.java ================================================ /* * 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.json; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.common.collect.ImmutableMap; import java.util.HashMap; import java.util.Map; import javax.annotation.Nullable; /** * Template for a Docker config file. * *

Example: * *

{@code
 * {
 *   "auths": {
 *     "registry": {
 *       "auth": "username:password string in base64",
 *       "username": "..."
 *       "password": "..."
 *       "identityToken": "..."
 *     },
 *     "anotherregistry": {},
 *     ...
 *   },
 *   "credsStore": "legacy credential helper config acting as a \"default\" helper",
 *   "credHelpers": {
 *     "registry": "credential helper name",
 *     "anotherregistry": "another credential helper name",
 *     ...
 *   }
 * }
 * }
* *

Each entry in {@code credHelpers} is a mapping from a registry to a credential helper that * 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}. * *

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) 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; @Nullable private String credsStore; /** Maps from registry to credential helper name. */ private final Map credHelpers = new HashMap<>(); public DockerConfigTemplate(Map auths) { this.auths = ImmutableMap.copyOf(auths); } @SuppressWarnings("unused") private DockerConfigTemplate() { auths = new HashMap<>(); } public Map getAuths() { return auths; } @Nullable public String getCredsStore() { return credsStore; } public Map getCredHelpers() { return credHelpers; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/registry/json/ErrorEntryTemplate.java ================================================ /* * 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.json; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.google.cloud.tools.jib.json.JsonTemplate; import javax.annotation.Nullable; // TODO: Should include detail field as well - need to have custom parser @JsonIgnoreProperties(ignoreUnknown = true) public class ErrorEntryTemplate implements JsonTemplate { @Nullable private String code; @Nullable private String message; public ErrorEntryTemplate(String code, String message) { this.code = code; this.message = message; } /** Necessary for Jackson to create from JSON. */ @SuppressWarnings("unused") private ErrorEntryTemplate() {} @Nullable public String getCode() { return code; } @Nullable public String getMessage() { return message; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/registry/json/ErrorResponseTemplate.java ================================================ /* * 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.json; import com.google.cloud.tools.jib.json.JsonTemplate; import com.google.common.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * Template for the registry response body JSON when a request errored. * *

Example: * *

{@code
 * {
 *   "errors": [
 *     {
 *       "code": "MANIFEST_UNKNOWN",
 *       "message": "manifest unknown",
 *       "detail": {"Tag": "latest"}
 *     }
 *   ]
 * }
 * }
*/ public class ErrorResponseTemplate implements JsonTemplate { private final List errors = new ArrayList<>(); public List getErrors() { return Collections.unmodifiableList(errors); } @VisibleForTesting public ErrorResponseTemplate addError(ErrorEntryTemplate errorEntryTemplate) { errors.add(errorEntryTemplate); return this; } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarExtractor.java ================================================ /* * 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())); } } } } ================================================ FILE: jib-core/src/main/java/com/google/cloud/tools/jib/tar/TarStreamBuilder.java ================================================ /* * 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.tar; import com.google.cloud.tools.jib.blob.Blob; import com.google.cloud.tools.jib.blob.Blobs; import java.io.IOException; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.LinkedHashMap; import java.util.Map; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; /** Builds a tarball archive. */ public class TarStreamBuilder { /** * 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<>(); /** * Writes each entry in the filesystem to the tarball archive stream. * * @param out the stream to write to. * @throws IOException if building the tarball fails. */ public void writeAsTarArchiveTo(OutputStream out) throws IOException { try (TarArchiveOutputStream tarArchiveOutputStream = new TarArchiveOutputStream(out, StandardCharsets.UTF_8.name())) { // Enables PAX extended headers to support long file names. tarArchiveOutputStream.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); tarArchiveOutputStream.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_POSIX); for (Map.Entry entry : archiveMap.entrySet()) { tarArchiveOutputStream.putArchiveEntry(entry.getKey()); entry.getValue().writeTo(tarArchiveOutputStream); tarArchiveOutputStream.closeArchiveEntry(); } } } /** * Adds a {@link TarArchiveEntry} to the archive. * * @param entry the {@link TarArchiveEntry} */ public void addTarArchiveEntry(TarArchiveEntry entry) { archiveMap.put( entry, entry.isFile() ? Blobs.from(entry.getPath()) : Blobs.from(ignored -> {}, true)); } /** * 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 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 addByteEntry(byte[] contents, String name, Instant modificationTime) { TarArchiveEntry entry = new TarArchiveEntry(name); entry.setSize(contents.length); entry.setModTime(modificationTime.toEpochMilli()); archiveMap.put(entry, Blobs.from(outputStream -> outputStream.write(contents), true)); } /** * 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); } } ================================================ FILE: jib-core/src/main/resources/commons-logging.properties ================================================ org.apache.commons.logging.Log=org.apache.commons.logging.impl.NoOpLog ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/MultithreadedExecutor.java ================================================ /* * 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); } } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/api/ContainerizerTest.java ================================================ /* * 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)); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/api/CredentialTest.java ================================================ /* * 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()); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/api/DescriptorDigestTest.java ================================================ /* * 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.security.DigestException; import java.util.HashMap; import java.util.Map; import org.junit.Assert; import org.junit.Test; /** Tests for {@link DescriptorDigest}. */ public class DescriptorDigestTest { @Test public void testCreateFromHash_pass() throws DigestException { String goodHash = createGoodHash('a'); DescriptorDigest descriptorDigest = DescriptorDigest.fromHash(goodHash); Assert.assertEquals(goodHash, descriptorDigest.getHash()); Assert.assertEquals("sha256:" + goodHash, descriptorDigest.toString()); } @Test public void testCreateFromHash_fail() { String badHash = "not a valid hash"; try { DescriptorDigest.fromHash(badHash); Assert.fail("Invalid hash should have caused digest creation failure."); } catch (DigestException ex) { Assert.assertEquals("Invalid hash: " + badHash, ex.getMessage()); } } @Test public void testCreateFromHash_failIncorrectLength() { String badHash = createGoodHash('a') + 'a'; try { DescriptorDigest.fromHash(badHash); Assert.fail("Invalid hash should have caused digest creation failure."); } catch (DigestException ex) { Assert.assertEquals("Invalid hash: " + badHash, ex.getMessage()); } } @Test public void testCreateFromDigest_pass() throws DigestException { String goodHash = createGoodHash('a'); String goodDigest = "sha256:" + createGoodHash('a'); DescriptorDigest descriptorDigest = DescriptorDigest.fromDigest(goodDigest); Assert.assertEquals(goodHash, descriptorDigest.getHash()); Assert.assertEquals(goodDigest, descriptorDigest.toString()); } @Test public void testCreateFromDigest_fail() { String badDigest = "sha256:not a valid digest"; try { DescriptorDigest.fromDigest(badDigest); Assert.fail("Invalid digest should have caused digest creation failure."); } catch (DigestException ex) { Assert.assertEquals("Invalid digest: " + badDigest, ex.getMessage()); } } @Test public void testUseAsMapKey() throws DigestException { DescriptorDigest descriptorDigestA1 = DescriptorDigest.fromHash(createGoodHash('a')); DescriptorDigest descriptorDigestA2 = DescriptorDigest.fromHash(createGoodHash('a')); DescriptorDigest descriptorDigestA3 = DescriptorDigest.fromDigest("sha256:" + createGoodHash('a')); DescriptorDigest descriptorDigestB = DescriptorDigest.fromHash(createGoodHash('b')); Map digestMap = new HashMap<>(); digestMap.put(descriptorDigestA1, "digest with a"); Assert.assertEquals("digest with a", digestMap.get(descriptorDigestA1)); Assert.assertEquals("digest with a", digestMap.get(descriptorDigestA2)); Assert.assertEquals("digest with a", digestMap.get(descriptorDigestA3)); Assert.assertNull(digestMap.get(descriptorDigestB)); digestMap.put(descriptorDigestA2, "digest with a"); Assert.assertEquals("digest with a", digestMap.get(descriptorDigestA1)); Assert.assertEquals("digest with a", digestMap.get(descriptorDigestA2)); Assert.assertEquals("digest with a", digestMap.get(descriptorDigestA3)); Assert.assertNull(digestMap.get(descriptorDigestB)); digestMap.put(descriptorDigestA3, "digest with a"); Assert.assertEquals("digest with a", digestMap.get(descriptorDigestA1)); Assert.assertEquals("digest with a", digestMap.get(descriptorDigestA2)); Assert.assertEquals("digest with a", digestMap.get(descriptorDigestA3)); Assert.assertNull(digestMap.get(descriptorDigestB)); digestMap.put(descriptorDigestB, "digest with b"); Assert.assertEquals("digest with a", digestMap.get(descriptorDigestA1)); Assert.assertEquals("digest with a", digestMap.get(descriptorDigestA2)); Assert.assertEquals("digest with a", digestMap.get(descriptorDigestA3)); Assert.assertEquals("digest with b", digestMap.get(descriptorDigestB)); } /** Creates a 32 byte hexademical string to fit valid hash pattern. */ private static String createGoodHash(char character) { StringBuilder goodHashBuffer = new StringBuilder(64); for (int i = 0; i < 64; i++) { goodHashBuffer.append(character); } return goodHashBuffer.toString(); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/api/DockerClientResolverTest.java ================================================ /* * 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); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/api/DockerDaemonImageTest.java ================================================ /* * 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()); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/api/ImageReferenceTest.java ================================================ /* * 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.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; /** Tests for {@link ImageReference}. */ public class ImageReferenceTest { private static final List goodRegistries = Arrays.asList("some.domain---name.123.com:8080", "gcr.io", "localhost", null, ""); private static final List goodRepositories = Arrays.asList("some123_abc/repository__123-456/name---here", "distroless/java", "repository"); private static final List goodTags = Arrays.asList("some-.-.Tag", "", "latest", null); private static final List goodDigests = Arrays.asList( "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", null); private static final List badImageReferences = Arrays.asList( "", ":justsometag", "@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "repository@sha256:a", "repository@notadigest", "Repositorywithuppercase", "registry:8080/Repositorywithuppercase/repository:sometag", "domain.name:nonnumberport/repository", "domain.name:nonnumberport//:no-repository"); @Test public void testParse_pass() throws InvalidImageReferenceException { for (String goodRegistry : goodRegistries) { for (String goodRepository : goodRepositories) { for (String goodTag : goodTags) { for (String goodDigest : goodDigests) { verifyParse(goodRegistry, goodRepository, goodTag, goodDigest); } } } } } @Test public void testParse_dockerHub_official() throws InvalidImageReferenceException { String imageReferenceString = "busybox"; ImageReference imageReference = ImageReference.parse(imageReferenceString); Assert.assertEquals("registry-1.docker.io", imageReference.getRegistry()); Assert.assertEquals("library/busybox", imageReference.getRepository()); Assert.assertEquals("latest", imageReference.getTag().orElse(null)); } @Test public void testParse_dockerHub_user() throws InvalidImageReferenceException { String imageReferenceString = "someuser/someimage"; ImageReference imageReference = ImageReference.parse(imageReferenceString); Assert.assertEquals("registry-1.docker.io", imageReference.getRegistry()); Assert.assertEquals("someuser/someimage", imageReference.getRepository()); Assert.assertEquals("latest", imageReference.getTag().orElse(null)); } @Test public void testParse_invalid() { for (String badImageReference : badImageReferences) { try { ImageReference.parse(badImageReference); Assert.fail(badImageReference + " should not be a valid image reference"); } catch (InvalidImageReferenceException ex) { MatcherAssert.assertThat(ex.getMessage(), CoreMatchers.containsString(badImageReference)); } } } @Test public void testOf_smoke() { String expectedRegistry = "someregistry"; String expectedRepository = "somerepository"; String expectedTag = "sometag"; Assert.assertEquals( expectedRegistry, ImageReference.of(expectedRegistry, expectedRepository, expectedTag).getRegistry()); Assert.assertEquals( expectedRepository, ImageReference.of(expectedRegistry, expectedRepository, expectedTag).getRepository()); Assert.assertEquals( expectedTag, ImageReference.of(expectedRegistry, expectedRepository, expectedTag).getTag().orElse(null)); Assert.assertEquals( "registry-1.docker.io", ImageReference.of(null, expectedRepository, expectedTag).getRegistry()); Assert.assertEquals( "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(null, expectedRepository, null).getTag().orElse(null)); Assert.assertEquals( expectedRepository, ImageReference.of(null, expectedRepository, null).getRepository()); } @Test public void testToString() throws InvalidImageReferenceException { Assert.assertEquals("someimage", ImageReference.of(null, "someimage", null).toString()); Assert.assertEquals("someimage", ImageReference.of("", "someimage", "").toString()); Assert.assertEquals( "someotherimage", ImageReference.of(null, "library/someotherimage", null).toString()); Assert.assertEquals( "someregistry/someotherimage", ImageReference.of("someregistry", "someotherimage", null).toString()); 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 testToStringWithQualifier() { Assert.assertEquals( "someimage:latest", ImageReference.of(null, "someimage", null).toStringWithQualifier()); Assert.assertEquals( "someimage:latest", ImageReference.of("", "someimage", "").toStringWithQualifier()); Assert.assertEquals( "someotherimage:latest", ImageReference.of(null, "library/someotherimage", null).toStringWithQualifier()); Assert.assertEquals( "someregistry/someotherimage:latest", ImageReference.of("someregistry", "someotherimage", null).toStringWithQualifier()); Assert.assertEquals( "anotherregistry/anotherimage:sometag", 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()); } @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-1.docker.io"; } String expectedRepository = repository; if ("registry-1.docker.io".equals(expectedRegistry) && repository.indexOf('/') < 0) { expectedRepository = "library/" + expectedRepository; } String expectedTag = tag; 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(); if (!Strings.isNullOrEmpty(registry)) { imageReferenceBuilder.append(registry).append('/'); } imageReferenceBuilder.append(repository); if (!Strings.isNullOrEmpty(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().orElse(null)); Assert.assertEquals(expectedDigest, imageReference.getDigest().orElse(null)); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/api/JavaContainerBuilderTest.java ================================================ /* * 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()); } } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerBuilderTest.java ================================================ /* * 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()); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/api/JibContainerTest.java ================================================ /* * 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()); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/api/MainClassFinderTest.java ================================================ /* * 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()); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/api/PortsTest.java ================================================ /* * 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.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; /** 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"); ImmutableSet expected = new ImmutableSet.Builder() .add( 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(); 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 { Ports.parse(Collections.singletonList(input)); Assert.fail(); } catch (NumberFormatException ex) { Assert.assertEquals( "Invalid port configuration: '" + input + "'. Make sure the port is a single number or a range of two numbers separated " + "with a '-', with or without protocol specified (e.g. '/tcp' or " + "'/udp').", ex.getMessage()); } } try { Ports.parse(Collections.singletonList("4002-4000")); Assert.fail(); } catch (NumberFormatException ex) { Assert.assertEquals( "Invalid port range '4002-4000'; smaller number must come first.", ex.getMessage()); } badInputs = Arrays.asList("0", "70000", "0-400", "1-70000"); for (String input : badInputs) { try { Ports.parse(Collections.singletonList(input)); Assert.fail(); } catch (NumberFormatException ex) { Assert.assertEquals( "Port number '" + input + "' is out of usual range (1-65535).", ex.getMessage()); } } } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/api/RegistryImageTest.java ================================================ /* * 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()); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/api/TarImageTest.java ================================================ /* * 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()); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/blob/BlobTest.java ================================================ /* * 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.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; 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.Files; import java.nio.file.Path; import java.nio.file.Paths; import org.junit.Assert; import org.junit.Test; /** Tests for {@link Blob}. */ public class BlobTest { @Test public void testFromInputStream() throws IOException { String expected = "crepecake"; InputStream inputStream = new ByteArrayInputStream(expected.getBytes(StandardCharsets.UTF_8)); verifyBlobWriteTo(expected, Blobs.from(inputStream)); } @Test public void testFromFile() throws IOException, URISyntaxException { Path fileA = Paths.get(Resources.getResource("core/fileA").toURI()); String expected = new String(Files.readAllBytes(fileA), StandardCharsets.UTF_8); verifyBlobWriteTo(expected, Blobs.from(fileA)); } @Test public void testFromString() throws IOException { String expected = "crepecake"; verifyBlobWriteTo(expected, Blobs.from(expected)); } @Test public void testFromWritableContents() throws IOException { String expected = "crepecake"; WritableContents writableContents = outputStream -> outputStream.write(expected.getBytes(StandardCharsets.UTF_8)); verifyBlobWriteTo(expected, Blobs.from(writableContents, false)); } /** Checks that the {@link Blob} streams the expected string. */ private void verifyBlobWriteTo(String expected, Blob blob) throws IOException { OutputStream outputStream = new ByteArrayOutputStream(); BlobDescriptor blobDescriptor = blob.writeTo(outputStream); String output = outputStream.toString(); Assert.assertEquals(expected, output); byte[] expectedBytes = expected.getBytes(StandardCharsets.UTF_8); Assert.assertEquals(expectedBytes.length, blobDescriptor.getSize()); DescriptorDigest expectedDigest = Digests.computeDigest(new ByteArrayInputStream(expectedBytes)).getDigest(); Assert.assertEquals(expectedDigest, blobDescriptor.getDigest()); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/builder/ProgressEventDispatcherTest.java ================================================ /* * 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 } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/builder/TimerEventDispatcherTest.java ================================================ /* * 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; } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/builder/TimerTest.java ================================================ /* * 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); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildAndCacheApplicationLayerStepTest.java ================================================ /* * 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.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.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 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.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; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; /** Tests for {@link BuildAndCacheApplicationLayerStep}. */ @RunWith(MockitoJUnitRunner.class) public class BuildAndCacheApplicationLayerStepTest { // TODO: Consolidate with BuildStepsIntegrationTest. private static final AbsoluteUnixPath EXTRACTION_PATH_ROOT = AbsoluteUnixPath.get("/some/extraction/path/"); 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, CacheCorruptedException { ImmutableList fakeLayerConfigurations = ImmutableList.of( fakeDependenciesLayerConfiguration, fakeSnapshotDependenciesLayerConfiguration, fakeResourcesLayerConfiguration, fakeClassesLayerConfiguration, fakeExtraFilesLayerConfiguration); Mockito.when(mockBuildContext.getLayerConfigurations()).thenReturn(fakeLayerConfigurations); // Populates the cache. List applicationLayers = buildFakeLayersToCache(); Assert.assertEquals(5, applicationLayers.size()); 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. Assert.assertEquals( applicationLayers.get(0).getBlobDescriptor().getDigest(), dependenciesCachedLayer.getDigest()); Assert.assertEquals( applicationLayers.get(1).getBlobDescriptor().getDigest(), snapshotDependenciesCachedLayer.getDigest()); Assert.assertEquals( applicationLayers.get(2).getBlobDescriptor().getDigest(), resourcesCachedLayer.getDigest()); Assert.assertEquals( 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).getBlobDescriptor().getDigest(), dependenciesCachedLayer.getDigest()); Assert.assertEquals( applicationLayers.get(1).getBlobDescriptor().getDigest(), resourcesCachedLayer.getDigest()); Assert.assertEquals( 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()); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildImageStepTest.java ================================================ /* * 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.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.ProgressEventDispatcher; import com.google.cloud.tools.jib.cache.CachedLayer; 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.collect.ImmutableSet; import java.security.DigestException; 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; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; /** Tests for {@link BuildImageStep}. */ @RunWith(MockitoJUnitRunner.class) public class BuildImageStepTest { @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"); 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( 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_inheritedEntrypointAndProgramArguments() { Mockito.when(mockContainerConfiguration.getEntrypoint()).thenReturn(null); Mockito.when(mockContainerConfiguration.getProgramArguments()).thenReturn(null); Image image = new BuildImageStep( 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( "[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)); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildManifestListOrSingleManifestStepTest.java ================================================ /* * 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()); } } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/BuildResultTest.java ================================================ /* * 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)); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/LocalBaseImageStepsTest.java ================================================ /* * 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"))); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/ObtainBaseImageLayerStepTest.java ================================================ /* * 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()); } } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PlatformCheckerTest.java ================================================ /* * 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."); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStepTest.java ================================================ /* * 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; } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PushBlobStepTest.java ================================================ /* * 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(); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PushImageStepTest.java ================================================ /* * 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(); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/RegistryCredentialRetrieverTest.java ================================================ /* * 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(); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/StepsRunnerTest.java ================================================ /* * 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"); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageFilesTest.java ================================================ /* * 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"))); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java ================================================ /* * 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(); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageWriterTest.java ================================================ /* * 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()))); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheTest.java ================================================ /* * 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.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.nio.file.FileAlreadyExistsException; import java.nio.file.Files; 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; /** Tests for {@link Cache}. */ public class CacheTest { /** * 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); } /** * 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(); } 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 testWithDirectory_existsButNotDirectory() throws IOException { Path file = temporaryFolder.newFile().toPath(); try { Cache.withDirectory(file); Assert.fail(); } catch (CacheDirectoryCreationException ex) { MatcherAssert.assertThat( ex.getCause(), CoreMatchers.instanceOf(FileAlreadyExistsException.class)); } } @Test 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()); verifyIsLayer1(cache.writeUncompressedLayer(layerBlob1, layerEntries1)); verifyIsLayer1(cache.retrieve(layerEntries1).orElseThrow(AssertionError::new)); Assert.assertFalse(cache.retrieve(layerDigest2).isPresent()); // 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()); } /** * 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()); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/cache/CachedLayerTest.java ================================================ /* * 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.blob.Blobs; import java.io.IOException; 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 CachedLayer}. */ @RunWith(MockitoJUnitRunner.class) public class CachedLayerTest { @Mock private DescriptorDigest mockLayerDigest; @Mock private DescriptorDigest mockLayerDiffId; @Test public void testBuilder_fail() { try { CachedLayer.builder().build(); Assert.fail("missing required"); } catch (NullPointerException ex) { MatcherAssert.assertThat(ex.getMessage(), CoreMatchers.containsString("layerDigest")); } try { CachedLayer.builder().setLayerDigest(mockLayerDigest).build(); Assert.fail("missing required"); } catch (NullPointerException ex) { MatcherAssert.assertThat(ex.getMessage(), CoreMatchers.containsString("layerDiffId")); } try { CachedLayer.builder().setLayerDigest(mockLayerDigest).setLayerDiffId(mockLayerDiffId).build(); Assert.fail("missing required"); } 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())); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/cache/LayerEntriesSelectorTest.java ================================================ /* * 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))); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/cache/RetryTest.java ================================================ /* * 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. */ } } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/configuration/BuildContextTest.java ================================================ /* * 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); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/configuration/ContainerConfigurationTest.java ================================================ /* * 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()); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/configuration/DockerHealthCheckTest.java ================================================ /* * 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()); } } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/docker/AnotherDockerClient.java ================================================ /* * 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; } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/docker/CliDockerClientTest.java ================================================ /* * 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; }); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/docker/json/DockerManifestEntryTemplateTest.java ================================================ /* * 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()); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/event/EventHandlersTest.java ================================================ /* * 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); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/event/events/LogEventTest.java ================================================ /* * 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()); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/event/events/ProgressEventTest.java ================================================ /* * 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)); } } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/AllocationCompletionTrackerTest.java ================================================ /* * 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()); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/AllocationTest.java ================================================ /* * 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()); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/event/progress/ProgressEventHandlerTest.java ================================================ /* * 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); } } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/DirectoryWalkerTest.java ================================================ /* * 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.Path; import java.nio.file.Paths; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import org.junit.Assert; import org.junit.Before; import org.junit.Test; /** Tests for {@link DirectoryWalker}. */ public class DirectoryWalkerTest { private final Set walkedPaths = new HashSet<>(); private final PathConsumer addToWalkedPaths = walkedPaths::add; private Path testDir; @Before public void setUp() throws URISyntaxException { testDir = Paths.get(Resources.getResource("core/layer").toURI()); } @Test public void testWalk() throws IOException { new DirectoryWalker(testDir).walk(addToWalkedPaths); Set expectedPaths = new HashSet<>( Arrays.asList( testDir, testDir.resolve("a"), testDir.resolve("a").resolve("b"), testDir.resolve("a").resolve("b").resolve("bar"), testDir.resolve("c"), testDir.resolve("c").resolve("cat"), testDir.resolve("foo"))); Assert.assertEquals(expectedPaths, walkedPaths); } @Test public void testWalk_withFilter() throws IOException { // Filters to immediate subdirectories of testDir, and foo. new DirectoryWalker(testDir) .filter(path -> path.getParent().equals(testDir)) .filter(path -> !path.endsWith("foo")) .walk(addToWalkedPaths); Set expectedPaths = new HashSet<>(Arrays.asList(testDir.resolve("a"), testDir.resolve("c"))); Assert.assertEquals(expectedPaths, walkedPaths); } @Test public void testWalk_withFilterRoot() throws IOException { new DirectoryWalker(testDir).filterRoot().walk(addToWalkedPaths); Set expectedPaths = new HashSet<>( Arrays.asList( testDir.resolve("a"), testDir.resolve("a").resolve("b"), testDir.resolve("a").resolve("b").resolve("bar"), testDir.resolve("c"), testDir.resolve("c").resolve("cat"), testDir.resolve("foo"))); Assert.assertEquals(expectedPaths, walkedPaths); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/FileOperationsTest.java ================================================ /* * 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.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 org.junit.Assert; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; /** Tests for {@link FileOperations}. */ public class FileOperationsTest { @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("core/application/dependencies/libraryA.jar").toURI()); Path libraryB = 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); assertFilesEqual(libraryA, destDir.resolve("libraryA.jar")); assertFilesEqual(libraryB, destDir.resolve("libraryB.jar")); Assert.assertTrue(Files.exists(destDir.resolve("layer").resolve("a").resolve("b"))); Assert.assertTrue(Files.exists(destDir.resolve("layer").resolve("c"))); assertFilesEqual( dirLayer.resolve("a").resolve("b").resolve("bar"), destDir.resolve("layer").resolve("a").resolve("b").resolve("bar")); assertFilesEqual( dirLayer.resolve("c").resolve("cat"), destDir.resolve("layer").resolve("c").resolve("cat")); assertFilesEqual(dirLayer.resolve("foo"), destDir.resolve("layer").resolve("foo")); } private void assertFilesEqual(Path file1, Path file2) throws IOException { Assert.assertArrayEquals(Files.readAllBytes(file1), Files.readAllBytes(file2)); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/LockFileTest.java ================================================ /* * 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()); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/TempDirectoryProviderTest.java ================================================ /* * 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"))); } } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/filesystem/XdgDirectoriesTest.java ================================================ /* * 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())); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/frontend/CredentialRetrieverFactoryTest.java ================================================ /* * 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); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/global/JibSystemPropertiesTest.java ================================================ /* * 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()); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/hash/CountingDigestOutputStreamTest.java ================================================ /* * 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.common.collect.ImmutableMap; 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.security.DigestException; import java.util.Map; import org.junit.Assert; import org.junit.Test; /** Tests for {@link CountingDigestOutputStream}. */ public class CountingDigestOutputStreamTest { 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 : KNOWN_SHA256_HASHES.entrySet()) { String toHash = knownHash.getKey(); String expectedHash = knownHash.getValue(); OutputStream underlyingOutputStream = new ByteArrayOutputStream(); CountingDigestOutputStream countingDigestOutputStream = new CountingDigestOutputStream(underlyingOutputStream); byte[] bytesToHash = toHash.getBytes(StandardCharsets.UTF_8); InputStream toHashInputStream = new ByteArrayInputStream(bytesToHash); ByteStreams.copy(toHashInputStream, countingDigestOutputStream); BlobDescriptor blobDescriptor = countingDigestOutputStream.computeDigest(); Assert.assertEquals(DescriptorDigest.fromHash(expectedHash), blobDescriptor.getDigest()); Assert.assertEquals(bytesToHash.length, blobDescriptor.getSize()); } } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/http/FailoverHttpClientTest.java ================================================ /* * 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); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/http/NotifyingOutputStreamTest.java ================================================ /* * 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()); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/http/RequestTest.java ================================================ /* * 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 org.junit.Assert; import org.junit.Test; /** Tests for {@link Request}. */ public class RequestTest { @Test 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()); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/http/RequestWrapper.java ================================================ /* * 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(); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/http/ResponseTest.java ================================================ /* * 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.HttpResponse; import com.google.common.io.ByteStreams; import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; 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 Response}. */ @RunWith(MockitoJUnitRunner.class) public class ResponseTest { @Mock private HttpResponse httpResponseMock; @Test public void testGetContent() throws IOException { byte[] expectedResponse = "crepecake\nis\ngood!".getBytes(StandardCharsets.UTF_8); ByteArrayInputStream responseInputStream = new ByteArrayInputStream(expectedResponse); Mockito.when(httpResponseMock.getContent()).thenReturn(responseInputStream); try (Response response = new Response(httpResponseMock)) { Assert.assertArrayEquals(expectedResponse, ByteStreams.toByteArray(response.getBody())); } } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/http/TestWebServer.java ================================================ /* * 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; } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/http/WithServerFailoverHttpClientTest.java ================================================ /* * 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 ")); } } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTarballTest.java ================================================ /* * 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()); } } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/image/ImageTest.java ================================================ /* * 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.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.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; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; /** Tests for {@link Image}. */ @RunWith(MockitoJUnitRunner.class) public class ImageTest { @Mock private Layer mockLayer; @Mock private DescriptorDigest mockDescriptorDigest; @Before public void setUp() throws LayerPropertyNotFoundException { Mockito.when(mockLayer.getBlobDescriptor()) .thenReturn(new BlobDescriptor(mockDescriptorDigest)); } @Test public void test_smokeTest() throws LayerPropertyNotFoundException { Image image = Image.builder(V22ManifestTemplate.class) .setCreated(Instant.ofEpochSecond(10000)) .addEnvironmentVariable("crepecake", "is great") .addEnvironmentVariable("VARIABLE", "VALUE") .setEntrypoint(Arrays.asList("some", "command")) .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(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.getProgramArguments()); Assert.assertEquals(ImmutableSet.of(Port.tcp(1000), Port.tcp(2000)), image.getExposedPorts()); Assert.assertEquals( 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()); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/image/LayerTest.java ================================================ /* * 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.image; import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.blob.BlobDescriptor; 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 Layer}. */ @RunWith(MockitoJUnitRunner.class) public class LayerTest { @Mock private DescriptorDigest mockDescriptorDigest; @Mock private BlobDescriptor mockBlobDescriptor; @Mock private DescriptorDigest mockDiffId; @Test public void testNew_reference() throws LayerPropertyNotFoundException { Layer layer = new ReferenceLayer(mockBlobDescriptor, mockDiffId); try { layer.getBlob(); Assert.fail("Blob content should not be available for reference layer"); } catch (LayerPropertyNotFoundException ex) { Assert.assertEquals("Blob not available for reference layer", ex.getMessage()); } Assert.assertEquals(mockBlobDescriptor, layer.getBlobDescriptor()); Assert.assertEquals(mockDiffId, layer.getDiffId()); } @Test public void testNew_digestOnly() throws LayerPropertyNotFoundException { Layer layer = new DigestOnlyLayer(mockDescriptorDigest); try { layer.getBlob(); Assert.fail("Blob content should not be available for digest-only layer"); } catch (LayerPropertyNotFoundException ex) { Assert.assertEquals("Blob not available for digest-only layer", ex.getMessage()); } Assert.assertFalse(layer.getBlobDescriptor().hasSize()); Assert.assertEquals(mockDescriptorDigest, layer.getBlobDescriptor().getDigest()); try { layer.getDiffId(); Assert.fail("Diff ID should not be available for digest-only layer"); } catch (LayerPropertyNotFoundException ex) { Assert.assertEquals("Diff ID not available for digest-only layer", ex.getMessage()); } } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/image/ReproducibleLayerBuilderTest.java ================================================ /* * 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 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.common.collect.ImmutableList; import com.google.common.io.ByteStreams; import com.google.common.io.Resources; import java.io.BufferedOutputStream; 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.nio.file.StandardOpenOption; import java.nio.file.attribute.FileTime; import java.time.Instant; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; /** Tests for {@link ReproducibleLayerBuilder}. */ public class ReproducibleLayerBuilderTest { /** * Verifies the correctness of the next {@link TarArchiveEntry} in the {@link * TarArchiveInputStream}. * * @param tarArchiveInputStream the {@link TarArchiveInputStream} to read from * @param expectedExtractionPath the expected extraction path of the next entry * @param expectedFile the file to match against the contents of the next entry * @throws IOException if an I/O exception occurs */ private static void verifyNextTarArchiveEntry( TarArchiveInputStream tarArchiveInputStream, String expectedExtractionPath, Path expectedFile) throws IOException { TarArchiveEntry header = tarArchiveInputStream.getNextEntry(); assertThat(header.getName()).isEqualTo(expectedExtractionPath); byte[] expectedBytes = Files.readAllBytes(expectedFile); byte[] extractedBytes = ByteStreams.toByteArray(tarArchiveInputStream); assertThat(extractedBytes).isEqualTo(expectedBytes); } /** * 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); } 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(); @Test public void testBuild() throws URISyntaxException, IOException { Path layerDirectory = Paths.get(Resources.getResource("core/layer").toURI()); Path blobA = Paths.get(Resources.getResource("core/blobA").toURI()); ReproducibleLayerBuilder layerBuilder = 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. Blob unwrittenBlob = layerBuilder.build(); Path temporaryFile = temporaryFolder.newFile().toPath(); try (OutputStream temporaryFileOutputStream = new BufferedOutputStream(Files.newOutputStream(temporaryFile))) { unwrittenBlob.writeTo(temporaryFileOutputStream); } // Reads the file back. try (TarArchiveInputStream tarArchiveInputStream = new TarArchiveInputStream(Files.newInputStream(temporaryFile))) { verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/"); verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/here/"); verifyNextTarArchiveEntryIsDirectory(tarArchiveInputStream, "extract/here/apple/"); verifyNextTarArchiveEntry(tarArchiveInputStream, "extract/here/apple/blobA", blobA); 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); } } @Test public void testToBlob_reproducibility() throws IOException { Path testRoot = temporaryFolder.getRoot().toPath(); Path root1 = Files.createDirectories(testRoot.resolve("files1")); Path root2 = Files.createDirectories(testRoot.resolve("files2")); // 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); Path fileA2 = createFile(root2, "fileA", contentA, 20000); String contentB = "yumyum"; Path fileB1 = createFile(root1, "fileB", contentB, 10000); Path fileB2 = createFile(root2, "fileB", contentB, 20000); // 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( ImmutableList.of( defaultLayerEntry(fileA1, AbsoluteUnixPath.get("/somewhere/fileA")), defaultLayerEntry(fileB1, AbsoluteUnixPath.get("/somewhere/fileB")))) .build(); Blob reproduced = 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); assertThat(layerContent).isEqualTo(reproducedLayerContent); } @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(modificationTime)); return newFile; } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ContainerConfigurationTemplateTest.java ================================================ /* * 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.image.json; 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.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 java.time.Instant; import java.util.Arrays; import org.junit.Assert; import org.junit.Test; /** Tests for {@link ContainerConfigurationTemplate}. */ public class ContainerConfigurationTemplateTest { @Test public void testToJson() throws IOException, URISyntaxException, DigestException { // Loads the expected JSON string. 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", ImmutableMap.of(), "2000/tcp", 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. Assert.assertEquals(expectedJson, JsonTemplateMapper.toUtf8String(containerConfigJson)); } @Test public void testFromJson() throws IOException, URISyntaxException, DigestException { // Loads the JSON string. 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()); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ImageToJsonTranslatorTest.java ================================================ /* * 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.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.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.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 java.time.Duration; import java.time.Instant; import java.util.Arrays; import java.util.Map; import org.junit.Assert; import org.junit.Test; /** Tests for {@link ImageToJsonTranslator}. */ public class ImageToJsonTranslatorTest { private ImageToJsonTranslator imageToJsonTranslator; 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"); 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, DigestException { setUp(V22ManifestTemplate.class); // Loads the expected JSON string. 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. JsonTemplate containerConfiguration = imageToJsonTranslator.getContainerConfiguration(); Assert.assertEquals(expectedJson, JsonTemplateMapper.toUtf8String(containerConfiguration)); } @Test 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, DigestException { setUp(OciManifestTemplate.class); testGetManifest(OciManifestTemplate.class, "core/json/translated_ocimanifest.json"); } @Test public void testPortListToMap() { 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.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}. */ private void testGetManifest( Class manifestTemplateClass, String translatedJsonFilename) throws URISyntaxException, IOException { // Loads the expected JSON string. Path jsonFile = Paths.get(Resources.getResource(translatedJsonFilename).toURI()); String expectedJson = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); // Translates the image to the manifest and writes the JSON string. JsonTemplate containerConfiguration = imageToJsonTranslator.getContainerConfiguration(); BlobDescriptor blobDescriptor = Digests.computeDigest(containerConfiguration); T manifestTemplate = imageToJsonTranslator.getManifestTemplate(manifestTemplateClass, blobDescriptor); Assert.assertEquals(expectedJson, JsonTemplateMapper.toUtf8String(manifestTemplate)); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/image/json/JsonToImageTranslatorTest.java ================================================ /* * 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.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.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.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; /** Tests for {@link JsonToImageTranslator}. */ public class JsonToImageTranslatorTest { @Test public void testToImage_v21() throws IOException, LayerPropertyNotFoundException, DigestException, URISyntaxException, BadContainerConfigurationFormatException { // Loads the JSON string. Path jsonFile = 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); List layers = image.getLayers(); Assert.assertEquals(2, layers.size()); Assert.assertEquals( DescriptorDigest.fromDigest( "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("core/json/v22manifest.json", V22ManifestTemplate.class); } @Test public void testToImage_oci() throws IOException, LayerPropertyNotFoundException, LayerCountMismatchException, DigestException, URISyntaxException, BadContainerConfigurationFormatException { 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.of( "1000", ImmutableMap.of(), "2000/tcp", ImmutableMap.of(), "3000/udp", ImmutableMap.of()); ImmutableSet expected = ImmutableSet.of(Port.tcp(1000), Port.tcp(2000), Port.udp(3000)); Assert.assertEquals(expected, JsonToImageTranslator.portMapToSet(input)); 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) { try { 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("core/json/containerconfig.json").toURI()); ContainerConfigurationTemplate containerConfigurationTemplate = JsonTemplateMapper.readJsonFromFile( containerConfigurationJsonFile, ContainerConfigurationTemplate.class); // Loads the manifest JSON. Path manifestJsonFile = Paths.get(getClass().getClassLoader().getResource(jsonFilename).toURI()); T manifestTemplate = JsonTemplateMapper.readJsonFromFile(manifestJsonFile, manifestTemplateClass); Image image = JsonToImageTranslator.toImage(manifestTemplate, containerConfigurationTemplate); List layers = image.getLayers(); Assert.assertEquals(1, layers.size()); Assert.assertEquals( new BlobDescriptor( 1000000, DescriptorDigest.fromDigest( "sha256:4945ba5011739b0b98c4a41afe224e417f47c7c99b2ce76830999c9a0861b236")), layers.get(0).getBlobDescriptor()); Assert.assertEquals( DescriptorDigest.fromDigest( "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"), layers.get(0).getDiffId()); 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()), 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()); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/image/json/ManifestListGeneratorTest.java ================================================ /* * 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()); } } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OciIndexTemplateTest.java ================================================ /* * 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 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)); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/image/json/OciManifestTemplateTest.java ================================================ /* * 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.image.json; 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; 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 OciManifestTemplate}. */ public class OciManifestTemplateTest { @Test public void testToJson() throws DigestException, IOException, URISyntaxException { // Loads the expected JSON string. 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(); manifestJson.setContainerConfiguration( 1000, DescriptorDigest.fromDigest( "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad")); manifestJson.addLayer( 1000_000, DescriptorDigest.fromHash( "4945ba5011739b0b98c4a41afe224e417f47c7c99b2ce76830999c9a0861b236")); // Serializes the JSON object. Assert.assertEquals(expectedJson, JsonTemplateMapper.toUtf8String(manifestJson)); } @Test public void testFromJson() throws IOException, URISyntaxException, DigestException { // Loads the JSON string. Path jsonFile = Paths.get(Resources.getResource("core/json/ocimanifest.json").toURI()); // Deserializes into a manifest JSON object. OciManifestTemplate manifestJson = JsonTemplateMapper.readJsonFromFile(jsonFile, OciManifestTemplate.class); Assert.assertEquals( DescriptorDigest.fromDigest( "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"), manifestJson.getContainerConfiguration().getDigest()); Assert.assertEquals(1000, manifestJson.getContainerConfiguration().getSize()); Assert.assertEquals( DescriptorDigest.fromHash( "4945ba5011739b0b98c4a41afe224e417f47c7c99b2ce76830999c9a0861b236"), manifestJson.getLayers().get(0).getDigest()); Assert.assertEquals(1000_000, manifestJson.getLayers().get(0).getSize()); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V21ManifestTemplateTest.java ================================================ /* * 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.image.json; 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; import java.net.URISyntaxException; 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; /** Tests for {@link V21ManifestTemplate}. */ public class V21ManifestTemplateTest { @Test public void testFromJson() throws URISyntaxException, IOException, DigestException { // Loads the JSON string. Path jsonFile = Paths.get(Resources.getResource("core/json/v21manifest.json").toURI()); // Deserializes into a manifest JSON object. V21ManifestTemplate manifestJson = JsonTemplateMapper.readJsonFromFile(jsonFile, V21ManifestTemplate.class); Assert.assertEquals( DescriptorDigest.fromDigest( "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"), manifestJson.getFsLayers().get(0).getDigest()); 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()); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestListTemplateTest.java ================================================ /* * 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()); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/image/json/V22ManifestTemplateTest.java ================================================ /* * 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.image.json; 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.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 java.util.Arrays; import java.util.List; import org.junit.Assert; import org.junit.Test; /** Tests for {@link V22ManifestTemplate}. */ public class V22ManifestTemplateTest { @Test public void testToJson() throws DigestException, IOException, URISyntaxException { // Loads the expected JSON string. 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. V22ManifestTemplate manifestJson = new V22ManifestTemplate(); manifestJson.setContainerConfiguration( 1000, DescriptorDigest.fromDigest( "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad")); manifestJson.addLayer( 1000_000, DescriptorDigest.fromHash( "4945ba5011739b0b98c4a41afe224e417f47c7c99b2ce76830999c9a0861b236")); // Serializes the JSON object. Assert.assertEquals(expectedJson, JsonTemplateMapper.toUtf8String(manifestJson)); } @Test public void testFromJson() throws IOException, URISyntaxException, DigestException { // Loads the JSON string. Path jsonFile = Paths.get(Resources.getResource("core/json/v22manifest.json").toURI()); // Deserializes into a manifest JSON object. V22ManifestTemplate manifestJson = JsonTemplateMapper.readJsonFromFile(jsonFile, V22ManifestTemplate.class); Assert.assertEquals( DescriptorDigest.fromDigest( "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"), manifestJson.getContainerConfiguration().getDigest()); Assert.assertEquals(1000, manifestJson.getContainerConfiguration().getSize()); Assert.assertEquals( DescriptorDigest.fromHash( "4945ba5011739b0b98c4a41afe224e417f47c7c99b2ce76830999c9a0861b236"), manifestJson.getLayers().get(0).getDigest()); 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()); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/json/JsonTemplateMapperTest.java ================================================ /* * 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.json; import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.common.io.Resources; import java.io.ByteArrayInputStream; 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 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; /** Tests for {@link JsonTemplateMapper}. */ public class JsonTemplateMapperTest { private static class TestJson implements JsonTemplate { private int number; private String text; private DescriptorDigest digest; private InnerObject innerObject; private List list; private static class InnerObject implements JsonTemplate { // This field has the same name as a field in the outer class, but either NOT interfere with // the other. private int number; private List texts; private List digests; } } @Test public void testWriteJson() throws DigestException, IOException, URISyntaxException { 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(); testJson.number = 54; testJson.text = "crepecake"; testJson.digest = DescriptorDigest.fromDigest( "sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad"); testJson.innerObject = new TestJson.InnerObject(); testJson.innerObject.number = 23; testJson.innerObject.texts = Arrays.asList("first text", "second text"); testJson.innerObject.digests = Arrays.asList( DescriptorDigest.fromDigest( "sha256:91e0cae00b86c289b33fee303a807ae72dd9f0315c16b74e6ab0cdbe9d996c10"), DescriptorDigest.fromHash( "4945ba5011739b0b98c4a41afe224e417f47c7c99b2ce76830999c9a0861b236")); TestJson.InnerObject innerObject1 = new TestJson.InnerObject(); innerObject1.number = 42; innerObject1.texts = Collections.emptyList(); TestJson.InnerObject innerObject2 = new TestJson.InnerObject(); innerObject2.number = 99; innerObject2.texts = Collections.singletonList("some text"); innerObject2.digests = Collections.singletonList( DescriptorDigest.fromDigest( "sha256:d38f571aa1c11e3d516e0ef7e513e7308ccbeb869770cb8c4319d63b10a0075e")); testJson.list = Arrays.asList(innerObject1, innerObject2); 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(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); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java ================================================ /* * 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.HttpHeaders; import com.google.api.client.http.HttpMethods; 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; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; /** Tests for {@link AuthenticationMethodRetriever}. */ @RunWith(MockitoJUnitRunner.class) public class AuthenticationMethodRetrieverTest { @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, "user-agent", httpClient); @Test public void testGetContent() { Assert.assertNull(testAuthenticationMethodRetriever.getContent()); } @Test public void testGetAccept() { Assert.assertEquals(0, testAuthenticationMethodRetriever.getAccept().size()); } @Test public void testHandleResponse() { Assert.assertFalse( testAuthenticationMethodRetriever.handleResponse(Mockito.mock(Response.class)).isPresent()); } @Test public void testGetApiRoute() throws MalformedURLException { Assert.assertEquals( new URL("http://someApiBase/"), testAuthenticationMethodRetriever.getApiRoute("http://someApiBase/")); } @Test public void testGetHttpMethod() { Assert.assertEquals(HttpMethods.GET, testAuthenticationMethodRetriever.getHttpMethod()); } @Test public void testGetActionDescription() { Assert.assertEquals( "retrieve authentication method for someServerUrl", testAuthenticationMethodRetriever.getActionDescription()); } @Test public void testHandleHttpResponseException_invalidStatusCode() throws RegistryErrorException { Mockito.when(mockResponseException.getStatusCode()).thenReturn(-1); try { testAuthenticationMethodRetriever.handleHttpResponseException(mockResponseException); Assert.fail( "Authentication method retriever should only handle HTTP 401 Unauthorized errors"); } catch (ResponseException ex) { Assert.assertEquals(mockResponseException, ex); } } @Test public void testHandleHttpResponseException_noHeader() throws ResponseException { Mockito.when(mockResponseException.getStatusCode()) .thenReturn(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED); Mockito.when(mockResponseException.getHeaders()).thenReturn(mockHeaders); Mockito.when(mockHeaders.getAuthenticate()).thenReturn(null); try { testAuthenticationMethodRetriever.handleHttpResponseException(mockResponseException); Assert.fail( "Authentication method retriever should fail if 'WWW-Authenticate' header is not found"); } catch (RegistryErrorException ex) { MatcherAssert.assertThat( ex.getMessage(), CoreMatchers.containsString("'WWW-Authenticate' header not found")); } } @Test public void testHandleHttpResponseException_badAuthenticationMethod() throws ResponseException { String authenticationMethod = "bad authentication method"; Mockito.when(mockResponseException.getStatusCode()) .thenReturn(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED); Mockito.when(mockResponseException.getHeaders()).thenReturn(mockHeaders); Mockito.when(mockHeaders.getAuthenticate()).thenReturn(authenticationMethod); try { testAuthenticationMethodRetriever.handleHttpResponseException(mockResponseException); Assert.fail( "Authentication method retriever should fail if 'WWW-Authenticate' header failed to parse"); } catch (RegistryErrorException ex) { MatcherAssert.assertThat( ex.getMessage(), CoreMatchers.containsString( "Failed get authentication method from 'WWW-Authenticate' header")); } } @Test public void testHandleHttpResponseException_pass() throws RegistryErrorException, ResponseException, MalformedURLException { String authenticationMethod = "Bearer realm=\"https://somerealm\",service=\"someservice\",scope=\"somescope\""; Mockito.when(mockResponseException.getStatusCode()) .thenReturn(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED); Mockito.when(mockResponseException.getHeaders()).thenReturn(mockHeaders); Mockito.when(mockHeaders.getAuthenticate()).thenReturn(authenticationMethod); RegistryAuthenticator registryAuthenticator = testAuthenticationMethodRetriever.handleHttpResponseException(mockResponseException).get(); Assert.assertEquals( new URL("https://somerealm?service=someservice&scope=repository:someImageName:someScope"), registryAuthenticator.getAuthenticationUrl( null, Collections.singletonMap("someImageName", "someScope"))); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobCheckerTest.java ================================================ /* * 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.HttpStatusCodes; import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.blob.BlobDescriptor; 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 java.io.IOException; import java.net.MalformedURLException; 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; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; /** Tests for {@link BlobChecker}. */ @RunWith(MockitoJUnitRunner.class) public class BlobCheckerTest { @Mock private Response mockResponse; private final RegistryEndpointRequestProperties fakeRegistryEndpointRequestProperties = new RegistryEndpointRequestProperties("someServerUrl", "someImageName"); private BlobChecker testBlobChecker; private DescriptorDigest fakeDigest; @Before public void setUpFakes() throws DigestException { fakeDigest = DescriptorDigest.fromHash( "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); testBlobChecker = new BlobChecker(fakeRegistryEndpointRequestProperties, fakeDigest); } @Test public void testHandleResponse() throws RegistryErrorException { Mockito.when(mockResponse.getContentLength()).thenReturn(0L); BlobDescriptor expectedBlobDescriptor = new BlobDescriptor(0, fakeDigest); BlobDescriptor blobDescriptor = testBlobChecker.handleResponse(mockResponse).get(); Assert.assertEquals(expectedBlobDescriptor, blobDescriptor); } @Test public void testHandleResponse_noContentLength() { Mockito.when(mockResponse.getContentLength()).thenReturn(-1L); try { testBlobChecker.handleResponse(mockResponse); Assert.fail("Should throw exception if Content-Length header is not present"); } catch (RegistryErrorException ex) { MatcherAssert.assertThat( ex.getMessage(), CoreMatchers.containsString("Did not receive Content-Length header")); } } @Test 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(mockResponseException.getContent()) .thenReturn(JsonTemplateMapper.toUtf8String(emptyErrorResponseTemplate)); Assert.assertFalse( testBlobChecker.handleHttpResponseException(mockResponseException).isPresent()); } @Test 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(mockResponseException.getContent()) .thenReturn(JsonTemplateMapper.toUtf8String(emptyErrorResponseTemplate)); try { testBlobChecker.handleHttpResponseException(mockResponseException); Assert.fail("Non-BLOB_UNKNOWN errors should not be handled"); } catch (ResponseException ex) { Assert.assertEquals(mockResponseException, ex); } } @Test 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(mockResponseException.getContent()) .thenReturn(JsonTemplateMapper.toUtf8String(emptyErrorResponseTemplate)); try { testBlobChecker.handleHttpResponseException(mockResponseException); Assert.fail("Non-BLOB_UNKNOWN errors should not be handled"); } catch (ResponseException ex) { Assert.assertEquals(mockResponseException, ex); } } @Test public void testHandleHttpResponseException_invalidStatusCode() { ResponseException mockResponseException = Mockito.mock(ResponseException.class); Mockito.when(mockResponseException.getStatusCode()).thenReturn(-1); try { testBlobChecker.handleHttpResponseException(mockResponseException); Assert.fail("Non-404 status codes should not be handled"); } catch (ResponseException ex) { Assert.assertEquals(mockResponseException, ex); } } @Test public void testGetApiRoute() throws MalformedURLException { Assert.assertEquals( new URL("http://someApiBase/someImageName/blobs/" + fakeDigest), testBlobChecker.getApiRoute("http://someApiBase/")); } @Test public void testGetContent() { Assert.assertNull(testBlobChecker.getContent()); } @Test public void testGetAccept() { Assert.assertEquals(0, testBlobChecker.getAccept().size()); } @Test public void testGetActionDescription() { Assert.assertEquals( "check BLOB exists for someServerUrl/someImageName with digest " + fakeDigest, testBlobChecker.getActionDescription()); } @Test public void testGetHttpMethod() { Assert.assertEquals("HEAD", testBlobChecker.getHttpMethod()); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPullerTest.java ================================================ /* * 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.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 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; import org.junit.runner.RunWith; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; /** Tests for {@link BlobPuller}. */ @RunWith(MockitoJUnitRunner.class) public class BlobPullerTest { private final RegistryEndpointRequestProperties fakeRegistryEndpointRequestProperties = new RegistryEndpointRequestProperties("someServerUrl", "someImageName"); private DescriptorDigest fakeDigest; private final ByteArrayOutputStream layerContentOutputStream = new ByteArrayOutputStream(); private final CountingDigestOutputStream layerOutputStream = new CountingDigestOutputStream(layerContentOutputStream); private BlobPuller testBlobPuller; @Before public void setUpFakes() throws DigestException { fakeDigest = DescriptorDigest.fromHash( "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); testBlobPuller = new BlobPuller( fakeRegistryEndpointRequestProperties, fakeDigest, layerOutputStream, ignored -> {}, ignored -> {}); } @Test public void testHandleResponse() throws IOException, UnexpectedBlobDigestException { 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.getContentLength()).thenReturn((long) "some BLOB content".length()); Mockito.when(mockResponse.getBody()).thenReturn(blobContent); LongAdder byteCount = new LongAdder(); BlobPuller blobPuller = 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.computeDigest().getDigest()); Assert.assertEquals("some BLOB content".length(), byteCount.sum()); } @Test public void testHandleResponse_unexpectedDigest() throws IOException { 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(blobContent); try { testBlobPuller.handleResponse(mockResponse); Assert.fail("Receiving an unexpected digest should fail"); } catch (UnexpectedBlobDigestException ex) { Assert.assertEquals( "The pulled BLOB has digest '" + testBlobDigest + "', but the request digest was '" + fakeDigest + "'", ex.getMessage()); } } @Test public void testGetApiRoute() throws MalformedURLException { Assert.assertEquals( new URL("http://someApiBase/someImageName/blobs/" + fakeDigest), testBlobPuller.getApiRoute("http://someApiBase/")); } @Test public void testGetActionDescription() { Assert.assertEquals( "pull BLOB for someServerUrl/someImageName with digest " + fakeDigest, testBlobPuller.getActionDescription()); } @Test public void testGetHttpMethod() { Assert.assertEquals("GET", testBlobPuller.getHttpMethod()); } @Test public void testGetContent() { Assert.assertNull(testBlobPuller.getContent()); } @Test public void testGetAccept() { Assert.assertEquals(0, testBlobPuller.getAccept().size()); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/registry/BlobPusherTest.java ================================================ /* * 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.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 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; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; /** Tests for {@link BlobPusher}. */ @RunWith(MockitoJUnitRunner.class) public class BlobPusherTest { 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; private BlobPusher testBlobPusher; @Before public void setUpFakes() throws DigestException { fakeDescriptorDigest = DescriptorDigest.fromHash( "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); testBlobPusher = new BlobPusher( new RegistryEndpointRequestProperties("someServerUrl", "someImageName"), fakeDescriptorDigest, TEST_BLOB, null); } @Test public void testInitializer_getContent() { Assert.assertNull(testBlobPusher.initializer().getContent()); } @Test public void testGetAccept() { Assert.assertEquals(0, testBlobPusher.initializer().getAccept().size()); } @Test public void testInitializer_handleResponse_created() throws IOException, RegistryException { Mockito.when(mockResponse.getStatusCode()).thenReturn(201); // Created Assert.assertFalse(testBlobPusher.initializer().handleResponse(mockResponse).isPresent()); } @Test public void testInitializer_handleResponse_accepted() throws IOException, RegistryException { Mockito.when(mockResponse.getStatusCode()).thenReturn(202); // Accepted Mockito.when(mockResponse.getHeader("Location")) .thenReturn(Collections.singletonList("location")); GenericUrl requestUrl = new GenericUrl("https://someurl"); Mockito.when(mockResponse.getRequestUrl()).thenReturn(requestUrl); Assert.assertEquals( new URL("https://someurl/location"), testBlobPusher.initializer().handleResponse(mockResponse).get()); } @Test public void testInitializer_handleResponse_accepted_multipleLocations() throws IOException, RegistryException { Mockito.when(mockResponse.getStatusCode()).thenReturn(202); // Accepted Mockito.when(mockResponse.getHeader("Location")) .thenReturn(Arrays.asList("location1", "location2")); try { testBlobPusher.initializer().handleResponse(mockResponse); Assert.fail("Multiple 'Location' headers should be a registry error"); } catch (RegistryErrorException ex) { MatcherAssert.assertThat( ex.getMessage(), CoreMatchers.containsString("Expected 1 'Location' header, but found 2")); } } @Test public void testInitializer_handleResponse_unrecognized() throws IOException, RegistryException { Mockito.when(mockResponse.getStatusCode()).thenReturn(-1); // Unrecognized try { testBlobPusher.initializer().handleResponse(mockResponse); Assert.fail("Multiple 'Location' headers should be a registry error"); } catch (RegistryErrorException ex) { MatcherAssert.assertThat( ex.getMessage(), CoreMatchers.containsString("Received unrecognized status code -1")); } } @Test public void testInitializer_getApiRoute_nullSource() throws MalformedURLException { Assert.assertEquals( 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/")); } @Test public void testInitializer_getHttpMethod() { Assert.assertEquals("POST", testBlobPusher.initializer().getHttpMethod()); } @Test public void testInitializer_getActionDescription() { Assert.assertEquals( "push BLOB for someServerUrl/someImageName with digest " + fakeDescriptorDigest, testBlobPusher.initializer().getActionDescription()); } @Test public void testWriter_getContent() throws IOException { LongAdder byteCount = new LongAdder(); BlobHttpContent body = testBlobPusher.writer(mockUrl, byteCount::add).getContent(); Assert.assertNotNull(body); Assert.assertEquals("application/octet-stream", body.getType()); 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, ignored -> {}).getAccept().size()); } @Test public void testWriter_handleResponse() throws IOException, RegistryException { Mockito.when(mockResponse.getHeader("Location")) .thenReturn(Collections.singletonList("https://somenewurl/location")); GenericUrl requestUrl = new GenericUrl("https://someurl"); Mockito.when(mockResponse.getRequestUrl()).thenReturn(requestUrl); Assert.assertEquals( new URL("https://somenewurl/location"), 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, ignored -> {}).getApiRoute("")); } @Test public void testWriter_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, ignored -> {}).getActionDescription()); } @Test public void testCommitter_getContent() { Assert.assertNull(testBlobPusher.committer(mockUrl).getContent()); } @Test public void testCommitter_GetAccept() { 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))); } @Test public void testCommitter_getApiRoute() throws MalformedURLException { Assert.assertEquals( new URL("https://someurl?somequery=somevalue&digest=" + fakeDescriptorDigest), testBlobPusher.committer(new URL("https://someurl?somequery=somevalue")).getApiRoute("")); } @Test public void testCommitter_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()); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/registry/DockerRegistryBearerTokenTest.java ================================================ /* * 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")); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/registry/ErrorResponseUtilTest.java ================================================ /* * 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); } } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPullerTest.java ================================================ /* * 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.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.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; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; /** Tests for {@link ManifestPuller}. */ @RunWith(MockitoJUnitRunner.class) public class ManifestPullerTest { private static InputStream stringToInputStreamUtf8(String string) { return new ByteArrayInputStream(string.getBytes(StandardCharsets.UTF_8)); } private final RegistryEndpointRequestProperties fakeRegistryEndpointRequestProperties = new RegistryEndpointRequestProperties("someServerUrl", "someImageName"); private final ManifestPuller testManifestPuller = 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("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(v21Manifest); ManifestAndDigest manifestAndDigest = new ManifestPuller<>( fakeRegistryEndpointRequestProperties, "test-image-tag", V21ManifestTemplate.class) .handleResponse(mockResponse); 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("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(v22Manifest); ManifestAndDigest manifestAndDigest = new ManifestPuller<>( fakeRegistryEndpointRequestProperties, "test-image-tag", V22ManifestTemplate.class) .handleResponse(mockResponse); 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(stringToInputStreamUtf8("{}")); try { testManifestPuller.handleResponse(mockResponse); Assert.fail("An empty manifest should throw an error"); } catch (UnknownManifestFormatException ex) { Assert.assertEquals("Cannot find field 'schemaVersion' in manifest", ex.getMessage()); } } @Test public void testHandleResponse_invalidSchemaVersion() throws IOException { Mockito.when(mockResponse.getBody()) .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()); } } @Test public void testHandleResponse_unknownSchemaVersion() throws IOException { Mockito.when(mockResponse.getBody()) .thenReturn(stringToInputStreamUtf8("{\"schemaVersion\":0}")); try { testManifestPuller.handleResponse(mockResponse); Assert.fail("An unknown manifest schemaVersion should throw an error"); } catch (UnknownManifestFormatException ex) { Assert.assertEquals("Unknown schemaVersion: 0 - only 1 and 2 are supported", ex.getMessage()); } } @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( new URL("http://someApiBase/someImageName/manifests/test-image-tag"), testManifestPuller.getApiRoute("http://someApiBase/")); } @Test public void testGetHttpMethod() { Assert.assertEquals("GET", testManifestPuller.getHttpMethod()); } @Test public void testGetActionDescription() { Assert.assertEquals( "pull image manifest for someServerUrl/someImageName:test-image-tag", testManifestPuller.getActionDescription()); } @Test public void testGetContent() { Assert.assertNull(testManifestPuller.getContent()); } @Test public void testGetAccept() { Assert.assertEquals( Arrays.asList( OciManifestTemplate.MANIFEST_MEDIA_TYPE, V22ManifestTemplate.MANIFEST_MEDIA_TYPE, V21ManifestTemplate.MEDIA_TYPE, V22ManifestListTemplate.MANIFEST_MEDIA_TYPE, OciIndexTemplate.MEDIA_TYPE), testManifestPuller.getAccept()); Assert.assertEquals( Collections.singletonList(OciManifestTemplate.MANIFEST_MEDIA_TYPE), new ManifestPuller<>( fakeRegistryEndpointRequestProperties, "test-image-tag", OciManifestTemplate.class) .getAccept()); Assert.assertEquals( Collections.singletonList(V22ManifestTemplate.MANIFEST_MEDIA_TYPE), new ManifestPuller<>( fakeRegistryEndpointRequestProperties, "test-image-tag", V22ManifestTemplate.class) .getAccept()); Assert.assertEquals( Collections.singletonList(V21ManifestTemplate.MEDIA_TYPE), 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()); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/registry/ManifestPusherTest.java ================================================ /* * 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.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; import java.io.ByteArrayOutputStream; import java.io.IOException; 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.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; /** Tests for {@link ManifestPusher}. */ @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("core/json/v22manifest.json").toURI()); fakeManifestTemplate = JsonTemplateMapper.readJsonFromFile(v22manifestJsonFile, V22ManifestTemplate.class); testManifestPusher = new ManifestPusher( new RegistryEndpointRequestProperties("someServerUrl", "someImageName"), fakeManifestTemplate, "test-image-tag", mockEventHandlers); } @Test public void testGetContent() throws IOException { BlobHttpContent body = testManifestPusher.getContent(); Assert.assertNotNull(body); Assert.assertEquals(V22ManifestTemplate.MANIFEST_MEDIA_TYPE, body.getType()); ByteArrayOutputStream bodyCaptureStream = new ByteArrayOutputStream(); body.writeTo(bodyCaptureStream); String v22manifestJson = new String(Files.readAllBytes(v22manifestJsonFile), StandardCharsets.UTF_8); Assert.assertEquals( v22manifestJson, new String(bodyCaptureStream.toByteArray(), StandardCharsets.UTF_8)); } @Test 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 public void testApiRoute() throws MalformedURLException { Assert.assertEquals( new URL("http://someApiBase/someImageName/manifests/test-image-tag"), testManifestPusher.getApiRoute("http://someApiBase/")); } @Test public void testGetHttpMethod() { Assert.assertEquals("PUT", testManifestPusher.getHttpMethod()); } @Test public void testGetActionDescription() { Assert.assertEquals( "push image manifest for someServerUrl/someImageName:test-image-tag", testManifestPusher.getActionDescription()); } @Test 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); } } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/registry/PlainHttpClient.java ================================================ /* * 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); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAliasGroupTest.java ================================================ /* * 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); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticationFailedExceptionTest.java ================================================ /* * 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()); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java ================================================ /* * 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 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\"", 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\"", 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( 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 { assertThat( RegistryAuthenticator.fromAuthenticationMethod( "Basic", registryEndpointRequestProperties, "user-agent", httpClient)) .isEmpty(); 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(); 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\"", registryEndpointRequestProperties, "user-agent", httpClient); Assert.fail("Authentication method without 'Bearer ' or 'Basic ' should fail"); } catch (RegistryAuthenticationFailedException ex) { Assert.assertEquals( "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()); } } @Test public void testFromAuthenticationMethod_noRealm() { try { 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 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() throws MalformedURLException, RegistryAuthenticationFailedException { RegistryAuthenticator registryAuthenticator = RegistryAuthenticator.fromAuthenticationMethod( "Bearer realm=\"https://somerealm\"", registryEndpointRequestProperties, "user-agent", httpClient) .get(); 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( "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()); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryClientTest.java ================================================ /* * 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.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; /** * Tests for {@link RegistryClient}. More comprehensive tests can be found in the integration tests. */ @RunWith(MockitoJUnitRunner.class) public class RegistryClientTest { @Mock private EventHandlers eventHandlers; private DescriptorDigest digest; private TestWebServer registry; private TestWebServer authServer; @Before 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 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 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 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( "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); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryEndpointCallerTest.java ================================================ /* * 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.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.BlobHttpContent; 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.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 javax.annotation.Nullable; import javax.net.ssl.SSLException; import javax.net.ssl.SSLPeerUnverifiedException; import org.apache.http.NoHttpResponseException; 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; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; /** Tests for {@link RegistryEndpointCaller}. */ @RunWith(MockitoJUnitRunner.class) public class RegistryEndpointCallerTest { /** Implementation of {@link RegistryEndpointProvider} for testing. */ private static class TestRegistryEndpointProvider implements RegistryEndpointProvider { @Override public String getHttpMethod() { return "httpMethod"; } @Override public URL getApiRoute(String apiRouteBase) throws MalformedURLException { return new URL(apiRouteBase + "api"); } @Nullable @Override public BlobHttpContent getContent() { return null; } @Override public List getAccept() { return Collections.emptyList(); } @Nullable @Override public String handleResponse(Response response) throws IOException { 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; } } 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; private RegistryEndpointCaller endpointCaller; @Before public void setUp() throws IOException { endpointCaller = new RegistryEndpointCaller<>( mockEventHandlers, "userAgent", new TestRegistryEndpointProvider(), Authorization.fromBasicCredentials("user", "pass"), new RegistryEndpointRequestProperties("serverUrl", "imageName"), mockHttpClient); Mockito.when(mockResponse.getBody()) .thenReturn(new ByteArrayInputStream("body".getBytes(StandardCharsets.UTF_8))); } @Test public void testCall_secureCallerOnUnverifiableServer() throws IOException, RegistryException { Mockito.when(mockHttpClient.call(Mockito.any(), Mockito.any(), Mockito.any())) .thenThrow(Mockito.mock(SSLPeerUnverifiedException.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 mockNoResponseException = Mockito.mock(NoHttpResponseException.class); setUpRegistryResponse(mockNoResponseException); try { endpointCaller.call(); Assert.fail("Call should have failed"); } catch (NoHttpResponseException ex) { Assert.assertSame(mockNoResponseException, ex); } } @Test 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); } @Test public void testCall_badRequest() throws IOException, RegistryException { verifyThrowsRegistryErrorException(HttpStatusCodes.STATUS_CODE_BAD_REQUEST); } @Test public void testCall_notFound() throws IOException, RegistryException { verifyThrowsRegistryErrorException(HttpStatusCodes.STATUS_CODE_NOT_FOUND); } @Test public void testCall_methodNotAllowed() throws IOException, RegistryException { verifyThrowsRegistryErrorException(HttpStatusCodes.STATUS_CODE_METHOD_NOT_ALLOWED); } @Test public void testCall_unknown() throws IOException, RegistryException { ResponseException responseException = mockResponseException(HttpStatusCodes.STATUS_CODE_SERVER_ERROR); setUpRegistryResponse(responseException); try { endpointCaller.call(); Assert.fail("Call should have failed"); } catch (ResponseException ex) { Assert.assertSame(responseException, ex); } } @Test 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_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_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 testHttpTimeout_propertyNotSet() throws IOException, RegistryException { ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(Request.class); Mockito.when(mockHttpClient.call(Mockito.any(), Mockito.any(), requestCaptor.capture())) .thenReturn(mockResponse); System.clearProperty(JibSystemProperties.HTTP_TIMEOUT); endpointCaller.call(); // We fall back to the default timeout: // https://github.com/GoogleContainerTools/jib/pull/656#discussion_r203562639 Assert.assertEquals(20000, new RequestWrapper(requestCaptor.getValue()).getHttpTimeout()); } @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); 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()); } /** * Verifies that a response with {@code httpStatusCode} throws {@link * RegistryUnauthorizedException}. */ private void verifyThrowsRegistryUnauthorizedException(int httpStatusCode) throws IOException, RegistryException { ResponseException responseException = mockResponseException(httpStatusCode); setUpRegistryResponse(responseException); try { endpointCaller.call(); Assert.fail("Call should have failed"); } catch (RegistryUnauthorizedException ex) { Assert.assertEquals("serverUrl/imageName", ex.getImageReference()); Assert.assertSame(responseException, ex.getCause()); } } /** * Verifies that a response with {@code httpStatusCode} throws {@link * RegistryUnauthorizedException}. */ private void verifyThrowsRegistryErrorException(int httpStatusCode) throws IOException, RegistryException { ResponseException errorResponse = mockResponseException(httpStatusCode); Mockito.when(errorResponse.getContent()) .thenReturn("{\"errors\":[{\"code\":\"code\",\"message\":\"message\"}]}"); setUpRegistryResponse(errorResponse); try { endpointCaller.call(); Assert.fail("Call should have failed"); } catch (RegistryErrorException ex) { MatcherAssert.assertThat( ex.getMessage(), CoreMatchers.startsWith( "Tried to actionDescription but failed because: unknown error code: code (message)")); } } 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); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryErrorExceptionBuilderTest.java ================================================ /* * 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.google.api.client.http.HttpResponseException; import com.google.cloud.tools.jib.registry.json.ErrorEntryTemplate; 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 RegistryErrorExceptionBuilder}. */ @RunWith(MockitoJUnitRunner.class) public class RegistryErrorExceptionBuilderTest { @Mock private HttpResponseException mockHttpResponseException; @Test public void testAddErrorEntry() { RegistryErrorExceptionBuilder builder = 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 error code: unknown (some " + "unknown error happened)", ex.getMessage()); } } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigCredentialRetrieverTest.java ================================================ /* * 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.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; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; /** Tests for {@link DockerConfigCredentialRetriever}. */ @RunWith(MockitoJUnitRunner.class) public class DockerConfigCredentialRetrieverTest { private static final Credential FAKE_CREDENTIAL = Credential.from("username", "password"); @Mock private DockerCredentialHelper mockDockerCredentialHelper; @Mock private DockerConfig mockDockerConfig; @Mock private Consumer mockLogger; private Path dockerConfigFile; @Before public void setUp() 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 { DockerConfigCredentialRetriever dockerConfigCredentialRetriever = DockerConfigCredentialRetriever.create("some registry", Paths.get("fake/path")); Assert.assertFalse(dockerConfigCredentialRetriever.retrieve(mockLogger).isPresent()); } @Test public void testRetrieve_hasAuth() throws IOException { DockerConfigCredentialRetriever dockerConfigCredentialRetriever = 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")); } @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_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 = DockerConfigCredentialRetriever.create("some registry", dockerConfigFile); 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_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 = DockerConfigCredentialRetriever.create("unknown registry", dockerConfigFile); Assert.assertFalse(dockerConfigCredentialRetriever.retrieve(mockLogger).isPresent()); } @Test 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 = DockerConfigCredentialRetriever.create("index.docker.io", 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_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 = DockerConfigCredentialRetriever.create("no auth field", dockerConfigFile); Optional credentials = dockerConfigCredentialRetriever.retrieve(mockLogger); Assert.assertFalse(credentials.isPresent()); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerConfigTest.java ================================================ /* * 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()); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/registry/credentials/DockerCredentialHelperTest.java ================================================ /* * 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); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarExtractorTest.java ================================================ /* * 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"); } } ================================================ FILE: jib-core/src/test/java/com/google/cloud/tools/jib/tar/TarStreamBuilderTest.java ================================================ /* * 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.tar; 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.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; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.junit.Assert; import org.junit.Before; import org.junit.Test; /** Tests for {@link TarStreamBuilder}. */ public class TarStreamBuilderTest { private Path fileA; private Path fileB; private Path directoryA; 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("core/fileA").toURI()); fileB = Paths.get(Resources.getResource("core/fileB").toURI()); directoryA = Paths.get(Resources.getResource("core/directoryA").toURI()); fileAContents = Files.readAllBytes(fileA); fileBContents = Files.readAllBytes(fileB); } @Test public void testToBlob_tarArchiveEntries() throws IOException { setUpWithTarEntries(); verifyBlobWithoutCompression(); } @Test public void testToBlob_strings() throws IOException { setUpWithStrings(); verifyBlobWithoutCompression(); } @Test public void testToBlob_stringsAndTarArchiveEntries() throws IOException { setUpWithStringsAndTarEntries(); verifyBlobWithoutCompression(); } @Test public void testToBlob_tarArchiveEntriesWithCompression() throws IOException { setUpWithTarEntries(); verifyBlobWithCompression(); } @Test public void testToBlob_stringsWithCompression() throws IOException { setUpWithStrings(); verifyBlobWithCompression(); } @Test public void testToBlob_stringsAndTarArchiveEntriesWithCompression() throws IOException { setUpWithStringsAndTarEntries(); 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() throws IOException { // Prepares a test TarStreamBuilder. 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, "some/really/long/path/that/exceeds/100/characters/abcdefghijklmnopqrstuvwxyz0123456789012345678901234567890")); } /** Creates a TarStreamBuilder using Strings. */ private void setUpWithStrings() throws IOException { // Prepares a test TarStreamBuilder. 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", Instant.EPOCH); } /** Creates a TarStreamBuilder using Strings and TarArchiveEntries. */ private void setUpWithStringsAndTarEntries() throws IOException { // Prepares a test TarStreamBuilder. 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", Instant.EPOCH); } /** Creates a compressed blob from the TarStreamBuilder and verifies it. */ private void verifyBlobWithCompression() throws IOException { // 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); verifyTarArchive(tarArchiveInputStream); } /** Creates an uncompressed blob from the TarStreamBuilder and verifies it. */ private void verifyBlobWithoutCompression() throws IOException { // Writes the BLOB and captures the output. ByteArrayOutputStream tarByteOutputStream = new ByteArrayOutputStream(); testTarStreamBuilder.writeAsTarArchiveTo(tarByteOutputStream); // Rearrange the output into input for verification. ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(tarByteOutputStream.toByteArray()); TarArchiveInputStream tarArchiveInputStream = new TarArchiveInputStream(byteArrayInputStream); verifyTarArchive(tarArchiveInputStream); } /** * Helper method to verify that the files were archived correctly by reading {@code * tarArchiveInputStream}. */ private void verifyTarArchive(TarArchiveInputStream tarArchiveInputStream) throws IOException { // Verifies fileA was archived correctly. TarArchiveEntry headerFileA = tarArchiveInputStream.getNextEntry(); Assert.assertEquals("some/path/to/resourceFileA", headerFileA.getName()); byte[] fileAString = ByteStreams.toByteArray(tarArchiveInputStream); Assert.assertArrayEquals(fileAContents, fileAString); // Verifies fileB was archived correctly. TarArchiveEntry headerFileB = tarArchiveInputStream.getNextEntry(); Assert.assertEquals("crepecake", headerFileB.getName()); byte[] fileBString = ByteStreams.toByteArray(tarArchiveInputStream); Assert.assertArrayEquals(fileBContents, fileBString); // Verifies directoryA was archived correctly. TarArchiveEntry headerDirectoryA = tarArchiveInputStream.getNextEntry(); Assert.assertEquals("some/path/to/", headerDirectoryA.getName()); // Verifies the long file was archived correctly. TarArchiveEntry headerFileALong = tarArchiveInputStream.getNextEntry(); Assert.assertEquals( "some/really/long/path/that/exceeds/100/characters/abcdefghijklmnopqrstuvwxyz0123456789012345678901234567890", headerFileALong.getName()); byte[] fileALongString = ByteStreams.toByteArray(tarArchiveInputStream); Assert.assertArrayEquals(fileAContents, fileALongString); Assert.assertNull(tarArchiveInputStream.getNextEntry()); } } ================================================ FILE: jib-core/src/test/resources/META-INF/services/com.google.cloud.tools.jib.api.DockerClient ================================================ com.google.cloud.tools.jib.docker.AnotherDockerClient ================================================ FILE: jib-core/src/test/resources/core/application/dependencies/libraryA.jar ================================================ ================================================ FILE: jib-core/src/test/resources/core/application/dependencies/libraryB.jar ================================================ ================================================ FILE: jib-core/src/test/resources/core/application/dependencies/more/dependency-1.0.0.jar ================================================ ]e$Ềx, .3I݅38VKAM)=5~'qю$[- :&% Eo7Ns`iZ0MT.9J[}?\E }UvJdo(i"Mԛ_+/cI.-O=Hi ================================================ FILE: jib-core/src/test/resources/core/application/resources/resourceA ================================================ ================================================ FILE: jib-core/src/test/resources/core/application/resources/resourceB ================================================ ================================================ FILE: jib-core/src/test/resources/core/application/resources/world ================================================ world ================================================ FILE: jib-core/src/test/resources/core/blobA ================================================ aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz ================================================ FILE: jib-core/src/test/resources/core/class-finder-tests/simple/NotEvenAClass.txt ================================================ Hi there. ================================================ FILE: jib-core/src/test/resources/core/directoryA/.gitkeep ================================================ ================================================ FILE: jib-core/src/test/resources/core/docker/emptyFile ================================================ ================================================ FILE: jib-core/src/test/resources/core/extraction/test-cache/local/config/066872f17ae819f846a6d5abcfc3165abe13fb0a157640fa8cb7af81077670c0 ================================================ {"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"]}} ================================================ FILE: jib-core/src/test/resources/core/fileA ================================================ Crepe cakes are good. ================================================ FILE: jib-core/src/test/resources/core/fileB ================================================ Fast image builds are great. ================================================ FILE: jib-core/src/test/resources/core/json/basic.json ================================================ {"number":54,"text":"crepecake","digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","innerObject":{"number":23,"texts":["first text","second text"],"digests":["sha256:91e0cae00b86c289b33fee303a807ae72dd9f0315c16b74e6ab0cdbe9d996c10","sha256:4945ba5011739b0b98c4a41afe224e417f47c7c99b2ce76830999c9a0861b236"]},"list":[{"number":42,"texts":[]},{"number":99,"texts":["some text"],"digests":["sha256:d38f571aa1c11e3d516e0ef7e513e7308ccbeb869770cb8c4319d63b10a0075e"]}]} ================================================ FILE: jib-core/src/test/resources/core/json/basic_list.json ================================================ [{"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":[]}] ================================================ FILE: jib-core/src/test/resources/core/json/containerconfig.json ================================================ {"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"]}} ================================================ FILE: jib-core/src/test/resources/core/json/dockerconfig.json ================================================ { "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" } } ================================================ FILE: jib-core/src/test/resources/core/json/dockerconfig_extra_matches.json ================================================ { "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"} } } ================================================ FILE: jib-core/src/test/resources/core/json/dockerconfig_identity_token.json ================================================ { "auths":{ "some registry":{ "auth":"MDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDAwMDAwOg==", "identitytoken":"cool identity token" } } } ================================================ FILE: jib-core/src/test/resources/core/json/dockerconfig_index_docker_io_v1.json ================================================ { "auths":{ "index.docker.io/v1/":{"auth":"dG9rZW4gZm9yOiBpbmRleC5kb2NrZXIuaW8vdjEv"} } } ================================================ FILE: jib-core/src/test/resources/core/json/legacy_dockercfg ================================================ { "some registry":{"auth":"c29tZTpvdGhlcjphdXRo"}, "https://example.com/v2/":{"auth":"dXNlcjpwYXNz"} } ================================================ FILE: jib-core/src/test/resources/core/json/loadmanifest.json ================================================ [{"Config":"config.json","RepoTags":["testregistry/testrepo:testtag"],"Layers":["layer1.tar.gz","layer2.tar.gz","layer3.tar.gz"]}] ================================================ FILE: jib-core/src/test/resources/core/json/loadmanifest2.json ================================================ [{"Config":"config.json","RepoTags":["testregistry/testrepo:testtag"],"Layers":["layer1.tar.gz","layer2.tar.gz","layer3.tar.gz"],"LayerSources":{}}] ================================================ FILE: jib-core/src/test/resources/core/json/metadata-v2.json ================================================ {"layers":[{"reference":{"size":631,"digest":"sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef","diffId":"sha256:b56ae66c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a4647"}},{"reference":{"size":223,"digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","diffId":"sha256:a3f3e99c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a8372"},"properties":{"layerEntries":[{"sourceFiles":["some/source/path"],"extractionPath":"some/extraction/path"}],"lastModifiedTime":255073580723571}}]} ================================================ FILE: jib-core/src/test/resources/core/json/metadata-v3.json ================================================ {"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}}]} ================================================ FILE: jib-core/src/test/resources/core/json/metadata.json ================================================ {"layers":[{"reference":{"size":631,"digest":"sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef","diffId":"sha256:b56ae66c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a4647"}},{"reference":{"size":223,"digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","diffId":"sha256:a3f3e99c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a8372"},"properties":{"sourceFiles":["some/source/path"],"lastModifiedTime":255073580723571}}]} ================================================ FILE: jib-core/src/test/resources/core/json/metadata_corrupted.json ================================================ {"layers":[{"reference":{"size":223,"digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","diffId":"sha256:a3f3e99c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a8372"}}]} ================================================ FILE: jib-core/src/test/resources/core/json/metadata_windows-v2.json ================================================ {"layers":[{"reference":{"size":631,"digest":"sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef","diffId":"sha256:b56ae66c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a4647"}},{"reference":{"size":223,"digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","diffId":"sha256:a3f3e99c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a8372"},"properties":{"layerEntries":[{"sourceFiles":["some\\source\\path"],"extractionPath":"some/extraction/path"}],"lastModifiedTime":255073580723571}}]} ================================================ FILE: jib-core/src/test/resources/core/json/metadata_windows.json ================================================ {"layers":[{"reference":{"size":631,"digest":"sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef","diffId":"sha256:b56ae66c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a4647"}},{"reference":{"size":223,"digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","diffId":"sha256:a3f3e99c29370df48e7377c8f9baa744a3958058a766793f821dadcb144a8372"},"properties":{"sourceFiles":["some\\source\\path"],"lastModifiedTime":255073580723571}}]} ================================================ FILE: jib-core/src/test/resources/core/json/ociindex.json ================================================ { "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" } } ] } ================================================ FILE: jib-core/src/test/resources/core/json/ociindex_platforms.json ================================================ { "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" } } ] } ================================================ FILE: jib-core/src/test/resources/core/json/ocimanifest.json ================================================ {"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","size":1000},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:4945ba5011739b0b98c4a41afe224e417f47c7c99b2ce76830999c9a0861b236","size":1000000}]} ================================================ FILE: jib-core/src/test/resources/core/json/translated_ocimanifest.json ================================================ {"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}]} ================================================ FILE: jib-core/src/test/resources/core/json/translated_v22manifest.json ================================================ {"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}]} ================================================ FILE: jib-core/src/test/resources/core/json/v21manifest.json ================================================ { "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"} ] } ================================================ FILE: jib-core/src/test/resources/core/json/v22manifest.json ================================================ {"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:8c662931926fa990b41da3c9f42663a537ccd498130030f9149173a0493832ad","size":1000},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:4945ba5011739b0b98c4a41afe224e417f47c7c99b2ce76830999c9a0861b236","size":1000000}]} ================================================ FILE: jib-core/src/test/resources/core/json/v22manifest_list.json ================================================ { "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" ] } }, { "mediaType": "application/vnd.docker.image.manifest.v2+json", "size": 7682, "digest": "sha256:cccbcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501999", "platform": { "architecture": "amd64", "os": "linux", "os.version": "potato-9" } } ] } ================================================ FILE: jib-core/src/test/resources/core/json/v22manifest_optional_properties.json ================================================ { "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"} } ] } ================================================ FILE: jib-core/src/test/resources/core/layer/a/b/bar ================================================ bar ================================================ FILE: jib-core/src/test/resources/core/layer/c/cat ================================================ cat ================================================ FILE: jib-core/src/test/resources/core/layer/foo ================================================ foo ================================================ FILE: jib-core/src/test/resources/core/random-contents/file1 ================================================ ================================================ FILE: jib-core/src/test/resources/core/random-contents/file2 ================================================ ================================================ FILE: jib-core/src/test/resources/core/random-contents/sub-directory/file3 ================================================ ================================================ FILE: jib-core/src/test/resources/core/random-contents/sub-directory/file4 ================================================ ================================================ FILE: jib-core/src/test/resources/core/random-contents/sub-directory/leaf/file5 ================================================ ================================================ FILE: jib-core/src/test/resources/core/random-contents/sub-directory/leaf/file6 ================================================ ================================================ FILE: jib-core/src/test/resources/core/webAppSampleDockerfile ================================================ FROM tomcat:8.5-jre8-alpine COPY libs / COPY snapshot-libs / COPY resources / COPY classes / COPY root / ENTRYPOINT ["catalina.sh","run"] ================================================ FILE: jib-core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker ================================================ mock-maker-inline ================================================ FILE: jib-gradle-plugin/CHANGELOG.md ================================================ # Change Log All notable changes to this project will be documented in this file. ## [unreleased] ### Added ### Changed ### Fixed ## 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)) ## 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)) ## 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)) ## 0.9.2 ### Added - 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)) ## 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)) ### 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)) ### 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)) ## 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)) ### Changed - 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)) ## 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)) ================================================ FILE: jib-gradle-plugin/README.md ================================================ ![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 a [Gradle](https://gradle.org/) plugin for building Docker and [OCI](https://github.com/opencontainers/image-spec) images for your Java applications. For the Maven plugin, see the [jib-maven-plugin project](../jib-maven-plugin). For information about the project, see the [Jib project README](../README.md). | ☑️ 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 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 '3.5.3' } ``` *See the [Gradle Plugin Portal](https://plugins.gradle.org/plugin/com.google.cloud.tools.jib) for more details.* You can containerize your application easily with one command: ```shell gradle jib --image= ``` This builds and pushes a container image for your application to a container registry. *If you encounter authentication issues, see [Authentication Methods](#authentication-methods).* To build to a Docker daemon, use: ```shell gradle jibDockerBuild ``` If you would like to set up Jib as part of your Gradle build, follow the guide below. ## Configuration Configure the plugin by setting the image to push to: #### Using [Google Container Registry (GCR)](https://cloud.google.com/container-registry/)... *Make sure you have the [`docker-credential-gcr` command line tool](https://cloud.google.com/container-registry/docs/advanced-authentication#docker_credential_helper). Jib automatically uses `docker-credential-gcr` for obtaining credentials. See [Authentication Methods](#authentication-methods) for other ways of authenticating.* For example, to build the image `gcr.io/my-gcp-project/my-app`, the configuration would be: ```groovy jib.to.image = 'gcr.io/my-gcp-project/my-app' ``` #### Using [Amazon Elastic Container Registry (ECR)](https://aws.amazon.com/ecr/)... *Make sure you have the [`docker-credential-ecr-login` command line tool](https://github.com/awslabs/amazon-ecr-credential-helper). Jib automatically uses `docker-credential-ecr-login` for obtaining credentials. See [Authentication Methods](#authentication-methods) for other ways of authenticating.* For example, to build the image `aws_account_id.dkr.ecr.region.amazonaws.com/my-app`, the configuration would be: ```groovy jib.to.image = 'aws_account_id.dkr.ecr.region.amazonaws.com/my-app' ``` #### Using [Docker Hub Registry](https://hub.docker.com/)... *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-docker-id/my-app`, the configuration would be: ```groovy jib.to.image = '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: ```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 Build your container image with: ```shell gradle jib ``` 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).* #### Build to Docker daemon Jib can also build your image directly to a Docker daemon. This uses the `docker` command line tool and requires that you have `docker` available on your `PATH`. ```shell 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://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: ```groovy tasks.build.dependsOn tasks.jib ``` Then, ```gradle build``` will build and containerize your application. ### Additional Build Artifacts 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. ## Multi Module Projects 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 The plugin provides the `jib` extension for configuration with the following options for customizing the image build: Field | Type | Default | Description --- | --- | --- | --- `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. `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` | `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. 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)): 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 --- | --- | --- | --- `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)* #### 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`, `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 jib { from { image = 'openjdk:alpine' } to { image = 'localhost:5000/my-image/built-with-jib' credHelper = 'osxkeychain' tags = ['tag2', 'latest'] } container { 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. #### 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 Docker credential helpers are CLI tools that handle authentication with various registries. 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:* ```groovy jib { from { image = 'aws_account_id.dkr.ecr.region.amazonaws.com/my-base-image' credHelper = 'ecr-login' } to { image = 'gcr.io/my-gcp-project/my-app' credHelper = 'gcr' } } ``` #### Using Specific Credentials You can specify credentials directly in the extension for the `from` and/or `to` images. ```groovy jib { from { image = 'aws_account_id.dkr.ecr.region.amazonaws.com/my-base-image' auth { username = USERNAME // Defined in 'gradle.properties'. password = PASSWORD } } to { image = 'gcr.io/my-gcp-project/my-app' auth { username = 'oauth2accesstoken' password = 'gcloud auth print-access-token'.execute().text.trim() } } } ``` 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)): ```groovy jib { to { image = 'gcr.io/my-gcp-project/my-app' auth { username = '_json_key' password = file('keyfile.json').text } } } ``` ### Custom Container Entrypoint 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}`.) 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. ================================================ FILE: jib-gradle-plugin/build.gradle ================================================ plugins { 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() } } task install { dependsOn publishToMavenLocal } } /* LOCAL SNAPSHOT DEV */ dependencies { sourceProject project(':jib-core') sourceProject project(':jib-plugins-common') ensureNoProjectDependencies() implementation dependencyStrings.GRADLE_EXTENSION testImplementation dependencyStrings.JUNIT testImplementation dependencyStrings.TRUTH testImplementation dependencyStrings.TRUTH8 testImplementation dependencyStrings.MOCKITO_CORE testImplementation dependencyStrings.SLF4J_API testImplementation dependencyStrings.SYSTEM_RULES testImplementation project(path:':jib-plugins-common', configuration:'tests') integrationTestImplementation project(path:':jib-core', configuration:'integrationTests') integrationTestImplementation dependencyStrings.JBCRYPT // 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' } /* RELEASE */ // Prepare release release { 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/' 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 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 */ ================================================ FILE: jib-gradle-plugin/gradle.properties ================================================ version = 3.5.4-SNAPSHOT ================================================ FILE: jib-gradle-plugin/scripts/release.sh ================================================ #!/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 ================================================ FILE: jib-gradle-plugin/scripts/update_gcs_latest.sh ================================================ #!/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 gsutil cp jib-gradle $destination gsutil acl ch -u allUsers:READ $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 ================================================ FILE: jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/DefaultTargetProjectIntegrationTest.java ================================================ /* * 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"); } } ================================================ FILE: jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/EmptyProjectIntegrationTest.java ================================================ /* * 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()); } } ================================================ FILE: jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/GradleLayerConfigurationIntegrationTest.java ================================================ /* * 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; } } ================================================ FILE: jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/JibRunHelper.java ================================================ /* * 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(); } } ================================================ FILE: jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SingleProjectIntegrationTest.java ================================================ /* * 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"); } } ================================================ FILE: jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/SpringBootProjectIntegrationTest.java ================================================ /* * 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(); } } ================================================ FILE: jib-gradle-plugin/src/integration-test/java/com/google/cloud/tools/jib/gradle/WarProjectIntegrationTest.java ================================================ /* * 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")); } } ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/default-target/build.gradle ================================================ 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'] } } ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/default-target/gradle.properties ================================================ version = default-target-version ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/default-target/settings.gradle ================================================ rootProject.name = 'default-target-name' ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/default-target/src/main/java/com/test/HelloWorld.java ================================================ /* * 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.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; /** 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'. ClassLoader classLoader = HelloWorld.class.getClassLoader(); 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] : "")); } } ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/default-target/src/main/resources/world ================================================ world ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/empty/build-broken-user.gradle ================================================ 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' } } ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/empty/build.gradle ================================================ 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')] } ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/empty/src/main/java/com/test/Empty.java ================================================ package com.test; public class Empty { public static void main(String[] args) {} } ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/multiproject/a_packaged/build.gradle ================================================ 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'] } } ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/multiproject/a_packaged/src/main/java/com/test/Empty.java ================================================ package com.test; public class Empty { public static void main(String[] args) {} } ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/multiproject/b_dependency/build.gradle ================================================ apply plugin: 'java-library' ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/multiproject/b_dependency/src/main/java/com/test/Empty.java ================================================ package com.test; public class Empty { public static void main(String[] args) {} } ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/multiproject/build.gradle ================================================ apply plugin: 'base' ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/multiproject/settings.gradle ================================================ include ':a_packaged' include ':b_dependency' ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-configuration.gradle ================================================ 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') } ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-cred-helper.gradle ================================================ 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' ] } } } ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-dockerclient.gradle ================================================ 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'] } } ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dirs-filtering.gradle ================================================ 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'] } } } } ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dirs.gradle ================================================ 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'] } ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dirs2.gradle ================================================ 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'] } } ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-extra-dirs3.gradle ================================================ 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' } } } } ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-jar-containerization.gradle ================================================ 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' } ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-java11-incompatible.gradle ================================================ 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") ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-java11.gradle ================================================ 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") ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-java17.gradle ================================================ 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") ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-local-base.gradle ================================================ 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') } ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-multi-platform.gradle ================================================ 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') } } ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build-timestamps-custom.gradle ================================================ 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' } } ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/build.gradle ================================================ 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') } ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/complex-build.gradle ================================================ 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 } ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/libs/dependency2 ================================================ ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/libs/dependency3 ================================================ ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/mock-docker.sh ================================================ #!/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" ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/custom-extra-dir/bar/cat ================================================ cat ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/custom-extra-dir/foo ================================================ foo ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/custom-extra-dir2/baz ================================================ baz ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/custom-extra-dir3/cat.json ================================================ ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/custom-extra-dir3/cat.txt ================================================ ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/custom-extra-dir3/sub/a.json ================================================ ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/custom-extra-dir3/sub/a.txt ================================================ ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/custom-extra-dir4/bar ================================================ ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/custom-extra-dir4/foo ================================================ ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/java/com/test/HelloWorld.java ================================================ /* * 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()); } } } } ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/simple/src/main/resources/world ================================================ world ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/spring-boot/build.gradle ================================================ 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' } ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/spring-boot/settings.gradle ================================================ pluginManagement { repositories { mavenCentral() gradlePluginPortal() } } rootProject.name = 'spring-boot' ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/spring-boot/src/main/java/hello/Application.java ================================================ 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); } } ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/spring-boot/src/main/java/hello/HelloController.java ================================================ 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"; } } ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/build-tomcat.gradle ================================================ 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' } ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/build.gradle ================================================ 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' } } ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/extra_js/bogus.js ================================================ // nothing inside ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/extra_static/bogus.html ================================================ nothing inside ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/main/java/example/HelloWorld.java ================================================ /* * 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); } } } ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/main/resources/world ================================================ world ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/main/webapp/META-INF/MANIFEST.MF ================================================ Manifest-Version: 1.0 Class-Path: ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/main/webapp/WEB-INF/web.xml ================================================ HelloWorld example.HelloWorld HelloWorld /hello ================================================ FILE: jib-gradle-plugin/src/integration-test/resources/gradle/projects/war_servlet25/src/main/webapp/index.html ================================================ Hello World

Hello World!

Available Servlets:
The HelloWorld servlet
================================================ FILE: jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/AuthParameters.java ================================================ /* * 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"; } } ================================================ FILE: jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BaseImageParameters.java ================================================ /* * 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); } } ================================================ FILE: jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java ================================================ /* * 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.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 java.io.IOException; import java.nio.file.Path; import java.util.Optional; import java.util.concurrent.Future; import javax.annotation.Nullable; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; import org.gradle.api.tasks.Nested; import org.gradle.api.tasks.TaskAction; import org.gradle.api.tasks.options.Option; /** Builds a container image and exports to the default Docker daemon. */ public class BuildDockerTask extends DefaultTask implements JibTask { private static final String HELPFUL_SUGGESTIONS_PREFIX = "Build to Docker daemon 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); } /** * 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 IOException, BuildStepsExecutionException, CacheDirectoryCreationException, MainClassInferenceException, InvalidGlobalConfigException { Preconditions.checkNotNull(jibExtension); // 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)); } 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.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(); } } @Override public BuildDockerTask setJibExtension(JibExtension jibExtension) { this.jibExtension = jibExtension; return this; } } ================================================ FILE: jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java ================================================ /* * 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 com.google.common.base.Strings; import java.io.IOException; import java.util.Optional; import java.util.concurrent.Future; import javax.annotation.Nullable; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; import org.gradle.api.tasks.Nested; import org.gradle.api.tasks.TaskAction; import org.gradle.api.tasks.options.Option; /** Builds a container image to registry. */ public class BuildImageTask extends DefaultTask implements JibTask { private static final String HELPFUL_SUGGESTIONS_PREFIX = "Build image 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); } /** * 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 IOException, BuildStepsExecutionException, 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 { 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(); } 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 BuildImageTask setJibExtension(JibExtension jibExtension) { this.jibExtension = jibExtension; return this; } } ================================================ FILE: jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildTarTask.java ================================================ /* * 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; } } ================================================ FILE: jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ContainerParameters.java ================================================ /* * 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.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; /** * A bean that configures properties of the container run from the image. This is configurable with * Groovy closures and can be validated when used as a task input. */ public class ContainerParameters { 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() { 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.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() { String mainClassProperty = System.getProperty(PropertyNames.CONTAINER_MAIN_CLASS); if (mainClassProperty != null) { return mainClassProperty; } return mainClass.getOrNull(); } public void setMainClass(String 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; } public void setArgs(List args) { this.args = args; } @Input @Optional 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; } } ================================================ FILE: jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/CredHelperParameters.java ================================================ /* * 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); } } ================================================ FILE: jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerClientParameters.java ================================================ /* * 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; } } ================================================ FILE: jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtensionParameters.java ================================================ /* * 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; } } ================================================ FILE: jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtensionParametersSpec.java ================================================ /* * 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); } } ================================================ FILE: jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtraDirectoriesParameters.java ================================================ /* * 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; } } ================================================ FILE: jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtraDirectoryParameters.java ================================================ /* * 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(); } } ================================================ FILE: jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ExtraDirectoryParametersSpec.java ================================================ /* * 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); } } ================================================ FILE: jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleHelpfulSuggestions.java ================================================ /* * 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"); } } ================================================ FILE: jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleProjectProperties.java ================================================ /* * 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.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. */ 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"; /** Name of the `main` {@link SourceSet} to use as source files. */ private static final String MAIN_SOURCE_SET_NAME = "main"; 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 SingleThreadedExecutor singleThreadedExecutor = new SingleThreadedExecutor(); private final ConsoleLogger consoleLogger; private final TempDirectoryProvider tempDirectoryProvider; private final Supplier>> extensionLoader; private final String configurationName; @VisibleForTesting GradleProjectProperties( Project project, Logger logger, TempDirectoryProvider tempDirectoryProvider, Supplier>> extensionLoader, String configurationName) { this.project = project; 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 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 void log(LogEvent logEvent) { consoleLogger.log(logEvent.getLevel(), logEvent.getMessage()); } @Override public String getToolName() { return TOOL_NAME; } @Override public String getToolVersion() { return TOOL_VERSION; } @Override public String getPluginName() { return PLUGIN_NAME; } @Nullable @Override 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.valueOf(value); } @Override public Path getDefaultCacheDirectory() { return project.getBuildDir().toPath().resolve(CACHE_DIRECTORY_NAME); } @Override public String getJarPluginName() { return JAR_PLUGIN_NAME; } @Override public boolean isWarProject() { return project.getPlugins().hasPlugin(WarPlugin.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. * * @param project the gradle project * @param extraDirectories the image's configured extra directories * @return the input files */ @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 { 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); } } ================================================ FILE: jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/GradleRawConfiguration.java ================================================ /* * 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(); } } ================================================ FILE: jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibExtension.java ================================================ /* * 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.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.Nested; import org.gradle.api.tasks.Optional; /** * Plugin extension for {@link JibPlugin}. * *

Example configuration: * *

{@code
 * jib {
 *   from {
 *     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'
 *   }
 *   container {
 *     jvmFlags = ['-Xms512m', '-Xdebug']
 *     mainClass = 'com.mycompany.myproject.Main'
 *     args = ['arg1', 'arg2']
 *     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]
 *     }
 *   }
 * }
 * }
*/ public class JibExtension { // Defines default configuration values. private static final boolean DEFAULT_ALLOW_INSECURE_REGISTIRIES = false; private static final String DEFAULT_CONTAINERIZING_MODE = "exploded"; private final BaseImageParameters from; private final TargetImageParameters to; private final ContainerParameters container; private final ExtraDirectoriesParameters extraDirectories; private final DockerClientParameters dockerClient; private final OutputPathsParameters outputPaths; private final SkaffoldParameters skaffold; private final Property allowInsecureRegistries; private final Property containerizingMode; private final Property configurationName; private final ListProperty pluginExtensions; private final ExtensionParametersSpec extensionParametersSpec; /** * Should be called using {@link org.gradle.api.plugins.ExtensionContainer#create}. * * @param project the injected gradle project */ public JibExtension(Project project) { ObjectFactory objectFactory = project.getObjects(); 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) { action.execute(to); } public void container(Action action) { action.execute(container); } public void extraDirectories(Action action) { action.execute(extraDirectories); } public void dockerClient(Action action) { action.execute(dockerClient); } public void outputPaths(Action action) { action.execute(outputPaths); } public void skaffold(Action action) { action.execute(skaffold); } public void pluginExtensions(Action action) { action.execute(extensionParametersSpec); } public void setAllowInsecureRegistries(boolean allowInsecureRegistries) { this.allowInsecureRegistries.set(allowInsecureRegistries); } public void setContainerizingMode(String containerizingMode) { this.containerizingMode.set(containerizingMode); } @Nested @Optional public BaseImageParameters getFrom() { return from; } @Nested @Optional public TargetImageParameters getTo() { return to; } @Nested @Optional public ContainerParameters getContainer() { return container; } @Nested @Optional public ExtraDirectoriesParameters getExtraDirectories() { return extraDirectories; } @Nested @Optional public DockerClientParameters getDockerClient() { return dockerClient; } @Nested @Optional public OutputPathsParameters getOutputPaths() { return outputPaths; } @Nested @Optional public SkaffoldParameters getSkaffold() { return skaffold; } @Input boolean getAllowInsecureRegistries() { if (System.getProperty(PropertyNames.ALLOW_INSECURE_REGISTRIES) != null) { return Boolean.getBoolean(PropertyNames.ALLOW_INSECURE_REGISTRIES); } return allowInsecureRegistries.get(); } @Input @Optional 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 public Property getConfigurationName() { String property = System.getProperty(PropertyNames.CONFIGURATION_NAME); if (property != null && !property.equals(configurationName.get())) { configurationName.set(property); } return configurationName; } @Nested @Optional public ListProperty getPluginExtensions() { return pluginExtensions; } } ================================================ FILE: jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibPlugin.java ================================================ /* * 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.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.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("5.1"); 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"; public static final String REQUIRED_VERSION_PROPERTY_NAME = "jib.requiredVersion"; private static void checkGradleVersion() { if (GRADLE_MIN_VERSION.compareTo(GradleVersion.current()) > 0) { throw new GradleException( "Detected " + GradleVersion.current() + ", but jib requires " + GRADLE_MIN_VERSION + " or higher. You can upgrade by running 'gradle wrapper --gradle-version=" + GRADLE_MIN_VERSION.getVersion() + "'."); } } /** 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)))); }); } } ================================================ FILE: jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/JibTask.java ================================================ /* * 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 org.gradle.api.Task; /** A task with a {@link JibExtension}. */ public interface JibTask extends Task { /** * Sets the task's {@link JibExtension}. * * @param jibExtension the {@link JibExtension} * @return this */ Task setJibExtension(JibExtension jibExtension); } ================================================ FILE: jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/OutputPathsParameters.java ================================================ /* * 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); } } ================================================ FILE: jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PlatformParameters.java ================================================ /* * 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); } } ================================================ FILE: jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/PlatformParametersSpec.java ================================================ /* * 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); } } ================================================ FILE: jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TargetImageParameters.java ================================================ /* * 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); } } ================================================ FILE: jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/TaskCommon.java ================================================ /* * 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() {} } ================================================ FILE: jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/CheckJibVersionTask.java ================================================ /* * 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 } } ================================================ FILE: jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskV2.java ================================================ /* * 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); } } } ================================================ FILE: jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/InitTask.java ================================================ /* * 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()); } } ================================================ FILE: jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/SkaffoldParameters.java ================================================ /* * 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; } } ================================================ FILE: jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/SkaffoldSyncParameters.java ================================================ /* * 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()); } } ================================================ FILE: jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/SkaffoldWatchParameters.java ================================================ /* * 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()); } } ================================================ FILE: jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/skaffold/SyncMapTask.java ================================================ /* * 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); } } } } ================================================ FILE: jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesExtensionTest.java ================================================ /* * 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()); } } } ================================================ FILE: jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleProjectPropertiesTest.java ================================================ /* * 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.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.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.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.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.junit.MockitoJUnitRunner; /** Test for {@link GradleProjectProperties}. */ @RunWith(MockitoJUnitRunner.class) public class GradleProjectPropertiesTest { 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 GradleProjectProperties gradleProjectProperties; private Project project; @Before 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( project, mockLogger, mockTempDirectoryProvider, mockExtensionLoader, JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME); } @Test public void testGetMainClassFromJar_success() { 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() { 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 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; } } ================================================ FILE: jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/GradleRawConfigurationTest.java ================================================ /* * 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()); } } ================================================ FILE: jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibExtensionTest.java ================================================ /* * 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 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.Before; import org.junit.Rule; import org.junit.Test; import org.junit.contrib.java.lang.system.RestoreSystemProperties; /** Tests for {@link JibExtension}. */ public class JibExtensionTest { @Rule public final RestoreSystemProperties systemPropertyRestorer = new RestoreSystemProperties(); private JibExtension testJibExtension; private Project fakeProject; @Before public void setUp() { fakeProject = ProjectBuilder.builder().build(); testJibExtension = fakeProject .getExtensions() .create(JibPlugin.JIB_EXTENSION_NAME, JibExtension.class, fakeProject); } @Test public void testFrom() { 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")); 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")); }); }); assertThat(testJibExtension.getFrom().getCredHelper().getHelper()) .isEqualTo("some cred helper"); assertThat(testJibExtension.getFrom().getCredHelper().getEnvironment()) .isEqualTo(Collections.singletonMap("ENV_VARIABLE", "Value")); } @Test public void testTo() { 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")); 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")); }); }); 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() { 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"); }); 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 testSetFormat() { testJibExtension.container( container -> { container.setFormat("OCI"); }); ContainerParameters container = testJibExtension.getContainer(); assertThat(container.getFormat()).isSameInstanceAs(ImageFormat.OCI); } @Test public void testContainerizingMode() { assertThat(testJibExtension.getContainerizingMode()).isEqualTo("exploded"); } @Test public void testConfigurationName() { assertThat(testJibExtension.getConfigurationName().get()).isEqualTo("runtimeClasspath"); } @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(); } @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"); } private TargetImageParameters generateTargetImageParametersWithTags(String... tags) { HashSet set = new HashSet<>(Arrays.asList(tags)); testJibExtension.to(to -> to.setTags(set)); return testJibExtension.getTo(); } } ================================================ FILE: jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/JibPluginTest.java ================================================ /* * 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 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.After; import org.junit.Assume; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; /** Tests for {@link JibPlugin}. */ public class JibPluginTest { 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() 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 { 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; } } ================================================ FILE: jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TaskCommonTest.java ================================================ /* * 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")); } } ================================================ FILE: jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/TestProject.java ================================================ /* * 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.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.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. */ public class TestProject extends TemporaryFolder implements Closeable { 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()); 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 String gradleVersion = JibPlugin.GRADLE_MIN_VERSION.getVersion(); private GradleRunner gradleRunner; 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); gradleRunner = GradleRunner.create() .withGradleVersion(gradleVersion) .withProjectDir(projectRoot.toFile()) .withPluginClasspath(); } 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(); } public Path getProjectRoot() { return projectRoot; } } ================================================ FILE: jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/skaffold/FilesTaskV2Test.java ================================================ /* * 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()); } } ================================================ FILE: jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/skaffold/InitTaskTest.java ================================================ /* * 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()); } } ================================================ FILE: jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/skaffold/SyncMapTaskTest.java ================================================ /* * 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'")); } } } ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/application/build/resources/main/resourceA ================================================ ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/application/build/resources/main/resourceB ================================================ ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/application/build/resources/main/world ================================================ world ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/application/dependencies/library.jarC.jar ================================================ ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/application/dependencies/libraryA.jar ================================================ ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/application/dependencies/libraryB.jar ================================================ ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/application/dependencies/more/dependency-1.0.0.jar ================================================ ]e$Ềx, .3I݅38VKAM)=5~'qю$[- :&% Eo7Ns`iZ0MT.9J[}?\E }UvJdo(i"Mԛ_+/cI.-O=Hi ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/application/extra-directory/foo ================================================ Unused file for committing extra directory ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/plugin-test/build.gradle ================================================ plugins { id 'java' id 'com.google.cloud.tools.jib' } ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/build.gradle ================================================ // this file doesn't do anything ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/build.gradle ================================================ 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' } ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/src/main/extra-resources-1/resource1.txt ================================================ ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/src/main/extra-resources-2/resource2.txt ================================================ ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/src/main/java/com/test/HelloWorld.java ================================================ /* * 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"); } } ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/complex-service/src/main/other-jib/extra-file ================================================ ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/gradle.properties ================================================ ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/lib/build.gradle ================================================ plugins { id 'java' } sourceCompatibility = 1.8 targetCompatibility = 1.8 repositories { mavenCentral() } ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/lib/src/main/java/com/lib/Lib.java ================================================ /* * 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"; } } ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/lib/src/main/resources/hi.txt ================================================ hi ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/settings.gradle ================================================ include ':simple-service' include ':complex-service' include ':lib' ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/simple-service/build.gradle ================================================ plugins { id 'java' id 'com.google.cloud.tools.jib' } sourceCompatibility = 1.8 targetCompatibility = 1.8 repositories { mavenCentral() } jib { from { image = 'scratch' } // only use buildTar } ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/all-local-multi-service/simple-service/src/main/java/com/test/HelloWorld.java ================================================ /* * 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"); } } ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/lazy-evaluation/build-extra-dirs.gradle ================================================ 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) } ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/lazy-evaluation/build.gradle ================================================ 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) } ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/lazy-evaluation/src/main/java/com/test/HelloWorld.java ================================================ /* * 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"); } } ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/lazy-evaluation/src/main/updated-custom-extra-dir/foo ================================================ Unused file for committing parent directory so that it exists for extraDirectories tests ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/build.gradle ================================================ plugins { id 'java' id 'com.google.cloud.tools.jib' } sourceCompatibility = 1.8 targetCompatibility = 1.8 repositories { mavenCentral() } jib { to { image = System.getProperty('_TARGET_IMAGE') } } ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/build.gradle ================================================ 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' } ================================================ FILE: 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 ================================================ 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 ================================================ FILE: 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 ================================================ 4.0.0 com.google.cloud.tools tiny-test-lib 0.0.1-SNAPSHOT POM was created from install:install-file ================================================ FILE: 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 ================================================ com.google.cloud.tools tiny-test-lib 0.0.1-SNAPSHOT 20190911205316 ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/src/main/extra-resources-1/resource1.txt ================================================ ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/src/main/extra-resources-2/resource2.txt ================================================ ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/src/main/java/com/test/HelloWorld.java ================================================ /* * 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"); } } ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/complex-service/src/main/other-jib/extra-file ================================================ ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/gradle.properties ================================================ ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/lib/build.gradle ================================================ plugins { id 'java' } sourceCompatibility = 1.8 targetCompatibility = 1.8 repositories { mavenCentral() } ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/lib/src/main/java/com/lib/Lib.java ================================================ /* * 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"; } } ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/lib/src/main/resources/hi.txt ================================================ hi ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/lib/src/test/java/com/lib/LibTest.java ================================================ /* * 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()); } } ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/settings.gradle ================================================ include ':simple-service' include ':complex-service' include ':lib' ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/simple-service/build.gradle ================================================ plugins { id 'java' id 'com.google.cloud.tools.jib' } sourceCompatibility = 1.8 targetCompatibility = 1.8 repositories { mavenCentral() } jib { to { image = System.getProperty('_TARGET_IMAGE') } } ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/multi-service/simple-service/src/main/java/com/test/HelloWorld.java ================================================ /* * 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"); } } ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/platform/build.gradle ================================================ subprojects { repositories { mavenCentral() } } ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/platform/platform/build.gradle ================================================ plugins { id 'java-platform' } dependencies { constraints { api 'org.apache.commons:commons-io:1.3.2' } } ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/platform/service/build.gradle ================================================ 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') } } ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/platform/service/src/main/java/com/test/HelloWorld.java ================================================ /* * 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"); } } ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/platform/settings.gradle ================================================ include ':platform' include ':service' ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/simple/build.gradle ================================================ 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') } } ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/simple/src/main/custom-extra-dir/bar/cat ================================================ cat ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/simple/src/main/custom-extra-dir/foo ================================================ foo ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/simple/src/main/java/com/test/HelloWorld.java ================================================ /* * 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.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; /** 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'. ClassLoader classLoader = HelloWorld.class.getClassLoader(); Path worldFile = Paths.get(classLoader.getResource("world").toURI()); String world = new String(Files.readAllBytes(worldFile), StandardCharsets.UTF_8); System.out.println(greeting + ", " + world + ". "); } } ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/simple/src/main/resources/world ================================================ world ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/skaffold-config/build.gradle ================================================ 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'] } } } ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/skaffold-config/other/file.txt ================================================ ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/skaffold-config/script.gradle ================================================ ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/skaffold-config/src/main/java/com/test/HelloWorld.java ================================================ /* * 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"); } } ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/skaffold-config/src/main/java/com/test2/GoodbyeWorld.java ================================================ /* * 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!"); } } ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/skaffold-config/src/main/jib/bar/cat ================================================ cat ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/skaffold-config/src/main/jib/foo ================================================ foo ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/skaffold-config/src/main/resources/world ================================================ world ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/build.gradle ================================================ 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') } } ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/pom-tomcat.xml ================================================ 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 ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/pom.xml ================================================ 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} ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/extra_js/bogus.js ================================================ // nothing inside ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/extra_static/bogus.html ================================================ nothing inside ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/java/example/HelloWorld.java ================================================ /* * 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); } } } ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/resources/world ================================================ world ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/webapp/META-INF/MANIFEST.MF ================================================ Manifest-Version: 1.0 Class-Path: ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/webapp/WEB-INF/web.xml ================================================ HelloWorld example.HelloWorld HelloWorld /hello ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/projects/war_servlet25/src/main/webapp/index.html ================================================ Hello World

Hello World!

Available Servlets:
The HelloWorld servlet
================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/webapp/META-INF/context.xml ================================================ ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/webapp/Test.jsp ================================================ ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/webapp/WEB-INF/classes/package/test.properties ================================================ ================================================ FILE: jib-gradle-plugin/src/test/resources/gradle/webapp/WEB-INF/web.xml ================================================ ================================================ FILE: jib-gradle-plugin-extension-api/CHANGELOG.md ================================================ # 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)) ================================================ FILE: jib-gradle-plugin-extension-api/build.gradle ================================================ 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 */ ================================================ FILE: jib-gradle-plugin-extension-api/gradle.properties ================================================ version = 0.4.1-SNAPSHOT ================================================ FILE: jib-gradle-plugin-extension-api/kokoro/release_build.sh ================================================ #!/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 ================================================ FILE: jib-gradle-plugin-extension-api/src/main/java/com/google/cloud/tools/jib/gradle/extension/GradleData.java ================================================ /* * 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(); } ================================================ FILE: jib-gradle-plugin-extension-api/src/main/java/com/google/cloud/tools/jib/gradle/extension/JibGradlePluginExtension.java ================================================ /* * 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; } ================================================ FILE: jib-maven-plugin/CHANGELOG.md ================================================ # Change Log All notable changes to this project will be documented in this file. ## [unreleased] ### Added ### Changed ### Fixed ## 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)) ## 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)) ## 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)) ## 0.9.2 ### Changed - 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)) ### 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)) ### 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)) ## 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)) ### 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)) ### Fixed - 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)) ## 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)) ### 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)) ## 0.1.5 ### Added - 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)) ## 0.1.3 ### Added - 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)) ## 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)) ### Changed - 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)) ================================================ FILE: jib-maven-plugin/README.md ================================================ ![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](https://github.com/opencontainers/image-spec) images for your Java applications. For the Gradle plugin, see the [jib-gradle-plugin project](../jib-gradle-plugin). For information about the project, see the [Jib project README](../README.md). | ☑️ 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:3.5.1: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).* To build to a Docker daemon, use: ```shell mvn compile com.google.cloud.tools:jib-maven-plugin:3.5.1:dockerBuild ``` If you would like to set up Jib as part of your Maven build, follow the guide below. ### Setup In your Maven Java project, add the plugin to your `pom.xml`: ```xml ... ... com.google.cloud.tools jib-maven-plugin 3.5.1 myimage ... ... ``` ### Configuration Configure the plugin by setting the image to push to: #### Using [Google Container Registry (GCR)](https://cloud.google.com/container-registry/)... *Make sure you have the [`docker-credential-gcr` command line tool](https://cloud.google.com/container-registry/docs/advanced-authentication#docker_credential_helper). Jib automatically uses `docker-credential-gcr` for obtaining credentials. See [Authentication Methods](#authentication-methods) for other ways of authenticating.* For example, to build the image `gcr.io/my-gcp-project/my-app`, the configuration would be: ```xml gcr.io/my-gcp-project/my-app ``` #### Using [Amazon Elastic Container Registry (ECR)](https://aws.amazon.com/ecr/)... *Make sure you have the [`docker-credential-ecr-login` command line tool](https://github.com/awslabs/amazon-ecr-credential-helper). Jib automatically uses `docker-credential-ecr-login` for obtaining credentials. See [Authentication Methods](#authentication-methods) for other ways of authenticating.* For example, to build the image `aws_account_id.dkr.ecr.region.amazonaws.com/my-app`, the configuration would be: ```xml aws_account_id.dkr.ecr.region.amazonaws.com/my-app ``` #### Using [Docker Hub Registry](https://hub.docker.com/)... *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-docker-id/my-app`, the configuration would be: ```xml 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 ``` #### 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 Build your container image with: ```shell mvn compile jib: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).* #### Build to Docker daemon Jib can also build your image directly to a Docker daemon. This uses the `docker` command line tool and requires that you have `docker` available on your `PATH`. ```shell 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://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.cloud.tools jib-maven-plugin ... package build ``` Then, you can build your container image by running: ```shell mvn package ``` ### Additional Build Artifacts 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. ## Multi Module Projects 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 Extended configuration options provide additional options for customizing the image build. Field | Type | Default | Description --- | --- | --- | --- `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. `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 | *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. `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 --- | --- | --- | --- `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 --- | --- | --- | --- `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. `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`, `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 openjdk:alpine localhost:5000/my-image:built-with-jib osxkeychain tag2 latest -Dmy.property=example.value -Xms512m -Xdebug mypackage.MyApp some args 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. #### 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 Docker credential helpers are CLI tools that handle authentication with various registries. 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:* ```xml ... aws_account_id.dkr.ecr.region.amazonaws.com/my-base-image ecr-login gcr.io/my-gcp-project/my-app gcr ... ``` #### 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. If you're considering putting credentials in Maven, we highly *recommend* using [maven password encryption](https://maven.apache.org/guides/mini/guide-encryption.html). *Example `settings.xml`:* ```xml ... ... MY_REGISTRY MY_USERNAME {MY_SECRET} ``` * The `id` field should be the registry server these credentials are for. * We *do not* recommend putting your raw password in `settings.xml`. ### 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`. 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. 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. ================================================ FILE: jib-maven-plugin/build.gradle ================================================ 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 */ ================================================ FILE: jib-maven-plugin/gradle.properties ================================================ version = 3.5.2-SNAPSHOT ================================================ FILE: jib-maven-plugin/kokoro/release_build.sh ================================================ #!/bin/bash # Fail on any error. set -o errexit # Display commands to stderr. set -o xtrace # 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 cd github/jib ./gradlew :jib-maven-plugin:prepareRelease ================================================ FILE: jib-maven-plugin/scripts/update_gcs_latest.sh ================================================ #!/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 gsutil cp jib-maven $destination gsutil acl ch -u allUsers:READ $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 ================================================ FILE: jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildDockerMojoIntegrationTest.java ================================================ /* * 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()); } } ================================================ FILE: jib-maven-plugin/src/integration-test/java/com/google/cloud/tools/jib/maven/BuildImageMojoIntegrationTest.java ================================================ /* * 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 : (?