From aa1954717cce4786e056153868a334054cd51a01 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 1 Nov 2019 19:10:19 -0700 Subject: [PATCH] Add cloud native buildpack module Add a Java implementation of the buildpacks.io specification allowing projects to be packaged into OCI containers. The `builder` class provides a Java equivalent of `pack build` command and is based on the `pack` CLI Go code published at https://github.com/buildpacks/pack. Closes gh-19828 --- eclipse/spring-boot-project.setup | 4 +- settings.gradle | 1 + .../build.gradle | 28 + .../build/AbstractBuildLog.java | 96 +++ .../build/ApiVersion.java | 135 ++++ .../cloudnativebuildpack/build/BuildLog.java | 113 ++++ .../build/BuildOwner.java | 100 +++ .../build/BuildRequest.java | 220 +++++++ .../cloudnativebuildpack/build/Builder.java | 115 ++++ .../build/BuilderMetadata.java | 263 ++++++++ .../build/EphemeralBuilder.java | 160 +++++ .../cloudnativebuildpack/build/Lifecycle.java | 300 +++++++++ .../build/LifecycleVersion.java | 140 ++++ .../cloudnativebuildpack/build/Phase.java | 120 ++++ .../build/PrintStreamBuildLog.java | 49 ++ .../cloudnativebuildpack/build/StackId.java | 94 +++ .../build/package-info.java | 20 + .../docker/DockerApi.java | 338 ++++++++++ .../docker/DockerConnectionSocketFactory.java | 57 ++ .../docker/DockerDnsResolver.java | 39 ++ .../docker/DockerException.java | 82 +++ .../DockerHttpClientConnectionManager.java | 42 ++ .../docker/DockerSchemePortResolver.java | 43 ++ .../cloudnativebuildpack/docker/Errors.java | 106 ++++ .../cloudnativebuildpack/docker/Http.java | 92 +++ .../docker/HttpClientHttp.java | 217 +++++++ .../docker/LoadImageUpdateEvent.java | 45 ++ .../docker/LogUpdateEvent.java | 138 ++++ .../docker/ProgressUpdateEvent.java | 102 +++ .../docker/PullImageUpdateEvent.java | 45 ++ .../docker/TotalProgressBar.java | 88 +++ .../docker/TotalProgressEvent.java | 49 ++ .../docker/TotalProgressPullListener.java | 142 +++++ .../docker/UpdateEvent.java | 28 + .../docker/UpdateListener.java | 64 ++ .../docker/package-info.java | 20 + .../docker/type/ContainerConfig.java | 182 ++++++ .../docker/type/ContainerContent.java | 76 +++ .../docker/type/ContainerReference.java | 67 ++ .../docker/type/Image.java | 114 ++++ .../docker/type/ImageArchive.java | 293 +++++++++ .../docker/type/ImageConfig.java | 122 ++++ .../docker/type/ImageName.java | 137 ++++ .../docker/type/ImageReference.java | 268 ++++++++ .../docker/type/Layer.java | 94 +++ .../docker/type/LayerId.java | 105 +++ .../docker/type/RandomString.java | 46 ++ .../docker/type/VolumeName.java | 143 +++++ .../docker/type/package-info.java | 20 + .../boot/cloudnativebuildpack/io/Content.java | 101 +++ .../cloudnativebuildpack/io/DefaultOwner.java | 51 ++ .../cloudnativebuildpack/io/IOConsumer.java | 38 ++ .../cloudnativebuildpack/io/IOSupplier.java | 38 ++ .../io/InspectedContent.java | 187 ++++++ .../boot/cloudnativebuildpack/io/Layout.java | 46 ++ .../boot/cloudnativebuildpack/io/Owner.java | 54 ++ .../cloudnativebuildpack/io/TarArchive.java | 71 +++ .../io/TarLayoutWriter.java | 84 +++ .../io/ZipFileTarArchive.java | 88 +++ .../cloudnativebuildpack/io/package-info.java | 20 + .../cloudnativebuildpack/json/JsonStream.java | 91 +++ .../json/MappedObject.java | 228 +++++++ .../json/SharedObjectMapper.java | 51 ++ .../json/package-info.java | 20 + .../socket/AbstractSocket.java | 87 +++ .../socket/BsdDomainSocket.java | 83 +++ .../socket/DomainSocket.java | 195 ++++++ .../socket/FileDescriptor.java | 123 ++++ .../socket/LinuxDomainSocket.java | 74 +++ .../socket/NamedPipeSocket.java | 162 +++++ .../socket/package-info.java | 20 + .../boot/cloudnativebuildpack/toml/Toml.java | 59 ++ .../toml/package-info.java | 20 + .../build/ApiVersionTests.java | 117 ++++ .../build/BuildLogTests.java | 44 ++ .../build/BuildOwnerTests.java | 86 +++ .../build/BuildRequestTests.java | 167 +++++ .../build/BuilderMetadataTests.java | 97 +++ .../build/BuilderTests.java | 151 +++++ .../build/EphemeralBuilderTests.java | 164 +++++ .../build/LifecycleTests.java | 225 +++++++ .../build/LifecycleVersionTests.java | 72 +++ .../build/PhaseTests.java | 119 ++++ .../build/PrintStreamBuildLogTests.java | 99 +++ .../build/StackIdTests.java | 85 +++ .../docker/DockerApiIntegrationTests.java | 42 ++ .../docker/DockerApiTests.java | 393 ++++++++++++ .../docker/DockerExceptionTests.java | 92 +++ .../docker/ErrorsTests.java | 54 ++ .../docker/HttpClientHttpTests.java | 194 ++++++ .../docker/LoadImageUpdateEventTests.java | 43 ++ .../docker/LogUpdateEventTests.java | 64 ++ .../docker/ProgressUpdateEventTests.java | 75 +++ .../docker/PullImageUpdateEventTests.java | 43 ++ .../docker/PullUpdateEventTests.java | 63 ++ .../docker/TotalProgressBarTests.java | 85 +++ .../docker/TotalProgressEventTests.java | 50 ++ .../TotalProgressPullListenerTests.java | 85 +++ .../docker/type/ContainerConfigTests.java | 67 ++ .../docker/type/ContainerContentTests.java | 70 ++ .../docker/type/ContainerReferenceTests.java | 62 ++ .../docker/type/ImageArchiveTests.java | 101 +++ .../docker/type/ImageConfigTests.java | 66 ++ .../docker/type/ImageNameTests.java | 109 ++++ .../docker/type/ImageReferenceTests.java | 235 +++++++ .../docker/type/ImageTests.java | 74 +++ .../docker/type/LayerIdTests.java | 82 +++ .../docker/type/LayerTests.java | 70 ++ .../docker/type/RandomStringTests.java | 46 ++ .../docker/type/VolumeNameTests.java | 114 ++++ .../cloudnativebuildpack/io/ContentTests.java | 82 +++ .../io/DefaultOwnerTests.java | 48 ++ .../io/InspectedContentTests.java | 99 +++ .../cloudnativebuildpack/io/OwnerTests.java | 37 ++ .../io/TarArchiveTests.java | 93 +++ .../io/TarLayoutWriterTests.java | 65 ++ .../io/ZipFileTarArchiveTests.java | 97 +++ .../json/AbstractJsonTests.java | 42 ++ .../json/JsonStreamTests.java | 83 +++ .../json/MappedObjectTests.java | 122 ++++ .../json/SharedObjectMapperTests.java | 50 ++ .../socket/FileDescriptorTests.java | 95 +++ .../cloudnativebuildpack/toml/TomlTests.java | 43 ++ .../build/builder-metadata.json | 222 +++++++ .../cloudnativebuildpack/build/image.json | 142 +++++ .../build/lifecycle-analyzer.json | 11 + .../build/lifecycle-builder.json | 10 + .../build/lifecycle-cacher.json | 11 + .../build/lifecycle-detector.json | 10 + .../build/lifecycle-exporter.json | 11 + .../build/lifecycle-restorer.json | 11 + .../build/print-stream-build-log.txt | 19 + .../build/run-image-with-bad-stack.json | 142 +++++ .../cloudnativebuildpack/build/run-image.json | 142 +++++ .../docker/create-container-response.json | 4 + .../cloudnativebuildpack/docker/errors.json | 14 + .../docker/load-stream.json | 1 + .../docker/log-update-event-ansi.stream | Bin 0 -> 1367 bytes .../docker/log-update-event.stream | Bin 0 -> 608 bytes .../docker/pull-stream.json | 598 ++++++++++++++++++ .../docker/pull-update-full.json | 9 + .../docker/pull-update-minimal.json | 3 + .../docker/pull-with-empty-details.json | 6 + .../docker/type/container-config.json | 17 + .../docker/type/image-archive-config.json | 224 +++++++ .../docker/type/image-archive-manifest.json | 57 ++ .../docker/type/image-config.json | 29 + .../docker/type/image.json | 142 +++++ .../docker/type/manifest.json | 51 ++ .../cloudnativebuildpack/json/stream.json | 598 ++++++++++++++++++ .../json/test-mapped-object.json | 15 + src/checkstyle/checkstyle-suppressions.xml | 2 + 152 files changed, 14613 insertions(+), 2 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/build.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/AbstractBuildLog.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/ApiVersion.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/BuildLog.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/BuildOwner.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/BuildRequest.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/Builder.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/BuilderMetadata.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/EphemeralBuilder.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/Lifecycle.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/LifecycleVersion.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/Phase.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/PrintStreamBuildLog.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/StackId.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/package-info.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/DockerApi.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/DockerConnectionSocketFactory.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/DockerDnsResolver.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/DockerException.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/DockerHttpClientConnectionManager.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/DockerSchemePortResolver.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/Errors.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/Http.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/HttpClientHttp.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/LoadImageUpdateEvent.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/LogUpdateEvent.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/ProgressUpdateEvent.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/PullImageUpdateEvent.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/TotalProgressBar.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/TotalProgressEvent.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/TotalProgressPullListener.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/UpdateEvent.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/UpdateListener.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/package-info.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/ContainerConfig.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/ContainerContent.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/ContainerReference.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/Image.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/ImageArchive.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/ImageConfig.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/ImageName.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/ImageReference.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/Layer.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/LayerId.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/RandomString.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/VolumeName.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/package-info.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/Content.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/DefaultOwner.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/IOConsumer.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/IOSupplier.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/InspectedContent.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/Layout.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/Owner.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/TarArchive.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/TarLayoutWriter.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/ZipFileTarArchive.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/package-info.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/json/JsonStream.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/json/MappedObject.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/json/SharedObjectMapper.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/json/package-info.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/socket/AbstractSocket.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/socket/BsdDomainSocket.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/socket/DomainSocket.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/socket/FileDescriptor.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/socket/LinuxDomainSocket.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/socket/NamedPipeSocket.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/socket/package-info.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/toml/Toml.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/toml/package-info.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/ApiVersionTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/BuildLogTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/BuildOwnerTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/BuildRequestTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/BuilderMetadataTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/BuilderTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/EphemeralBuilderTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/LifecycleTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/LifecycleVersionTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/PhaseTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/PrintStreamBuildLogTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/StackIdTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/DockerApiIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/DockerApiTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/DockerExceptionTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/ErrorsTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/HttpClientHttpTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/LoadImageUpdateEventTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/LogUpdateEventTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/ProgressUpdateEventTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/PullImageUpdateEventTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/PullUpdateEventTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/TotalProgressBarTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/TotalProgressEventTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/TotalProgressPullListenerTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/ContainerConfigTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/ContainerContentTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/ContainerReferenceTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/ImageArchiveTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/ImageConfigTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/ImageNameTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/ImageReferenceTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/ImageTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/LayerIdTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/LayerTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/RandomStringTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/VolumeNameTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/io/ContentTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/io/DefaultOwnerTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/io/InspectedContentTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/io/OwnerTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/io/TarArchiveTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/io/TarLayoutWriterTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/io/ZipFileTarArchiveTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/json/AbstractJsonTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/json/JsonStreamTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/json/MappedObjectTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/json/SharedObjectMapperTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/socket/FileDescriptorTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/toml/TomlTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/builder-metadata.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/image.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/lifecycle-analyzer.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/lifecycle-builder.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/lifecycle-cacher.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/lifecycle-detector.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/lifecycle-exporter.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/lifecycle-restorer.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/print-stream-build-log.txt create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/run-image-with-bad-stack.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/run-image.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/docker/create-container-response.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/docker/errors.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/docker/load-stream.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/docker/log-update-event-ansi.stream create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/docker/log-update-event.stream create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/docker/pull-stream.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/docker/pull-update-full.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/docker/pull-update-minimal.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/docker/pull-with-empty-details.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/docker/type/container-config.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/docker/type/image-archive-config.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/docker/type/image-archive-manifest.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/docker/type/image-config.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/docker/type/image.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/docker/type/manifest.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/json/stream.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/json/test-mapped-object.json diff --git a/eclipse/spring-boot-project.setup b/eclipse/spring-boot-project.setup index eec0ed0a43..b940e2276f 100644 --- a/eclipse/spring-boot-project.setup +++ b/eclipse/spring-boot-project.setup @@ -10,7 +10,7 @@ xmlns:setup.p2="http://www.eclipse.org/oomph/setup/p2/1.0" xmlns:setup.workingsets="http://www.eclipse.org/oomph/setup/workingsets/1.0" xmlns:workingsets="http://www.eclipse.org/oomph/workingsets/1.0" - xsi:schemaLocation="http://www.eclipse.org/oomph/setup/jdt/1.0 https://git.eclipse.org/c/oomph/org.eclipse.oomph.git/plain/setups/models/JDT.ecore http://www.eclipse.org/buildship/oomph/1.0 https://raw.githubusercontent.com/eclipse/buildship/master/org.eclipse.buildship.oomph/model/GradleImport-1.0.ecore http://www.eclipse.org/oomph/predicates/1.0 https://git.eclipse.org/c/oomph/org.eclipse.oomph.git/plain/setups/models/Predicates.ecore http://www.eclipse.org/oomph/setup/workingsets/1.0 https://git.eclipse.org/c/oomph/org.eclipse.oomph.git/plain/setups/models/SetupWorkingSets.ecore http://www.eclipse.org/oomph/workingsets/1.0 https://git.eclipse.org/c/oomph/org.eclipse.oomph.git/plain/setups/models/WorkingSets.ecore" + xsi:schemaLocation="http://www.eclipse.org/oomph/setup/jdt/1.0 http://git.eclipse.org/c/oomph/org.eclipse.oomph.git/plain/setups/models/JDT.ecore http://www.eclipse.org/buildship/oomph/1.0 https://raw.githubusercontent.com/eclipse/buildship/master/org.eclipse.buildship.oomph/model/GradleImport-1.0.ecore http://www.eclipse.org/oomph/predicates/1.0 http://git.eclipse.org/c/oomph/org.eclipse.oomph.git/plain/setups/models/Predicates.ecore http://www.eclipse.org/oomph/setup/workingsets/1.0 http://git.eclipse.org/c/oomph/org.eclipse.oomph.git/plain/setups/models/SetupWorkingSets.ecore http://www.eclipse.org/oomph/workingsets/1.0 http://git.eclipse.org/c/oomph/org.eclipse.oomph.git/plain/setups/models/WorkingSets.ecore" name="spring.boot.2.3.x" label="Spring Boot 2.3.x"> + pattern="spring-boot-(tools|antlib|configuration-.*|loader|.*-tools|.*-plugin|autoconfigure-processor|cloudnativebuildpack)"/> diff --git a/settings.gradle b/settings.gradle index e6ba31be25..0587bc5cba 100644 --- a/settings.gradle +++ b/settings.gradle @@ -41,6 +41,7 @@ include 'spring-boot-project:spring-boot-dependencies' include 'spring-boot-project:spring-boot-parent' include 'spring-boot-project:spring-boot-tools:spring-boot-antlib' include 'spring-boot-project:spring-boot-tools:spring-boot-autoconfigure-processor' +include 'spring-boot-project:spring-boot-tools:spring-boot-cloudnativebuildpack' include 'spring-boot-project:spring-boot-tools:spring-boot-configuration-metadata' include 'spring-boot-project:spring-boot-tools:spring-boot-configuration-processor' include 'spring-boot-project:spring-boot-tools:spring-boot-gradle-plugin' diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/build.gradle new file mode 100644 index 0000000000..c2d39b6939 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/build.gradle @@ -0,0 +1,28 @@ +plugins { + id 'java-library' + id 'org.springframework.boot.conventions' + id 'org.springframework.boot.deployed' + id 'org.springframework.boot.internal-dependency-management' +} + +description = 'Spring Boot Cloud Native Buildpack' + +dependencies { + api platform(project(':spring-boot-project:spring-boot-parent')) + api 'com.fasterxml.jackson.core:jackson-databind' + api 'com.fasterxml.jackson.module:jackson-module-parameter-names' + api 'net.java.dev.jna:jna-platform' + api 'org.apache.commons:commons-compress:1.19' + api 'org.apache.httpcomponents:httpclient' + api 'org.springframework:spring-core' + + testImplementation project(':spring-boot-project:spring-boot-tools:spring-boot-test-support') + testImplementation 'com.jayway.jsonpath:json-path' + testImplementation 'org.assertj:assertj-core' + testImplementation 'org.testcontainers:testcontainers' + testImplementation 'org.hamcrest:hamcrest' + testImplementation 'org.junit.jupiter:junit-jupiter' + testImplementation 'org.mockito:mockito-core' + testImplementation 'org.mockito:mockito-junit-jupiter' + testImplementation 'org.skyscreamer:jsonassert' +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/AbstractBuildLog.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/AbstractBuildLog.java new file mode 100644 index 0000000000..466c28a43e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/AbstractBuildLog.java @@ -0,0 +1,96 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.build; + +import java.util.List; +import java.util.function.Consumer; + +import org.springframework.boot.cloudnativebuildpack.docker.LogUpdateEvent; +import org.springframework.boot.cloudnativebuildpack.docker.TotalProgressEvent; +import org.springframework.boot.cloudnativebuildpack.docker.type.Image; +import org.springframework.boot.cloudnativebuildpack.docker.type.ImageReference; +import org.springframework.boot.cloudnativebuildpack.docker.type.VolumeName; + +/** + * Base class for {@link BuildLog} implementations. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public abstract class AbstractBuildLog implements BuildLog { + + @Override + public void start(BuildRequest request) { + log("Building image '" + request.getName() + "'"); + log(); + } + + @Override + public Consumer pullingBuilder(BuildRequest request, ImageReference imageReference) { + return getProgressConsumer(" > Pulling builder image '" + imageReference + "'"); + } + + @Override + public void pulledBulder(BuildRequest request, Image image) { + log(" > Pulled builder image '" + getDigest(image) + "'"); + } + + @Override + public Consumer pullingRunImage(BuildRequest request, ImageReference imageReference) { + return getProgressConsumer(" > Pulling run image '" + imageReference + "'"); + } + + @Override + public void pulledRunImage(BuildRequest request, Image image) { + log(" > Pulled run image '" + getDigest(image) + "'"); + } + + @Override + public void executingLifecycle(BuildRequest request, LifecycleVersion version, VolumeName buildCacheVolume) { + log(" > Executing lifecycle version " + version); + log(" > Using build cache volume '" + buildCacheVolume + "'"); + } + + @Override + public Consumer runningPhase(BuildRequest request, String name) { + log(); + log(" > Running " + name); + String prefix = String.format(" %-14s", "[" + name + "] "); + return (event) -> log(prefix + event); + } + + @Override + public void executedLifecycle(BuildRequest request) { + log(); + log("Successfully built image '" + request.getName() + "'"); + log(); + } + + private String getDigest(Image image) { + List digests = image.getDigests(); + return (digests.isEmpty() ? "" : digests.get(0)); + } + + protected void log() { + log(""); + } + + protected abstract void log(String message); + + protected abstract Consumer getProgressConsumer(String message); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/ApiVersion.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/ApiVersion.java new file mode 100644 index 0000000000..f91ed85e59 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/ApiVersion.java @@ -0,0 +1,135 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.build; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.util.Assert; + +/** + * API Version number comprised a major and minor value. + * + * @author Phillip Webb + */ +final class ApiVersion { + + /** + * The platform API version supported by this release. + */ + static final ApiVersion PLATFORM = new ApiVersion(0, 1); + + private static final Pattern PATTERN = Pattern.compile("^v?(\\d+)\\.(\\d*)$"); + + private final int major; + + private final int minor; + + private ApiVersion(int major, int minor) { + this.major = major; + this.minor = minor; + } + + /** + * Return the major version number. + * @return the major version + */ + int getMajor() { + return this.major; + } + + /** + * Return the minor version number. + * @return the minor version + */ + int getMinor() { + return this.minor; + } + + /** + * Assert that this API version supports the specified version. + * @param other the version to check against + * @see #supports(ApiVersion) + */ + void assertSupports(ApiVersion other) { + if (!supports(other)) { + throw new IllegalStateException( + "Version '" + other + "' is not supported by this version ('" + this + "')"); + } + } + + /** + * Returns if this API version supports the given version. A {@code 0.x} matches only + * the same version number. A 1.x or higher release matches when the versions have the + * same major version and a minor that is equal or greater. + * @param other the version to check against + * @return of the specified API is supported + * @see #assertSupports(ApiVersion) + */ + boolean supports(ApiVersion other) { + if (equals(other)) { + return true; + } + if (this.major == 0 || this.major != other.major) { + return false; + } + return this.minor >= other.minor; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ApiVersion other = (ApiVersion) obj; + return (this.major == other.major) && (this.minor == other.minor); + } + + @Override + public int hashCode() { + return this.major * 31 + this.minor; + } + + @Override + public String toString() { + return "v" + this.major + "." + this.minor; + } + + /** + * Factory method to parse a string into an {@link ApiVersion} instance. + * @param value the value to parse. + * @return the corresponding {@link ApiVersion} + * @throws IllegalArgumentException if the value could not be parsed + */ + static ApiVersion parse(String value) { + Assert.hasText(value, "Value must not be empty"); + Matcher matcher = PATTERN.matcher(value); + Assert.isTrue(matcher.matches(), "Malformed version number '" + value + "'"); + try { + int major = Integer.parseInt(matcher.group(1)); + int minor = Integer.parseInt(matcher.group(2)); + return new ApiVersion(major, minor); + } + catch (NumberFormatException ex) { + throw new IllegalArgumentException("Malformed version number '" + value + "'", ex); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/BuildLog.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/BuildLog.java new file mode 100644 index 0000000000..a57332915a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/BuildLog.java @@ -0,0 +1,113 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.build; + +import java.io.PrintStream; +import java.util.function.Consumer; + +import org.springframework.boot.cloudnativebuildpack.docker.LogUpdateEvent; +import org.springframework.boot.cloudnativebuildpack.docker.TotalProgressEvent; +import org.springframework.boot.cloudnativebuildpack.docker.type.Image; +import org.springframework.boot.cloudnativebuildpack.docker.type.ImageReference; +import org.springframework.boot.cloudnativebuildpack.docker.type.VolumeName; + +/** + * Callback interface used to provide {@link Builder} output logging. + * + * @author Phillip Webb + * @since 2.3.0 + * @see #toSystemOut() + */ +public interface BuildLog { + + /** + * Log that a build is starting. + * @param request the build request + */ + void start(BuildRequest request); + + /** + * Log that the builder image is being pulled. + * @param request the build request + * @param imageReference the builder image reference + * @return a consumer for progress update events + */ + Consumer pullingBuilder(BuildRequest request, ImageReference imageReference); + + /** + * Log that the builder image has been pulled. + * @param request the build request + * @param image the builder image that was pulled + */ + void pulledBulder(BuildRequest request, Image image); + + /** + * Log that a run image is being pulled. + * @param request the build request + * @param imageReference the run image reference + * @return a consumer for progress update events + */ + Consumer pullingRunImage(BuildRequest request, ImageReference imageReference); + + /** + * Log that a run image has been pulled. + * @param request the build request + * @param image the run image that was pulled + */ + void pulledRunImage(BuildRequest request, Image image); + + /** + * Log that the lifecycle is executing. + * @param request the build request + * @param version the lifecyle version + * @param buildCacheVolume the name of the build cache volume in use + */ + void executingLifecycle(BuildRequest request, LifecycleVersion version, VolumeName buildCacheVolume); + + /** + * Log that a specific phase is running. + * @param request the build request + * @param name the name of the phase + * @return a consumer for log updates + */ + Consumer runningPhase(BuildRequest request, String name); + + /** + * Log that the lifecycle has executed. + * @param request the build request + */ + void executedLifecycle(BuildRequest request); + + /** + * Factory method that returns a {@link BuildLog} the outputs to {@link System#out}. + * @return a build log instance that logs to system out + */ + static BuildLog toSystemOut() { + return to(System.out); + } + + /** + * Factory method that returns a {@link BuildLog} the outputs to a given + * {@link PrintStream}. + * @param out the print stream used to output the log + * @return a build log instance that logs to the given print stream + */ + static BuildLog to(PrintStream out) { + return new PrintStreamBuildLog(out); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/BuildOwner.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/BuildOwner.java new file mode 100644 index 0000000000..3ff65660a5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/BuildOwner.java @@ -0,0 +1,100 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.build; + +import java.util.Map; + +import org.springframework.boot.cloudnativebuildpack.io.Owner; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * The {@link Owner} that should perform the build. + * + * @author Phillip Webb + */ +class BuildOwner implements Owner { + + private static final String USER_PROPERTY_NAME = "CNB_USER_ID"; + + private static final String GROUP_PROPERTY_NAME = "CNB_GROUP_ID"; + + private final long uid; + + private final long gid; + + BuildOwner(Map env) { + this.uid = getValue(env, USER_PROPERTY_NAME); + this.gid = getValue(env, GROUP_PROPERTY_NAME); + } + + BuildOwner(long uid, long gid) { + this.uid = uid; + this.gid = gid; + } + + private long getValue(Map env, String name) { + String value = env.get(name); + Assert.state(StringUtils.hasText(value), "Missing '" + name + "' value from the builder environment"); + try { + return Long.parseLong(value); + } + catch (NumberFormatException ex) { + throw new IllegalStateException("Malformed '" + name + "' value '" + value + "' in the builder environment", + ex); + } + } + + @Override + public long getUid() { + return this.uid; + } + + @Override + public long getGid() { + return this.gid; + } + + @Override + public String toString() { + return this.uid + "/" + this.gid; + } + + /** + * Factory method to create the {@link BuildOwner} by inspecting the image env for + * {@code CNB_USER_ID}/{@code CNB_GROUP_ID} variables. + * @param env the env to parse + * @return a {@link BuildOwner} instance extracted from the env + * @throws IllegalStateException if the env does not contain the correct CNB variables + */ + static BuildOwner fromEnv(Map env) { + Assert.notNull(env, "Env must not be null"); + return new BuildOwner(env); + } + + /** + * Factory method to create a new {@link BuildOwner} with specified user/group + * identifier. + * @param uid the user identifier + * @param gid the group identifier + * @return a new {@link BuildOwner} instance + */ + static BuildOwner of(long uid, long gid) { + return new BuildOwner(uid, gid); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/BuildRequest.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/BuildRequest.java new file mode 100644 index 0000000000..5d2a5839d3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/BuildRequest.java @@ -0,0 +1,220 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.build; + +import java.io.File; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Function; + +import org.springframework.boot.cloudnativebuildpack.docker.type.ImageReference; +import org.springframework.boot.cloudnativebuildpack.io.Owner; +import org.springframework.boot.cloudnativebuildpack.io.TarArchive; +import org.springframework.util.Assert; + +/** + * A build request to be handled by the {@link Builder}. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public class BuildRequest { + + private static final ImageReference DEFAULT_BUILDER = ImageReference.of("cloudfoundry/cnb:0.0.43-bionic"); + + private final ImageReference name; + + private final Function applicationContent; + + private final ImageReference builder; + + private final Map env; + + private final boolean cleanCache; + + private final boolean versboseLogging; + + BuildRequest(ImageReference name, Function applicationContent) { + Assert.notNull(name, "Name must not be null"); + Assert.notNull(applicationContent, "ApplicationContent must not be null"); + this.name = name.inTaggedForm(); + this.applicationContent = applicationContent; + this.builder = DEFAULT_BUILDER; + this.env = Collections.emptyMap(); + this.cleanCache = false; + this.versboseLogging = false; + } + + BuildRequest(ImageReference name, Function applicationContent, ImageReference builder, + Map env, boolean cleanCache, boolean versboseLogging) { + this.name = name; + this.applicationContent = applicationContent; + this.builder = builder; + this.env = env; + this.cleanCache = cleanCache; + this.versboseLogging = versboseLogging; + } + + /** + * Return a new {@link BuildRequest} with an updated builder. + * @param builder the new builder to use + * @return an updated build request + */ + public BuildRequest withBuilder(ImageReference builder) { + Assert.notNull(builder, "Builder must not be null"); + return new BuildRequest(this.name, this.applicationContent, builder.inTaggedForm(), this.env, this.cleanCache, + this.versboseLogging); + } + + /** + * Return a new {@link BuildRequest} with an additional env variable. + * @param name the variable name + * @param value the variable value + * @return an updated build request + */ + public BuildRequest withEnv(String name, String value) { + Assert.hasText(name, "Name must not be empty"); + Assert.hasText(value, "Value must not be empty"); + Map env = new LinkedHashMap(this.env); + env.put(name, value); + return new BuildRequest(this.name, this.applicationContent, this.builder, Collections.unmodifiableMap(env), + this.cleanCache, this.versboseLogging); + } + + /** + * Return a new {@link BuildRequest} with an additional env variables. + * @param env the additional variables + * @return an updated build request + */ + public BuildRequest withEnv(Map env) { + Assert.notNull(env, "Env must not be null"); + Map updatedEnv = new LinkedHashMap(this.env); + updatedEnv.putAll(env); + return new BuildRequest(this.name, this.applicationContent, this.builder, + Collections.unmodifiableMap(updatedEnv), this.cleanCache, this.versboseLogging); + } + + /** + * Return a new {@link BuildRequest} with an specific clean cache settings. + * @param cleanCache if the cache should be cleaned + * @return an updated build request + */ + public BuildRequest withCleanCache(boolean cleanCache) { + return new BuildRequest(this.name, this.applicationContent, this.builder, this.env, cleanCache, + this.versboseLogging); + } + + /** + * Return a new {@link BuildRequest} with an specific verbose logging settings. + * @param verboseLogging if verbose logging should be used + * @return an updated build request + */ + public BuildRequest withVerboseLogging(boolean verboseLogging) { + return new BuildRequest(this.name, this.applicationContent, this.builder, this.env, this.cleanCache, + verboseLogging); + } + + /** + * Return the name of the image that should be created. + * @return the name of the image + */ + public ImageReference getName() { + return this.name; + } + + /** + * Return a {@link TarArchive} containing the application content that the buildpack + * should package. This is typically the contents of the Jar. + * @param owner the owner of the tar entries + * @return the application content + * @see TarArchive#fromZip(File, Owner) + */ + public TarArchive getApplicationContent(Owner owner) { + return this.applicationContent.apply(owner); + } + + /** + * Return the builder that should be used. + * @return the builder to use + */ + public ImageReference getBuilder() { + return this.builder; + } + + /** + * Return any env variable that should be passed to the builder. + * @return the builder env + */ + public Map getEnv() { + return this.env; + } + + /** + * Return if caches should be cleaned before packaging. + * @return if caches should be cleaned + */ + public boolean isCleanCache() { + return this.cleanCache; + } + + /** + * Return if verbose logging output should be used. + * @return if verbose logging should be used + */ + public boolean isVerboseLogging() { + return this.versboseLogging; + } + + /** + * Factory method to create a new {@link BuildRequest} from a JAR file. + * @param jarFile the source jar file + * @return a new build request instance + */ + public static BuildRequest forJarFile(File jarFile) { + assertJarFile(jarFile); + return forJarFile(ImageReference.forJarFile(jarFile).inTaggedForm(), jarFile); + } + + /** + * Factory method to create a new {@link BuildRequest} from a JAR file. + * @param name the name of the image that should be created + * @param jarFile the source jar file + * @return a new build request instance + */ + public static BuildRequest forJarFile(ImageReference name, File jarFile) { + assertJarFile(jarFile); + return new BuildRequest(name, (owner) -> TarArchive.fromZip(jarFile, owner)); + } + + /** + * Factory method to create a new {@link BuildRequest} with specific content. + * @param name the name of the image that should be created + * @param applicationContent function to provide the application content + * @return a new build request instance + */ + public static BuildRequest of(ImageReference name, Function applicationContent) { + return new BuildRequest(name, applicationContent); + } + + private static void assertJarFile(File jarFile) { + Assert.notNull(jarFile, "JarFile must not be null"); + Assert.isTrue(jarFile.exists(), "JarFile must exist"); + Assert.isTrue(jarFile.isFile(), "JarFile must be a file"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/Builder.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/Builder.java new file mode 100644 index 0000000000..89dc39041f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/Builder.java @@ -0,0 +1,115 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.build; + +import java.io.IOException; +import java.util.function.Consumer; + +import org.springframework.boot.cloudnativebuildpack.build.BuilderMetadata.Stack; +import org.springframework.boot.cloudnativebuildpack.docker.DockerApi; +import org.springframework.boot.cloudnativebuildpack.docker.DockerException; +import org.springframework.boot.cloudnativebuildpack.docker.TotalProgressEvent; +import org.springframework.boot.cloudnativebuildpack.docker.TotalProgressPullListener; +import org.springframework.boot.cloudnativebuildpack.docker.UpdateListener; +import org.springframework.boot.cloudnativebuildpack.docker.type.Image; +import org.springframework.boot.cloudnativebuildpack.docker.type.ImageReference; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Central API for running buildpack operations. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public class Builder { + + private final BuildLog log; + + private final DockerApi docker; + + public Builder() { + this(BuildLog.toSystemOut()); + } + + public Builder(BuildLog log) { + this(log, new DockerApi()); + } + + Builder(BuildLog log, DockerApi docker) { + Assert.notNull(log, "Log must not be null"); + this.log = log; + this.docker = docker; + } + + public void build(BuildRequest request) throws DockerException, IOException { + Assert.notNull(request, "Request must not be null"); + this.log.start(request); + Image builderImage = pullBuilder(request); + BuilderMetadata builderMetadata = BuilderMetadata.fromImage(builderImage); + BuildOwner buildOwner = BuildOwner.fromEnv(builderImage.getConfig().getEnv()); + StackId stackId = StackId.fromImage(builderImage); + ImageReference runImageReference = getRunImageReference(builderMetadata.getStack()); + Image runImage = pullRunImage(request, runImageReference); + assertHasExpectedStackId(runImage, stackId); + EphemeralBuilder builder = new EphemeralBuilder(buildOwner, builderImage, builderMetadata, request.getEnv()); + this.docker.image().load(builder.getArchive(), UpdateListener.none()); + try { + executeLifecycle(request, runImageReference, builder); + } + finally { + this.docker.image().remove(builder.getName(), true); + } + } + + private Image pullBuilder(BuildRequest request) throws IOException { + ImageReference builderImageReference = request.getBuilder(); + Consumer progressConsumer = this.log.pullingBuilder(request, builderImageReference); + TotalProgressPullListener listener = new TotalProgressPullListener(progressConsumer); + Image builderImage = this.docker.image().pull(builderImageReference, listener); + this.log.pulledBulder(request, builderImage); + return builderImage; + } + + private ImageReference getRunImageReference(Stack stack) { + String name = stack.getRunImage().getImage(); + Assert.state(StringUtils.hasText(name), "Run image must be specified"); + return ImageReference.of(name); + } + + private Image pullRunImage(BuildRequest request, ImageReference name) throws IOException { + Consumer progressConsumer = this.log.pullingRunImage(request, name); + TotalProgressPullListener listener = new TotalProgressPullListener(progressConsumer); + Image image = this.docker.image().pull(name, listener); + this.log.pulledRunImage(request, image); + return image; + } + + private void assertHasExpectedStackId(Image image, StackId stackId) { + StackId pulledStackId = StackId.fromImage(image); + Assert.state(pulledStackId.equals(stackId), + "Run image stack '" + pulledStackId + "' does not match builder stack '" + stackId + "'"); + } + + private void executeLifecycle(BuildRequest request, ImageReference runImageReference, EphemeralBuilder builder) + throws IOException { + try (Lifecycle lifecycle = new Lifecycle(this.log, this.docker, request, runImageReference, builder)) { + lifecycle.execute(); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/BuilderMetadata.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/BuilderMetadata.java new file mode 100644 index 0000000000..b1b2662cb2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/BuilderMetadata.java @@ -0,0 +1,263 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.build; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.util.Map; +import java.util.function.Consumer; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import org.springframework.boot.cloudnativebuildpack.docker.type.Image; +import org.springframework.boot.cloudnativebuildpack.docker.type.ImageConfig; +import org.springframework.boot.cloudnativebuildpack.json.MappedObject; +import org.springframework.boot.cloudnativebuildpack.json.SharedObjectMapper; +import org.springframework.util.Assert; + +/** + * Builder metadata information. + * + * @author Phillip Webb + */ +class BuilderMetadata extends MappedObject { + + private static final String LABEL_NAME = "io.buildpacks.builder.metadata"; + + private static final String[] EMPTY_MIRRORS = {}; + + private final Stack stack; + + private final Lifecycle lifecycle; + + private final CreatedBy createdBy; + + BuilderMetadata(JsonNode node) { + super(node, MethodHandles.lookup()); + this.stack = valueAt("/stack", Stack.class); + this.lifecycle = valueAt("/lifecycle", Lifecycle.class); + this.createdBy = valueAt("/createdBy", CreatedBy.class); + } + + /** + * Return stack metadata. + * @return the stack metadata + */ + Stack getStack() { + return this.stack; + } + + /** + * Return lifecycle metadata. + * @return the lifecycle metadata + */ + Lifecycle getLifecycle() { + return this.lifecycle; + } + + /** + * Return information about who created the builder. + * @return the created by metadata + */ + CreatedBy getCreatedBy() { + return this.createdBy; + } + + /** + * Create an updated copy of this metadata. + * @param update consumer to apply updates + * @return an updated metadata instance + */ + BuilderMetadata copy(Consumer update) { + return new Update(this).run(update); + } + + /** + * Attach this metadata to the given update callback. + * @param update the update used to attach the metadata + */ + void attachTo(ImageConfig.Update update) { + try { + String json = SharedObjectMapper.get().writeValueAsString(getNode()); + update.withLabel(LABEL_NAME, json); + } + catch (JsonProcessingException ex) { + throw new IllegalStateException(ex); + } + } + + /** + * Factory method to extract {@link BuilderMetadata} from an image. + * @param image the source image + * @return the builder metadata + * @throws IOException on IO error + */ + static BuilderMetadata fromImage(Image image) throws IOException { + Assert.notNull(image, "Image must not be null"); + return fromImageConfig(image.getConfig()); + } + + /** + * Factory method to extract {@link BuilderMetadata} from image config. + * @param imageConfig the image config + * @return the builder metadata + * @throws IOException on IO error + */ + static BuilderMetadata fromImageConfig(ImageConfig imageConfig) throws IOException { + Assert.notNull(imageConfig, "ImageConfig must not be null"); + Map labels = imageConfig.getLabels(); + String json = (labels != null) ? labels.get(LABEL_NAME) : null; + Assert.notNull(json, "No '" + LABEL_NAME + "' label found in image config"); + return fromJson(json); + } + + /** + * Factory method create {@link BuilderMetadata} from some JSON. + * @param json the source JSON + * @return the builder metadata + * @throws IOException on IO error + */ + static BuilderMetadata fromJson(String json) throws IOException { + return new BuilderMetadata(SharedObjectMapper.get().readTree(json)); + } + + /** + * Stack metadata. + */ + interface Stack { + + /** + * Return run image metadata. + * @return the run image metadata + */ + RunImage getRunImage(); + + /** + * Run image metadata. + */ + interface RunImage { + + /** + * Return the builder image reference. + * @return the image reference + */ + String getImage(); + + /** + * Return stack mirrors. + * @return the stack mirrors + */ + default String[] getMirrors() { + return EMPTY_MIRRORS; + } + + } + + } + + /** + * Lifecycle metadata. + */ + interface Lifecycle { + + /** + * Return the lifecycle version. + * @return the lifecycle version + */ + String getVersion(); + + /** + * Return the API versions. + * @return the API versions + */ + Api getApi(); + + /** + * API versions. + */ + interface Api { + + /** + * Return the buildpack API version. + * @return the buildpack version + */ + String getBuildpack(); + + /** + * Return the platform API version. + * @return the platform version + */ + String getPlatform(); + + } + + } + + /** + * Created-by metadata. + */ + interface CreatedBy { + + /** + * Return the name of the creator. + * @return the creator name + */ + String getName(); + + /** + * Return the version of the creator. + * @return the creator version + */ + String getVersion(); + + } + + /** + * Update class used to change data when creating a copy. + */ + static final class Update { + + private ObjectNode copy; + + private Update(BuilderMetadata source) { + this.copy = source.getNode().deepCopy(); + } + + private BuilderMetadata run(Consumer update) { + update.accept(this); + return new BuilderMetadata(this.copy); + } + + /** + * Update the builder meta-data with a specific created by section. + * @param name the name of the creator + * @param version the version of the creator + */ + void withCreatedBy(String name, String version) { + ObjectNode createdBy = (ObjectNode) this.copy.at("/createdBy"); + if (createdBy == null) { + createdBy = this.copy.putObject("createdBy"); + } + createdBy.put("name", name); + createdBy.put("version", version); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/EphemeralBuilder.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/EphemeralBuilder.java new file mode 100644 index 0000000000..2b5141c69d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/EphemeralBuilder.java @@ -0,0 +1,160 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.build; + +import java.io.IOException; +import java.time.Clock; +import java.time.Instant; +import java.util.Map; + +import org.springframework.boot.cloudnativebuildpack.build.BuilderMetadata.Stack.RunImage; +import org.springframework.boot.cloudnativebuildpack.docker.type.Image; +import org.springframework.boot.cloudnativebuildpack.docker.type.ImageArchive; +import org.springframework.boot.cloudnativebuildpack.docker.type.ImageReference; +import org.springframework.boot.cloudnativebuildpack.docker.type.Layer; +import org.springframework.boot.cloudnativebuildpack.io.Content; +import org.springframework.boot.cloudnativebuildpack.io.Owner; +import org.springframework.boot.cloudnativebuildpack.toml.Toml; + +/** + * An short lived builder that is created for each {@link Lifecycle} run. + * + * @author Phillip Webb + */ +class EphemeralBuilder { + + private final BuildOwner buildOwner; + + private final BuilderMetadata builderMetadata; + + private final ImageArchive archive; + + /** + * Create a new {@link EphemeralBuilder} instance. + * @param buildOwner the build owner + * @param builderImage the image + * @param builderMetadata the builder metadata + * @param env the builder env + * @throws IOException on IO error + */ + EphemeralBuilder(BuildOwner buildOwner, Image builderImage, BuilderMetadata builderMetadata, + Map env) throws IOException { + this(Clock.systemUTC(), buildOwner, builderImage, builderMetadata, env); + } + + /** + * Create a new {@link EphemeralBuilder} instance with a specific clock. + * @param clock the clock used for the current time + * @param buildOwner the build owner + * @param builderImage the image + * @param builderMetadata the builder metadata + * @param env the builder env + * @throws IOException on IO error + */ + EphemeralBuilder(Clock clock, BuildOwner buildOwner, Image builderImage, BuilderMetadata builderMetadata, + Map env) throws IOException { + ImageReference name = ImageReference.random("pack.local/builder/").inTaggedForm(); + this.buildOwner = buildOwner; + this.builderMetadata = builderMetadata.copy(this::updateMetadata); + this.archive = ImageArchive.from(builderImage, (update) -> { + update.withUpdatedConfig(this.builderMetadata::attachTo); + update.withTag(name); + update.withCreateDate(Instant.now(clock)); + update.withNewLayer(getDefaultDirsLayer(buildOwner)); + update.withNewLayer(getStackLayer(builderMetadata)); + if (env != null && !env.isEmpty()) { + update.withNewLayer(getEnvLayer(env)); + } + }); + } + + private void updateMetadata(BuilderMetadata.Update update) { + update.withCreatedBy("Spring Boot", "dev"); + } + + private Layer getDefaultDirsLayer(Owner buildOwner) throws IOException { + return Layer.of((layout) -> { + layout.folder("/workspace", buildOwner); + layout.folder("/layers", buildOwner); + layout.folder("/cnb", Owner.ROOT); + layout.folder("/cnb/buildpacks", Owner.ROOT); + layout.folder("/platform", Owner.ROOT); + layout.folder("/platform/env", Owner.ROOT); + }); + } + + private Layer getStackLayer(BuilderMetadata builderMetadata) throws IOException { + Toml toml = getRunImageToml(builderMetadata.getStack().getRunImage()); + return Layer.of((layout) -> layout.file("/cnb/stack.toml", Owner.ROOT, Content.of(toml.toString()))); + } + + private Toml getRunImageToml(RunImage runImage) { + Toml toml = new Toml(); + toml.table("run-image"); + toml.string("image", runImage.getImage()); + toml.array("mirrors", runImage.getMirrors()); + return toml; + } + + private Layer getEnvLayer(Map env) throws IOException { + return Layer.of((layout) -> { + for (Map.Entry entry : env.entrySet()) { + String name = "/platform/env/" + entry.getKey(); + Content content = Content.of(entry.getValue()); + layout.file(name, Owner.ROOT, content); + } + }); + } + + /** + * Return the name of this archive as tagged in Docker. + * @return the ephemeral builder name + */ + ImageReference getName() { + return this.archive.getTag(); + } + + /** + * Return the build owner that should be used for written content. + * @return the builder owner + */ + Owner getBuildOwner() { + return this.buildOwner; + } + + /** + * Return the builder meta-data that was used to create this ephemeral builder. + * @return the builder meta-data + */ + BuilderMetadata getBuilderMetadata() { + return this.builderMetadata; + } + + /** + * Return the contents of ephemeral builder for passing to Docker. + * @return the ephemeral builder archive + */ + ImageArchive getArchive() { + return this.archive; + } + + @Override + public String toString() { + return this.archive.getTag().toString(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/Lifecycle.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/Lifecycle.java new file mode 100644 index 0000000000..d5dce332e5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/Lifecycle.java @@ -0,0 +1,300 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.build; + +import java.io.Closeable; +import java.io.IOException; +import java.util.function.Consumer; + +import org.springframework.boot.cloudnativebuildpack.docker.DockerApi; +import org.springframework.boot.cloudnativebuildpack.docker.LogUpdateEvent; +import org.springframework.boot.cloudnativebuildpack.docker.type.ContainerConfig; +import org.springframework.boot.cloudnativebuildpack.docker.type.ContainerContent; +import org.springframework.boot.cloudnativebuildpack.docker.type.ContainerReference; +import org.springframework.boot.cloudnativebuildpack.docker.type.ImageReference; +import org.springframework.boot.cloudnativebuildpack.docker.type.VolumeName; +import org.springframework.boot.cloudnativebuildpack.io.TarArchive; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * A buildpack lifecycle used to run the build {@link Phase phases} needed to package an + * application. + * + * @author Phillip Webb + */ +class Lifecycle implements Closeable { + + private static final LifecycleVersion LOGGING_SUPPORTED_VERSION = LifecycleVersion.parse("0.0.5"); + + private final BuildLog log; + + private final DockerApi docker; + + private final BuildRequest request; + + private final ImageReference runImageReference; + + private final EphemeralBuilder builder; + + private final LifecycleVersion version; + + private final VolumeName layersVolume; + + private final VolumeName applicationVolume; + + private final VolumeName buildCacheVolume; + + private final VolumeName launchCacheVolume; + + private boolean executed; + + private boolean applicationVolumePopulated; + + /** + * Create a new {@link Lifecycle} instance. + * @param log build output log + * @param docker the Docker API + * @param request the request to process + * @param runImageReferece a reference to run image that should be used + * @param builder the ephemeral builder used to run the phases + */ + Lifecycle(BuildLog log, DockerApi docker, BuildRequest request, ImageReference runImageReferece, + EphemeralBuilder builder) { + checkPlatformVersion(builder); + this.log = log; + this.docker = docker; + this.request = request; + this.runImageReference = runImageReferece; + this.builder = builder; + this.version = LifecycleVersion.parse(builder.getBuilderMetadata().getLifecycle().getVersion()); + this.layersVolume = createRandomVolumeName("pack-layers-"); + this.applicationVolume = createRandomVolumeName("pack-app-"); + this.buildCacheVolume = createCacheVolumeName(request, ".build"); + this.launchCacheVolume = createCacheVolumeName(request, ".launch"); + } + + protected VolumeName createRandomVolumeName(String prefix) { + return VolumeName.random(prefix); + } + + private VolumeName createCacheVolumeName(BuildRequest request, String suffix) { + return VolumeName.basedOn(request.getName(), ImageReference::toLegacyString, "pack-cache-", suffix, 6); + } + + private void checkPlatformVersion(EphemeralBuilder ephemeralBuilder) { + String platformVersion = ephemeralBuilder.getBuilderMetadata().getLifecycle().getApi().getPlatform(); + if (StringUtils.hasText(platformVersion)) { + ApiVersion.PLATFORM.assertSupports(ApiVersion.parse(platformVersion)); + } + } + + /** + * Execute this lifecycle by running each phase in turn. + * @throws IOException on IO error + */ + void execute() throws IOException { + Assert.state(!this.executed, "Lifecycle has already been executed"); + this.executed = true; + this.log.executingLifecycle(this.request, this.version, this.buildCacheVolume); + if (this.request.isCleanCache()) { + deleteVolume(this.buildCacheVolume); + } + run(detectPhase()); + run(restorePhase()); + run(analyzePhase()); + run(buildPhase()); + run(exportPhase()); + run(cachePhase()); + this.log.executedLifecycle(this.request); + } + + private Phase detectPhase() { + Phase phase = createPhase("detector"); + phase.withArgs("-app", Folder.APPLICATION); + phase.withArgs("-platform", Folder.PLATFORM); + phase.withLogLevelArg(); + return phase; + } + + private Phase restorePhase() { + Phase phase = createPhase("restorer"); + phase.withDaemonAccess(); + phase.withArgs("-path", Folder.CACHE); + phase.withArgs("-layers", Folder.LAYERS); + phase.withLogLevelArg(); + phase.withBinds(this.buildCacheVolume, Folder.CACHE); + return phase; + } + + private Phase analyzePhase() { + Phase phase = createPhase("analyzer"); + phase.withDaemonAccess(); + phase.withLogLevelArg(); + if (this.request.isCleanCache()) { + phase.withArgs("-skip-layers"); + } + phase.withArgs("-daemon"); + phase.withArgs("-layers", Folder.LAYERS); + phase.withArgs(this.request.getName()); + return phase; + } + + private Phase buildPhase() { + Phase phase = createPhase("builder"); + phase.withArgs("-layers", Folder.LAYERS); + phase.withArgs("-app", Folder.APPLICATION); + phase.withArgs("-platform", Folder.PLATFORM); + return phase; + } + + private Phase exportPhase() { + Phase phase = createPhase("exporter"); + phase.withDaemonAccess(); + phase.withLogLevelArg(); + phase.withArgs("-image", this.runImageReference); + phase.withArgs("-layers", Folder.LAYERS); + phase.withArgs("-app", Folder.APPLICATION); + phase.withArgs("-daemon"); + phase.withArgs("-launch-cache", Folder.LAUNCH_CACHE); + phase.withArgs(this.request.getName()); + phase.withBinds(this.launchCacheVolume, Folder.LAUNCH_CACHE); + return phase; + } + + private Phase cachePhase() { + Phase phase = createPhase("cacher"); + phase.withDaemonAccess(); + phase.withArgs("-path", Folder.CACHE); + phase.withArgs("-layers", Folder.LAYERS); + phase.withLogLevelArg(); + phase.withBinds(this.buildCacheVolume, Folder.CACHE); + return phase; + } + + private Phase createPhase(String name) { + boolean verboseLogging = this.request.isVerboseLogging() + && this.version.isEqualOrGreaterThan(LOGGING_SUPPORTED_VERSION); + Phase phase = new Phase(name, verboseLogging); + phase.withBinds(this.layersVolume, Folder.LAYERS); + phase.withBinds(this.applicationVolume, Folder.APPLICATION); + return phase; + } + + private void run(Phase phase) throws IOException { + Consumer logConsumer = this.log.runningPhase(this.request, phase.getName()); + ContainerConfig containerConfig = ContainerConfig.of(this.builder.getName(), phase::apply); + ContainerReference reference = createContainer(containerConfig); + try { + this.docker.container().start(reference); + this.docker.container().logs(reference, logConsumer::accept); + } + finally { + this.docker.container().remove(reference, true); + } + } + + private ContainerReference createContainer(ContainerConfig config) throws IOException { + if (this.applicationVolumePopulated) { + return this.docker.container().create(config); + } + try { + TarArchive applicationContent = this.request.getApplicationContent(this.builder.getBuildOwner()); + return this.docker.container().create(config, ContainerContent.of(applicationContent, Folder.APPLICATION)); + } + finally { + this.applicationVolumePopulated = true; + } + } + + @Override + public void close() throws IOException { + deleteVolume(this.layersVolume); + deleteVolume(this.applicationVolume); + } + + private void deleteVolume(VolumeName name) throws IOException { + this.docker.volume().delete(name, true); + } + + /** + * Common folders used by the various phases. + */ + private static class Folder { + + /** + * The folder used by buildpacks to write their layer contributions. A new layer + * folder is created for each lifecycle execution. + *

+ * Maps to the {@code } concept in the + * buildpack + * specification and the {@code -layers} argument from the reference lifecycle + * implementation. + */ + static final String LAYERS = "/layers"; + + /** + * The folder containing the original contributed application. A new application + * folder is created for each lifecycle execution. + *

+ * Maps to the {@code } concept in the + * buildpack + * specification and the {@code -app} argument from the reference lifecycle + * implementation. The reference lifecycle follows the Kubernetes/Docker + * convention of using {@code '/workspace'}. + *

+ * Note that application content is uploaded to the container with the first phase + * that runs and saved in a volume that is passed to supsequent phases. The folder + * is mutable and buildpacks may modify the content. + */ + static final String APPLICATION = "/workspace"; + + /** + * The folder used by buildpacks to obtain environment variables and platform + * specific concerns. The platform folder is read-only and is created/populated by + * the {@link EphemeralBuilder}. + *

+ * Maps to the {@code /env} and {@code /#} concepts in the + * buildpack + * specification and the {@code -platform} argument from the reference + * lifecycle implementation. + */ + static final String PLATFORM = "/platform"; + + /** + * The folder used by buildpacks for caching. The volume name is based on the + * image {@link BuildRequest#getName() name} being built, and is persistent across + * invocations even if the application content has changed. + *

+ * Maps to the {@code -path} argument from the reference lifecycle implementation + * cache and restore phases + */ + static final String CACHE = "/cache"; + + /** + * The folder used by buildpacks for launch related caching. The volume name is + * based on the image {@link BuildRequest#getName() name} being built, and is + * persistent across invocations even if the application content has changed. + *

+ * Maps to the {@code -launch-cache} argument from the reference lifecycle + * implementation export phase + */ + static final String LAUNCH_CACHE = "/launch-cache"; + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/LifecycleVersion.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/LifecycleVersion.java new file mode 100644 index 0000000000..d23b1a8b30 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/LifecycleVersion.java @@ -0,0 +1,140 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.build; + +import java.util.Comparator; + +import org.springframework.util.Assert; + +/** + * A lifecycle version number comprised of a major, minor and patch value. + * + * @author Phillip Webb + */ +class LifecycleVersion implements Comparable { + + private static final Comparator COMPARATOR = Comparator.comparingInt(LifecycleVersion::getMajor) + .thenComparingInt(LifecycleVersion::getMinor).thenComparing(LifecycleVersion::getPatch); + + private final int major; + + private final int minor; + + private final int patch; + + LifecycleVersion(int major, int minor, int patch) { + this.major = major; + this.minor = minor; + this.patch = patch; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + LifecycleVersion other = (LifecycleVersion) obj; + boolean result = true; + result = result && this.major == other.major; + result = result && this.minor == other.minor; + result = result && this.patch == other.patch; + return result; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + this.major; + result = prime * result + this.minor; + result = prime * result + this.patch; + return result; + } + + @Override + public String toString() { + return "v" + this.major + "." + this.minor + "." + this.patch; + } + + /** + * Return if this version is greater than or equal to the specified version. + * @param other the version to compare + * @return {@code true} if this version is greater than or equal to the specified + * version + */ + boolean isEqualOrGreaterThan(LifecycleVersion other) { + return this.compareTo(other) >= 0; + } + + @Override + public int compareTo(LifecycleVersion other) { + return COMPARATOR.compare(this, other); + } + + /** + * Return the major version number. + * @return the major version + */ + int getMajor() { + return this.major; + } + + /** + * Return the minor version number. + * @return the minor version + */ + int getMinor() { + return this.minor; + } + + /** + * Return the patch version number. + * @return the patch version + */ + int getPatch() { + return this.patch; + } + + /** + * Factory method to parse a string into a {@link LifecycleVersion} instance. + * @param value the value to parse. + * @return the corresponding {@link LifecycleVersion} + * @throws IllegalArgumentException if the value could not be parsed + */ + static LifecycleVersion parse(String value) { + Assert.hasText(value, "Value must not be empty"); + if (value.startsWith("v") || value.startsWith("V")) { + value = value.substring(1); + } + String[] components = value.split("\\."); + Assert.isTrue(components.length <= 3, "Malformed version number '" + value + "'"); + int[] versions = new int[3]; + for (int i = 0; i < components.length; i++) { + try { + versions[i] = Integer.parseInt(components[i]); + } + catch (NumberFormatException ex) { + throw new IllegalArgumentException("Malformed version number '" + value + "'", ex); + } + } + return new LifecycleVersion(versions[0], versions[1], versions[2]); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/Phase.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/Phase.java new file mode 100644 index 0000000000..8fb9752ae2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/Phase.java @@ -0,0 +1,120 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.build; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.boot.cloudnativebuildpack.docker.type.ContainerConfig; +import org.springframework.boot.cloudnativebuildpack.docker.type.VolumeName; +import org.springframework.util.StringUtils; + +/** + * An individual build phase executed as part of a {@link Lifecycle} run. + * + * @author Phillip Webb + */ +class Phase { + + private static final String DOMAIN_SOCKET_PATH = "/var/run/docker.sock"; + + private final String name; + + private final boolean verboseLogging; + + private boolean daemonAccess = false; + + private final List args = new ArrayList<>(); + + private final Map binds = new LinkedHashMap<>(); + + /** + * Create a new {@link Phase} instance. + * @param name the name of the phase + * @param verboseLogging if verbose logging is requested + */ + Phase(String name, boolean verboseLogging) { + this.name = name; + this.verboseLogging = verboseLogging; + } + + /** + * Update this phase with Docker daemon access. + */ + void withDaemonAccess() { + this.daemonAccess = true; + } + + /** + * Update this phase with a debug log level arguments if verbose logging has been + * requested. + */ + void withLogLevelArg() { + if (this.verboseLogging) { + this.args.add("-log-level"); + this.args.add("debug"); + } + } + + /** + * Update this phase with additional run arguments. + * @param args the arguments to add + */ + void withArgs(Object... args) { + Arrays.stream(args).map(Object::toString).forEach(this.args::add); + } + + /** + * Update this phase with an addition volume binding. + * @param source the source volume + * @param dest the destination location + */ + void withBinds(VolumeName source, String dest) { + this.binds.put(source, dest); + } + + /** + * Return the name of the phase. + * @return the phase name + */ + String getName() { + return this.name; + } + + @Override + public String toString() { + return this.name; + } + + /** + * Apply this phase settings to a {@link ContainerConfig} update. + * @param update the update to apply the phase to + */ + void apply(ContainerConfig.Update update) { + if (this.daemonAccess) { + update.withUser("root"); + update.withBind(DOMAIN_SOCKET_PATH, DOMAIN_SOCKET_PATH); + } + update.withCommand("/lifecycle/" + this.name, StringUtils.toStringArray(this.args)); + update.withLabel("author", "spring-boot"); + this.binds.forEach((source, dest) -> update.withBind(source, dest)); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/PrintStreamBuildLog.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/PrintStreamBuildLog.java new file mode 100644 index 0000000000..21db26caa7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/PrintStreamBuildLog.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.build; + +import java.io.PrintStream; +import java.util.function.Consumer; + +import org.springframework.boot.cloudnativebuildpack.docker.TotalProgressBar; +import org.springframework.boot.cloudnativebuildpack.docker.TotalProgressEvent; + +/** + * {@link BuildLog} implementation that prints output to a {@link PrintStream}. + * + * @author Phillip Webb + * @see BuildLog#to(PrintStream) + */ +class PrintStreamBuildLog extends AbstractBuildLog { + + private final PrintStream out; + + PrintStreamBuildLog(PrintStream out) { + this.out = out; + } + + @Override + protected void log(String message) { + this.out.println(message); + } + + @Override + protected Consumer getProgressConsumer(String prefix) { + return new TotalProgressBar(prefix, '.', false, this.out); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/StackId.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/StackId.java new file mode 100644 index 0000000000..4b2d7c87bb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/StackId.java @@ -0,0 +1,94 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.build; + +import java.util.Map; + +import org.springframework.boot.cloudnativebuildpack.docker.type.Image; +import org.springframework.boot.cloudnativebuildpack.docker.type.ImageConfig; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * A Stack ID. + * + * @author Phillip Webb + */ +class StackId { + + private static final String LABEL_NAME = "io.buildpacks.stack.id"; + + private final String value; + + StackId(String value) { + this.value = value; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + return this.value.equals(((StackId) obj).value); + } + + @Override + public int hashCode() { + return this.value.hashCode(); + } + + @Override + public String toString() { + return this.value; + } + + /** + * Factory method to create a {@link StackId} from an {@link Image}. + * @param image the source image + * @return the extracted stack ID + */ + static StackId fromImage(Image image) { + Assert.notNull(image, "Image must not be null"); + return fromImageConfig(image.getConfig()); + } + + /** + * Factory method to create a {@link StackId} from an {@link ImageConfig}. + * @param imageConfig the source image config + * @return the extracted stack ID + */ + private static StackId fromImageConfig(ImageConfig imageConfig) { + Map labels = imageConfig.getLabels(); + String value = (labels != null) ? labels.get(LABEL_NAME) : null; + Assert.state(StringUtils.hasText(value), "Missing '" + LABEL_NAME + "' stack label"); + return new StackId(value); + } + + /** + * Factory method to create a {@link StackId} with a given value. + * @param value the stack ID value + * @return a new stack ID instance + */ + static StackId of(String value) { + Assert.hasText(value, "Value must not be empty"); + return new StackId(value); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/package-info.java new file mode 100644 index 0000000000..441d48c0b8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/build/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 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. + */ + +/** + * Central API for performing a buildpack build. + */ +package org.springframework.boot.cloudnativebuildpack.build; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/DockerApi.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/DockerApi.java new file mode 100644 index 0000000000..393e583b93 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/DockerApi.java @@ -0,0 +1,338 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.apache.http.client.utils.URIBuilder; + +import org.springframework.boot.cloudnativebuildpack.docker.Http.Response; +import org.springframework.boot.cloudnativebuildpack.docker.type.ContainerConfig; +import org.springframework.boot.cloudnativebuildpack.docker.type.ContainerContent; +import org.springframework.boot.cloudnativebuildpack.docker.type.ContainerReference; +import org.springframework.boot.cloudnativebuildpack.docker.type.Image; +import org.springframework.boot.cloudnativebuildpack.docker.type.ImageArchive; +import org.springframework.boot.cloudnativebuildpack.docker.type.ImageReference; +import org.springframework.boot.cloudnativebuildpack.docker.type.VolumeName; +import org.springframework.boot.cloudnativebuildpack.json.JsonStream; +import org.springframework.boot.cloudnativebuildpack.json.SharedObjectMapper; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Provides access to the limited set of Docker APIs needed by pack. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public class DockerApi { + + private static final List FORCE_PARAMS = Collections.unmodifiableList(Arrays.asList("force", "1")); + + private final Http http; + + private final JsonStream jsonStream; + + private final ImageApi image; + + private final ContainerApi container; + + private final VolumeApi volume; + + /** + * Create a new {@link DockerApi} instance. + */ + public DockerApi() { + this(new HttpClientHttp()); + } + + /** + * Create a new {@link DockerApi} instance backed by a specific {@link HttpClientHttp} + * implementation. + * @param http the http implementation + */ + DockerApi(Http http) { + this.http = http; + this.jsonStream = new JsonStream(SharedObjectMapper.get()); + this.image = new ImageApi(); + this.container = new ContainerApi(); + this.volume = new VolumeApi(); + } + + private Http http() { + return this.http; + } + + private JsonStream jsonStream() { + return this.jsonStream; + } + + private URI buildUrl(String path, Collection params) { + return buildUrl(path, StringUtils.toStringArray(params)); + } + + private URI buildUrl(String path, String... params) { + try { + URIBuilder builder = new URIBuilder("docker://localhost/v1.40" + path); + int param = 0; + while (param < params.length) { + builder.addParameter(params[param++], params[param++]); + } + return builder.build(); + } + catch (URISyntaxException ex) { + throw new IllegalStateException(ex); + } + } + + /** + * Return the Docker API for image operations. + * @return the image API + */ + public ImageApi image() { + return this.image; + } + + /** + * Return the Docker API for container operations. + * @return the container API + */ + public ContainerApi container() { + return this.container; + } + + public VolumeApi volume() { + return this.volume; + } + + /** + * Docker API for image operations. + */ + public class ImageApi { + + ImageApi() { + } + + /** + * Pull an image from a registry. + * @param reference the image reference to pull + * @param listener a pull listener to receive update events + * @return the {@link ImageApi pulled image} instance + * @throws IOException on IO error + */ + public Image pull(ImageReference reference, UpdateListener listener) throws IOException { + Assert.notNull(reference, "Reference must not be null"); + Assert.notNull(listener, "Listener must not be null"); + URI createUri = buildUrl("/images/create", "fromImage", reference.toString()); + DigestCaptureUpdateListener digestCapture = new DigestCaptureUpdateListener(); + listener.onStart(); + try { + try (Response response = http().post(createUri)) { + jsonStream().get(response.getContent(), PullImageUpdateEvent.class, (event) -> { + digestCapture.onUpdate(event); + listener.onUpdate(event); + }); + } + URI imageUri = buildUrl("/images/" + reference.withDigest(digestCapture.getCapturedDigest()) + "/json"); + try (Response response = http().get(imageUri)) { + return Image.of(response.getContent()); + } + } + finally { + listener.onFinish(); + } + } + + /** + * Load an {@link ImageArchive} into Docker. + * @param archive the archive to load + * @param listener a pull listener to receive update events + * @throws IOException on IO error + */ + public void load(ImageArchive archive, UpdateListener listener) throws IOException { + Assert.notNull(archive, "Archive must not be null"); + Assert.notNull(listener, "Listener must not be null"); + URI loadUri = buildUrl("/images/load"); + listener.onStart(); + try { + try (Response response = http().post(loadUri, "application/x-tar", archive::writeTo)) { + jsonStream().get(response.getContent(), LoadImageUpdateEvent.class, listener::onUpdate); + } + } + finally { + listener.onFinish(); + } + } + + /** + * Remove a specific image. + * @param reference the reference the remove + * @param force if removal should be forced + * @throws IOException on IO error + */ + public void remove(ImageReference reference, boolean force) throws IOException { + Assert.notNull(reference, "Reference must not be null"); + Collection params = force ? FORCE_PARAMS : Collections.emptySet(); + URI uri = buildUrl("/images/" + reference, params); + http().delete(uri); + } + + } + + /** + * Docker API for container operations. + */ + public class ContainerApi { + + ContainerApi() { + } + + /** + * Create a new container a {@link ContainerConfig}. + * @param config the container config + * @param contents additional contents to include + * @return a {@link ContainerReference} for the newly created container + * @throws IOException on IO error + */ + public ContainerReference create(ContainerConfig config, ContainerContent... contents) throws IOException { + Assert.notNull(config, "Config must not be null"); + Assert.noNullElements(contents, "Contents must not contain null elements"); + ContainerReference containerReference = createContainer(config); + for (ContainerContent content : contents) { + uploadContainerContent(containerReference, content); + } + return containerReference; + } + + private ContainerReference createContainer(ContainerConfig config) throws IOException { + URI createUri = buildUrl("/containers/create"); + try (Response response = http().post(createUri, "application/json", config::writeTo)) { + ContainerReference containerReference = ContainerReference + .of(SharedObjectMapper.get().readTree(response.getContent()).at("/Id").asText()); + return containerReference; + } + } + + private void uploadContainerContent(ContainerReference reference, ContainerContent content) throws IOException { + URI uri = buildUrl("/containers/" + reference + "/archive", "path", content.getDestinationPath()); + http().put(uri, "application/x-tar", content.getArchive()::writeTo).close(); + } + + /** + * Start a specific container. + * @param reference the container reference to start + * @throws IOException on IO error + */ + public void start(ContainerReference reference) throws IOException { + Assert.notNull(reference, "Reference must not be null"); + URI uri = buildUrl("/containers/" + reference + "/start"); + http().post(uri); + } + + /** + * Return and follow logs for a specific container. + * @param reference the container reference + * @param listener a listener to receive log update events + * @throws IOException on IO error + */ + public void logs(ContainerReference reference, UpdateListener listener) throws IOException { + Assert.notNull(reference, "Reference must not be null"); + Assert.notNull(listener, "Listener must not be null"); + String[] params = { "stdout", "1", "stderr", "1", "follow", "1" }; + URI uri = buildUrl("/containers/" + reference + "/logs", params); + listener.onStart(); + try { + try (Response response = http().get(uri)) { + LogUpdateEvent.readAll(response.getContent(), listener::onUpdate); + } + } + finally { + listener.onFinish(); + } + } + + /** + * Remove a specific container. + * @param reference the container to remove + * @param force if removal should be forced + * @throws IOException on IO error + */ + public void remove(ContainerReference reference, boolean force) throws IOException { + Assert.notNull(reference, "Reference must not be null"); + Collection params = force ? FORCE_PARAMS : Collections.emptySet(); + URI uri = buildUrl("/containers/" + reference, params); + http().delete(uri); + } + + } + + /** + * Docker API for volume operations. + */ + public class VolumeApi { + + VolumeApi() { + } + + /** + * Delete a volume. + * @param name the name of the volume to delete + * @param force if the deletion should be forced + * @throws IOException on IO error + */ + public void delete(VolumeName name, boolean force) throws IOException { + Assert.notNull(name, "Name must not be null"); + Collection params = force ? FORCE_PARAMS : Collections.emptySet(); + URI uri = buildUrl("/volumes/" + name, params); + http().delete(uri); + } + + } + + /** + * {@link UpdateListener} used to capture the image digest. + */ + private static class DigestCaptureUpdateListener implements UpdateListener { + + private static final String PREFIX = "Digest:"; + + private String digest; + + @Override + public void onUpdate(ProgressUpdateEvent event) { + String status = event.getStatus(); + if (status != null && status.startsWith(PREFIX)) { + String digest = status.substring(PREFIX.length()).trim(); + Assert.state(this.digest == null || this.digest.equals(digest), "Different digests IDs provided"); + this.digest = digest; + } + } + + String getCapturedDigest() { + Assert.hasText(this.digest, "No digest found"); + return this.digest; + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/DockerConnectionSocketFactory.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/DockerConnectionSocketFactory.java new file mode 100644 index 0000000000..ec9e1fab8d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/DockerConnectionSocketFactory.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; + +import com.sun.jna.Platform; +import org.apache.http.HttpHost; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.protocol.HttpContext; + +import org.springframework.boot.cloudnativebuildpack.socket.DomainSocket; +import org.springframework.boot.cloudnativebuildpack.socket.NamedPipeSocket; + +/** + * {@link ConnectionSocketFactory} that connects to the Docker domain socket or named + * pipe. + * + * @author Phillip Webb + */ +class DockerConnectionSocketFactory implements ConnectionSocketFactory { + + private static final String DOMAIN_SOCKET_PATH = "/var/run/docker.sock"; + + private static final String WINDOWS_NAMED_PIPE_PATH = "//./pipe/docker_engine"; + + @Override + public Socket createSocket(HttpContext context) throws IOException { + if (Platform.isWindows()) { + NamedPipeSocket.get(WINDOWS_NAMED_PIPE_PATH); + } + return DomainSocket.get(DOMAIN_SOCKET_PATH); + } + + @Override + public Socket connectSocket(int connectTimeout, Socket sock, HttpHost host, InetSocketAddress remoteAddress, + InetSocketAddress localAddress, HttpContext context) throws IOException { + return sock; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/DockerDnsResolver.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/DockerDnsResolver.java new file mode 100644 index 0000000000..3113d81e12 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/DockerDnsResolver.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +import org.apache.http.conn.DnsResolver; + +/** + * {@link DnsResolver} used by the {@link DockerHttpClientConnectionManager} to ensure + * only the loopback address is used. + * + * @author Phillip Webb + */ +class DockerDnsResolver implements DnsResolver { + + private static final InetAddress[] LOOPBACK = new InetAddress[] { InetAddress.getLoopbackAddress() }; + + @Override + public InetAddress[] resolve(String host) throws UnknownHostException { + return LOOPBACK; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/DockerException.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/DockerException.java new file mode 100644 index 0000000000..aea74d8b05 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/DockerException.java @@ -0,0 +1,82 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker; + +import java.net.URI; + +import org.springframework.util.Assert; + +/** + * Exception throw when the Docker API fails. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public class DockerException extends RuntimeException { + + private final int statusCode; + + private final String reasonPhrase; + + private final Errors errors; + + DockerException(URI uri, int statusCode, String reasonPhrase, Errors errors) { + super(buildMessage(uri, statusCode, reasonPhrase, errors)); + this.statusCode = statusCode; + this.reasonPhrase = reasonPhrase; + this.errors = errors; + } + + /** + * Return the status code returned by the Docker API. + * @return the statusCode the status code + */ + public int getStatusCode() { + return this.statusCode; + } + + /** + * Return the reason phrase returned by the Docker API error. + * @return the reasonPhrase + */ + public String getReasonPhrase() { + return this.reasonPhrase; + } + + /** + * Return the Errors from the body of the Docker API error, or {@code null} if the + * error JSON could not be read. + * @return the errors or {@code null} + */ + public Errors getErrors() { + return this.errors; + } + + private static String buildMessage(URI uri, int statusCode, String reasonPhrase, Errors errors) { + Assert.notNull(uri, "URI must not be null"); + StringBuilder message = new StringBuilder( + "Docker API call to '" + uri + "' failed with status code " + statusCode); + if (reasonPhrase != null && !reasonPhrase.isEmpty()) { + message.append(" \"" + reasonPhrase + "\""); + } + if (errors != null && !errors.isEmpty()) { + message.append(" " + errors); + } + return message.toString(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/DockerHttpClientConnectionManager.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/DockerHttpClientConnectionManager.java new file mode 100644 index 0000000000..74c4c38f05 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/DockerHttpClientConnectionManager.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker; + +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.HttpClientConnectionManager; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.impl.conn.BasicHttpClientConnectionManager; + +/** + * {@link HttpClientConnectionManager} for Docker. + * + * @author Phillip Webb + */ +class DockerHttpClientConnectionManager extends BasicHttpClientConnectionManager { + + DockerHttpClientConnectionManager() { + super(getRegistry(), null, null, new DockerDnsResolver()); + } + + private static Registry getRegistry() { + RegistryBuilder builder = RegistryBuilder.create(); + builder.register("docker", new DockerConnectionSocketFactory()); + return builder.build(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/DockerSchemePortResolver.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/DockerSchemePortResolver.java new file mode 100644 index 0000000000..e8b166dedb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/DockerSchemePortResolver.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker; + +import org.apache.http.HttpHost; +import org.apache.http.conn.SchemePortResolver; +import org.apache.http.conn.UnsupportedSchemeException; +import org.apache.http.util.Args; + +/** + * {@link SchemePortResolver} for Docker. + * + * @author Phillip Webb + */ +class DockerSchemePortResolver implements SchemePortResolver { + + private static int DEFAULT_DOCKER_PORT = 2376; + + @Override + public int resolve(HttpHost host) throws UnsupportedSchemeException { + Args.notNull(host, "HTTP host"); + String name = host.getSchemeName(); + if ("docker".equals(name)) { + return DEFAULT_DOCKER_PORT; + } + throw new UnsupportedSchemeException(name + " protocol is not supported"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/Errors.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/Errors.java new file mode 100644 index 0000000000..8618b2f022 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/Errors.java @@ -0,0 +1,106 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker; + +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Stream; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Errors returned from the Docker API. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public class Errors implements Iterable { + + private final List errors; + + @JsonCreator + Errors(@JsonProperty("errors") List errors) { + this.errors = (errors != null) ? errors : Collections.emptyList(); + } + + @Override + public Iterator iterator() { + return this.errors.iterator(); + } + + /** + * Returns a sequential {@code Stream} of the errors. + * @return a stream of the errors + */ + public Stream stream() { + return this.errors.stream(); + } + + /** + * Return if the there are any contained errors. + * @return if the errors are empty + */ + public boolean isEmpty() { + return this.errors.isEmpty(); + } + + @Override + public String toString() { + return this.errors.toString(); + } + + /** + * An individual Docker error. + */ + public static class Error { + + private final String code; + + private final String message; + + @JsonCreator + Error(String code, String message) { + this.code = code; + this.message = message; + } + + /** + * Return the error code. + * @return the error code + */ + public String getCode() { + return this.code; + } + + /** + * Return the error message. + * @return the error message + */ + public String getMessage() { + return this.message; + } + + @Override + public String toString() { + return this.code + ": " + this.message; + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/Http.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/Http.java new file mode 100644 index 0000000000..0a4f3671a1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/Http.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; + +import org.springframework.boot.cloudnativebuildpack.io.IOConsumer; + +/** + * HTTP transport used by the {@link DockerApi}. + * + * @author Phillip Webb + */ +interface Http { + + /** + * Perform a HTTP GET operation. + * @param uri the destination URI + * @return the operation response + * @throws IOException on IO error + */ + Response get(URI uri) throws IOException; + + /** + * Perform a HTTP POST operation. + * @param uri the destination URI + * @return the operation response + * @throws IOException on IO error + */ + Response post(URI uri) throws IOException; + + /** + * Perform a HTTP POST operation. + * @param uri the destination URI + * @param contentType the content type to write + * @param writer a content writer + * @return the operation response + * @throws IOException on IO error + */ + Response post(URI uri, String contentType, IOConsumer writer) throws IOException; + + /** + * Perform a HTTP PUT operation. + * @param uri the destination URI + * @param contentType the content type to write + * @param writer a content writer + * @return the operation response + * @throws IOException on IO error + */ + Response put(URI uri, String contentType, IOConsumer writer) throws IOException; + + /** + * Perform a HTTP DELETE operation. + * @param uri the destination URI + * @return the operation response + * @throws IOException on IO error + */ + Response delete(URI uri) throws IOException; + + /** + * An HTTP operation response. + */ + interface Response extends Closeable { + + /** + * Return the content of the response. + * @return the reseponse content + * @throws IOException on IO error + */ + InputStream getContent() throws IOException; + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/HttpClientHttp.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/HttpClientHttp.java new file mode 100644 index 0000000000..d7afcd177e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/HttpClientHttp.java @@ -0,0 +1,217 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpHeaders; +import org.apache.http.StatusLine; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.entity.AbstractHttpEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; + +import org.springframework.boot.cloudnativebuildpack.io.Content; +import org.springframework.boot.cloudnativebuildpack.io.IOConsumer; +import org.springframework.boot.cloudnativebuildpack.json.SharedObjectMapper; + +/** + * {@link Http} implementation backed by a {@link HttpClient}. + * + * @author Phillip Webb + */ +class HttpClientHttp implements Http { + + private final CloseableHttpClient client; + + HttpClientHttp() { + HttpClientBuilder builder = HttpClients.custom(); + builder.setConnectionManager(new DockerHttpClientConnectionManager()); + builder.setSchemePortResolver(new DockerSchemePortResolver()); + this.client = builder.build(); + } + + HttpClientHttp(CloseableHttpClient client) { + this.client = client; + } + + /** + * Perform a HTTP GET operation. + * @param uri the destination URI + * @return the operation response + * @throws IOException on IO error + */ + @Override + public Response get(URI uri) throws IOException { + return execute(new HttpGet(uri)); + } + + /** + * Perform a HTTP POST operation. + * @param uri the destination URI + * @return the operation response + * @throws IOException on IO error + */ + @Override + public Response post(URI uri) throws IOException { + return execute(new HttpPost(uri)); + } + + /** + * Perform a HTTP POST operation. + * @param uri the destination URI + * @param contentType the content type to write + * @param writer a content writer + * @return the operation response + * @throws IOException on IO error + */ + + @Override + public Response post(URI uri, String contentType, IOConsumer writer) throws IOException { + return execute(new HttpPost(uri), contentType, writer); + } + + /** + * Perform a HTTP PUT operation. + * @param uri the destination URI + * @param contentType the content type to write + * @param writer a content writer + * @return the operation response + * @throws IOException on IO error + */ + + @Override + public Response put(URI uri, String contentType, IOConsumer writer) throws IOException { + return execute(new HttpPut(uri), contentType, writer); + } + + /** + * Perform a HTTP DELETE operation. + * @param uri the destination URI + * @return the operation response + * @throws IOException on IO error + */ + + @Override + public Response delete(URI uri) throws IOException { + return execute(new HttpDelete(uri)); + } + + private Response execute(HttpEntityEnclosingRequestBase request, String contentType, + IOConsumer writer) throws IOException { + request.setHeader(HttpHeaders.CONTENT_TYPE, contentType); + request.setEntity(new WritableHttpEntity(writer)); + return execute(request); + } + + private Response execute(HttpUriRequest request) throws IOException { + CloseableHttpResponse response = this.client.execute(request); + StatusLine statusLine = response.getStatusLine(); + int statusCode = statusLine.getStatusCode(); + HttpEntity entity = response.getEntity(); + if (statusCode >= 200 && statusCode < 300) { + return new HttpClientResponse(response); + } + Errors errors = null; + if (statusCode >= 400 && statusCode < 500) { + try { + errors = SharedObjectMapper.get().readValue(entity.getContent(), Errors.class); + } + catch (Exception ex) { + } + } + EntityUtils.consume(entity); + throw new DockerException(request.getURI(), statusCode, statusLine.getReasonPhrase(), errors); + } + + /** + * {@link HttpEntity} to send {@link Content} content. + * + * @author Phillip Webb + */ + private class WritableHttpEntity extends AbstractHttpEntity { + + private final IOConsumer writer; + + WritableHttpEntity(IOConsumer writer) { + this.writer = writer; + } + + @Override + public boolean isRepeatable() { + return false; + } + + @Override + public long getContentLength() { + return -1; + } + + @Override + public InputStream getContent() throws IOException, UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + @Override + public void writeTo(OutputStream outputStream) throws IOException { + this.writer.accept(outputStream); + } + + @Override + public boolean isStreaming() { + return true; + } + + } + + /** + * An HTTP operation response. + */ + private static class HttpClientResponse implements Response { + + private final CloseableHttpResponse response; + + HttpClientResponse(CloseableHttpResponse response) { + this.response = response; + } + + @Override + public InputStream getContent() throws IOException { + return this.response.getEntity().getContent(); + } + + @Override + public void close() throws IOException { + this.response.close(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/LoadImageUpdateEvent.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/LoadImageUpdateEvent.java new file mode 100644 index 0000000000..33e8995fa1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/LoadImageUpdateEvent.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker; + +import com.fasterxml.jackson.annotation.JsonCreator; + +/** + * A {@link ProgressUpdateEvent} fired as an image is loaded. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public class LoadImageUpdateEvent extends ProgressUpdateEvent { + + private final String stream; + + @JsonCreator + public LoadImageUpdateEvent(String stream, String status, ProgressDetail progressDetail, String progress) { + super(status, progressDetail, progress); + this.stream = stream; + } + + /** + * Return the stream response or {@code null} if no response is available. + * @return the stream response. + */ + public String getStream() { + return this.stream; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/LogUpdateEvent.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/LogUpdateEvent.java new file mode 100644 index 0000000000..6849ced82b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/LogUpdateEvent.java @@ -0,0 +1,138 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.function.Consumer; +import java.util.regex.Pattern; + +/** + * An update event used to provide log updates. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public class LogUpdateEvent extends UpdateEvent { + + private static final Pattern ANSI_PATTERN = Pattern.compile("\u001B\\[[;\\d]*m"); + + private static final Pattern TRAILING_NEW_LINE_PATTERN = Pattern.compile("\\n$"); + + private final StreamType streamType; + + private final byte[] payload; + + private final String string; + + LogUpdateEvent(StreamType streamType, byte[] payload) { + this.streamType = streamType; + this.payload = payload; + String string = new String(payload, StandardCharsets.UTF_8); + string = ANSI_PATTERN.matcher(string).replaceAll(""); + string = TRAILING_NEW_LINE_PATTERN.matcher(string).replaceAll(""); + this.string = string; + } + + public void print() { + switch (this.streamType) { + case STD_OUT: + System.out.println(this); + return; + case STD_ERR: + System.err.println(this); + return; + } + } + + public StreamType getStreamType() { + return this.streamType; + } + + public byte[] getPayload() { + return this.payload; + } + + @Override + public String toString() { + return this.string; + } + + static void readAll(InputStream inputStream, Consumer consumer) throws IOException { + try { + LogUpdateEvent event; + while ((event = LogUpdateEvent.read(inputStream)) != null) { + consumer.accept(event); + } + } + finally { + inputStream.close(); + } + } + + private static LogUpdateEvent read(InputStream inputStream) throws IOException { + byte[] header = read(inputStream, 8); + if (header == null) { + return null; + } + StreamType streamType = StreamType.values()[header[0]]; + long size = 0; + for (int i = 0; i < 4; i++) { + size = (size << 8) + (header[i + 4] & 0xff); + } + byte[] payload = read(inputStream, size); + return new LogUpdateEvent(streamType, payload); + } + + private static byte[] read(InputStream inputStream, long size) throws IOException { + byte[] data = new byte[(int) size]; + int offset = 0; + do { + int amountRead = inputStream.read(data, offset, data.length - offset); + if (amountRead == -1) { + return null; + } + offset += amountRead; + } + while (offset < data.length); + return data; + } + + /** + * Stream types supported by the event. + */ + public enum StreamType { + + /** + * Input from {@code stdin}. + */ + STD_IN, + + /** + * Output to {@code stdout}. + */ + STD_OUT, + + /** + * Output to {@code stderr}. + */ + STD_ERR + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/ProgressUpdateEvent.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/ProgressUpdateEvent.java new file mode 100644 index 0000000000..da9adc1fdf --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/ProgressUpdateEvent.java @@ -0,0 +1,102 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker; + +import com.fasterxml.jackson.annotation.JsonCreator; + +/** + * An {@link UpdateEvent} that includes progress information. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public abstract class ProgressUpdateEvent extends UpdateEvent { + + private final String status; + + private final ProgressDetail progressDetail; + + private final String progress; + + protected ProgressUpdateEvent(String status, ProgressDetail progressDetail, String progress) { + this.status = status; + this.progressDetail = (ProgressDetail.isEmpty(progressDetail)) ? null : progressDetail; + this.progress = progress; + } + + /** + * Return the status for the update. For example, "Extracting" or "Downloading". + * @return the status of the update. + */ + public String getStatus() { + return this.status; + } + + /** + * Return progress details if available. + * @return progress details or {@code null} + */ + public ProgressDetail getProgressDetail() { + return this.progressDetail; + } + + /** + * Return a text based progress bar if progress information is available. + * @return the progress bar or {@code null} + */ + public String getProgress() { + return this.progress; + } + + /** + * Provide details about the progress of a task. + */ + public static class ProgressDetail { + + private final Integer current; + + private final Integer total; + + @JsonCreator + public ProgressDetail(Integer current, Integer total) { + this.current = current; + this.total = total; + } + + /** + * Return the current progress value. + * @return the current progress + */ + public int getCurrent() { + return this.current; + } + + /** + * Return the total progress possible value. + * @return the total progress possible + */ + public int getTotal() { + return this.total; + } + + public static boolean isEmpty(ProgressDetail progressDetail) { + return progressDetail == null || progressDetail.current == null || progressDetail.total == null; + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/PullImageUpdateEvent.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/PullImageUpdateEvent.java new file mode 100644 index 0000000000..1b0cc0b74e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/PullImageUpdateEvent.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker; + +import com.fasterxml.jackson.annotation.JsonCreator; + +/** + * A {@link ProgressUpdateEvent} fired as an image is pulled. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public class PullImageUpdateEvent extends ProgressUpdateEvent { + + private final String id; + + @JsonCreator + public PullImageUpdateEvent(String id, String status, ProgressDetail progressDetail, String progress) { + super(status, progressDetail, progress); + this.id = id; + } + + /** + * Return the ID of the layer being updated if available. + * @return the ID of the updated layer or {@code null} + */ + public String getId() { + return this.id; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/TotalProgressBar.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/TotalProgressBar.java new file mode 100644 index 0000000000..f4ae52adfd --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/TotalProgressBar.java @@ -0,0 +1,88 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker; + +import java.io.PrintStream; +import java.util.function.Consumer; + +/** + * Utility to render a simple progress bar based on consumed {@link TotalProgressEvent} + * objects. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public class TotalProgressBar implements Consumer { + + private final char progressChar; + + private final boolean bookend; + + private final PrintStream out; + + private int printed; + + /** + * Create a new {@link TotalProgressBar} instance. + * @param prefix the prefix to output + */ + public TotalProgressBar(String prefix) { + this(prefix, System.out); + } + + /** + * Create a new {@link TotalProgressBar} instance. + * @param prefix the prefix to output + * @param out the output print stream to use + */ + public TotalProgressBar(String prefix, PrintStream out) { + this(prefix, '#', true, out); + } + + /** + * Create a new {@link TotalProgressBar} instance. + * @param prefix the prefix to output + * @param progressChar the progress char to print + * @param bookend if bookends should be printed + * @param out the output print stream to use + */ + public TotalProgressBar(String prefix, char progressChar, boolean bookend, PrintStream out) { + this.progressChar = progressChar; + this.bookend = bookend; + if (prefix != null && prefix.length() > 0) { + out.print(prefix); + out.print(" "); + } + if (bookend) { + out.print("[ "); + } + this.out = out; + } + + @Override + public void accept(TotalProgressEvent event) { + int percent = event.getPercent() / 2; + while (this.printed < percent) { + this.out.print(this.progressChar); + this.printed++; + } + if (event.getPercent() == 100) { + this.out.println(this.bookend ? " ]" : ""); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/TotalProgressEvent.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/TotalProgressEvent.java new file mode 100644 index 0000000000..c541ad52f1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/TotalProgressEvent.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker; + +import org.springframework.util.Assert; + +/** + * Event published by the {@link TotalProgressPullListener} showing the total progress of + * an operation. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public class TotalProgressEvent { + + private final int percent; + + /** + * Create a new {@link TotalProgressEvent} with a specific percent value. + * @param percent the progress as a percentage + */ + public TotalProgressEvent(int percent) { + Assert.isTrue(percent >= 0 && percent <= 100, "Percent must be in the range 0 to 100"); + this.percent = percent; + } + + /** + * Return the total progress. + * @return the total progress + */ + public int getPercent() { + return this.percent; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/TotalProgressPullListener.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/TotalProgressPullListener.java new file mode 100644 index 0000000000..46148f38c8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/TotalProgressPullListener.java @@ -0,0 +1,142 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; + +import org.springframework.boot.cloudnativebuildpack.docker.ProgressUpdateEvent.ProgressDetail; + +/** + * {@link UpdateListener} that calculates the total progress of the entire pull operation + * and publishes {@link TotalProgressEvent}. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public class TotalProgressPullListener implements UpdateListener { + + private final Map layers = new ConcurrentHashMap<>(); + + private final Consumer consumer; + + private boolean progressStarted; + + /** + * Create a new {@link TotalProgressPullListener} that prints a progress bar to + * {@link System#out}. + * @param prefix the prefix to output + */ + public TotalProgressPullListener(String prefix) { + this(new TotalProgressBar(prefix)); + } + + /** + * Create a new {@link TotalProgressPullListener} that sends {@link TotalProgressEvent + * events} to the given consumer. + * @param consumer the consumer that receives {@link TotalProgressEvent progress + * events} + */ + public TotalProgressPullListener(Consumer consumer) { + this.consumer = consumer; + } + + @Override + public void onStart() { + } + + @Override + public void onUpdate(PullImageUpdateEvent event) { + if (event.getId() != null) { + this.layers.computeIfAbsent(event.getId(), Layer::new).update(event); + } + this.progressStarted = this.progressStarted || event.getProgress() != null; + if (this.progressStarted) { + publish(0); + } + } + + @Override + public void onFinish() { + this.layers.values().forEach(Layer::finish); + publish(100); + } + + private void publish(int fallback) { + int count = 0; + int total = 0; + for (Layer layer : this.layers.values()) { + count++; + total += layer.getProgress(); + } + TotalProgressEvent event = new TotalProgressEvent( + (count != 0) ? withinPercentageBounds(total / count) : fallback); + this.consumer.accept(event); + } + + private static int withinPercentageBounds(int value) { + if (value < 0) { + return 0; + } + if (value > 100) { + return 100; + } + return value; + } + + /** + * Progress for an individual layer. + */ + private static class Layer { + + private int downloadProgress; + + private int extractProgress; + + Layer(String id) { + } + + void update(PullImageUpdateEvent event) { + if (event.getProgressDetail() != null) { + ProgressDetail detail = event.getProgressDetail(); + if ("Downloading".equals(event.getStatus())) { + this.downloadProgress = updateProgress(this.downloadProgress, detail); + } + if ("Extracting".equals(event.getStatus())) { + this.extractProgress = updateProgress(this.extractProgress, detail); + } + } + } + + private int updateProgress(int current, ProgressDetail detail) { + int result = withinPercentageBounds((int) ((100.0 / detail.getTotal()) * detail.getCurrent())); + return (result > current) ? result : current; + } + + void finish() { + this.downloadProgress = 100; + this.extractProgress = 100; + } + + int getProgress() { + return withinPercentageBounds((this.downloadProgress + this.extractProgress) / 2); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/UpdateEvent.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/UpdateEvent.java new file mode 100644 index 0000000000..d46f488e62 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/UpdateEvent.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker; + +/** + * Base class for update events published by Docker. + * + * @author Phillip Webb + * @since 2.3.0 + * @see UpdateListener + */ +public abstract class UpdateEvent { + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/UpdateListener.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/UpdateListener.java new file mode 100644 index 0000000000..1a8e1a3e43 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/UpdateListener.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker; + +/** + * Listener for update events published from the {@link DockerApi}. + * + * @param the update event type + * @author Phillip Webb + * @since 2.3.0 + */ +@FunctionalInterface +public interface UpdateListener { + + /** + * A no-op update listener. + * @see #none() + */ + UpdateListener NONE = (event) -> { + }; + + /** + * Called when the operation starts. + */ + default void onStart() { + } + + /** + * Called when an update event is available. + * @param event the update event + */ + void onUpdate(E event); + + /** + * Called when the operation finishes (with or without error). + */ + default void onFinish() { + } + + /** + * A no-op update listener that does nothing. + * @param the event type + * @return a no-op update listener + */ + @SuppressWarnings("unchecked") + static UpdateListener none() { + return (UpdateListener) NONE; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/package-info.java new file mode 100644 index 0000000000..6aead8fda0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 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. + */ + +/** + * A limited Docker API providing the operations needed by pack. + */ +package org.springframework.boot.cloudnativebuildpack.docker; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/ContainerConfig.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/ContainerConfig.java new file mode 100644 index 0000000000..36ae3ce5bd --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/ContainerConfig.java @@ -0,0 +1,182 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker.type; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import org.springframework.boot.cloudnativebuildpack.json.SharedObjectMapper; +import org.springframework.util.Assert; +import org.springframework.util.StreamUtils; +import org.springframework.util.StringUtils; + +/** + * Configuration used when creating a new container. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public class ContainerConfig { + + private final String json; + + ContainerConfig(String user, ImageReference image, String command, List args, Map labels, + Map binds) throws IOException { + Assert.notNull(image, "Image must not be null"); + Assert.hasText(command, "Command must not be empty"); + ObjectMapper objectMapper = SharedObjectMapper.get(); + ObjectNode node = objectMapper.createObjectNode(); + if (StringUtils.hasText(user)) { + node.put("User", user); + } + node.put("Image", image.toString()); + ArrayNode commandNode = node.putArray("Cmd"); + commandNode.add(command); + args.forEach(commandNode::add); + ObjectNode labelsNode = node.putObject("Labels"); + labels.forEach(labelsNode::put); + ObjectNode hostConfigNode = node.putObject("HostConfig"); + ArrayNode bindsNode = hostConfigNode.putArray("Binds"); + binds.forEach((source, dest) -> bindsNode.add(source + ":" + dest)); + this.json = objectMapper.writeValueAsString(node); + } + + /** + * Write this container configuration to the specified {@link OutputStream}. + * @param outputStream the output stream + * @throws IOException on IO error + */ + public void writeTo(OutputStream outputStream) throws IOException { + StreamUtils.copy(this.json, StandardCharsets.UTF_8, outputStream); + } + + @Override + public String toString() { + return this.json; + } + + /** + * Factory method to create a {@link ContainerConfig} with specific settings. + * @param imageReference the source image for the container config + * @param update an update callback used to customize the config + * @return a new {@link ContainerConfig} instance + */ + public static ContainerConfig of(ImageReference imageReference, Consumer update) { + Assert.notNull(imageReference, "ImageReference must not be null"); + Assert.notNull(update, "Update must not be null"); + return new Update(imageReference).run(update); + } + + /** + * Update class used to change data when creating a container config. + */ + public static class Update { + + private final ImageReference image; + + private String user; + + private String command; + + private List args = new ArrayList<>(); + + private Map labels = new LinkedHashMap<>(); + + private Map binds = new LinkedHashMap<>(); + + Update(ImageReference image) { + this.image = image; + } + + private ContainerConfig run(Consumer update) { + update.accept(this); + try { + return new ContainerConfig(this.user, this.image, this.command, this.args, this.labels, this.binds); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + /** + * Update the container config with a specific user. + * @param user the user to set + */ + public void withUser(String user) { + this.user = user; + } + + /** + * Update the container config with a specific command. + * @param command the command to set + * @param args additional arguments to add + * @see #withArgs(String...) + */ + public void withCommand(String command, String... args) { + this.command = command; + withArgs(args); + } + + /** + * Update the container config with additional args. + * @param args the arguments to add + */ + public void withArgs(String... args) { + this.args.addAll(Arrays.asList(args)); + } + + /** + * Update the container config with an additional label. + * @param name the label name + * @param value the label value + */ + public void withLabel(String name, String value) { + this.labels.put(name, value); + } + + /** + * Update the container config with an additional bind. + * @param sourceVolume the source volume + * @param dest the bind destination + */ + public void withBind(VolumeName sourceVolume, String dest) { + this.binds.put(sourceVolume.toString(), dest); + } + + /** + * Update the container config with an additional bind. + * @param source the bind source + * @param dest the bind destination + */ + public void withBind(String source, String dest) { + this.binds.put(source, dest); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/ContainerContent.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/ContainerContent.java new file mode 100644 index 0000000000..c7ed460e34 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/ContainerContent.java @@ -0,0 +1,76 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker.type; + +import org.springframework.boot.cloudnativebuildpack.io.TarArchive; +import org.springframework.util.Assert; + +/** + * Additional content that can be written to a created container. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public interface ContainerContent { + + /** + * Return the actual content to be added. + * @return the content + */ + TarArchive getArchive(); + + /** + * Return the destination path where the content should be added. + * @return the destination path + */ + String getDestinationPath(); + + /** + * Factory method to create a new {@link ContainerContent} instance written to the + * root of the container. + * @param archive the archive to add + * @return a new {@link ContainerContent} instance + */ + static ContainerContent of(TarArchive archive) { + return of(archive, "/"); + } + + /** + * Factory method to create a new {@link ContainerContent} instance. + * @param archive the archive to add + * @param destinationPath the destination path within the container + * @return a new {@link ContainerContent} instance + */ + static ContainerContent of(TarArchive archive, String destinationPath) { + Assert.notNull(archive, "Archive must not be null"); + Assert.hasText(destinationPath, "DestinationPath must not be empty"); + return new ContainerContent() { + + @Override + public TarArchive getArchive() { + return archive; + } + + @Override + public String getDestinationPath() { + return destinationPath; + } + + }; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/ContainerReference.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/ContainerReference.java new file mode 100644 index 0000000000..f80bd807bc --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/ContainerReference.java @@ -0,0 +1,67 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker.type; + +import org.springframework.util.Assert; + +/** + * A reference to a Docker container. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public final class ContainerReference { + + private final String value; + + private ContainerReference(String value) { + Assert.hasText(value, "Value must not be empty"); + this.value = value; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ContainerReference other = (ContainerReference) obj; + return this.value.equals(other.value); + } + + @Override + public int hashCode() { + return this.value.hashCode(); + } + + @Override + public String toString() { + return this.value; + } + + /** + * Factory method to create a {@link ContainerReference} with a specific value. + * @param value the container reference value + * @return a new container reference instance + */ + public static ContainerReference of(String value) { + return new ContainerReference(value); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/Image.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/Image.java new file mode 100644 index 0000000000..0d0c3f329a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/Image.java @@ -0,0 +1,114 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker.type; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.invoke.MethodHandles; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.JsonNode; + +import org.springframework.boot.cloudnativebuildpack.json.MappedObject; + +/** + * Image details as returned from {@code Docker inspect}. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public class Image extends MappedObject { + + private final List digests; + + private final ImageConfig config; + + private List layers; + + private final String os; + + Image(JsonNode node) { + super(node, MethodHandles.lookup()); + this.digests = getDigests(getNode().at("/RepoDigests")); + this.config = new ImageConfig(getNode().at("/Config")); + this.layers = extractLayers(valueAt("/RootFS/Layers", String[].class)); + this.os = valueAt("/Os", String.class); + } + + private List getDigests(JsonNode node) { + if (node.isEmpty()) { + return Collections.emptyList(); + } + List digests = new ArrayList<>(); + node.forEach((child) -> digests.add(child.asText())); + return Collections.unmodifiableList(digests); + } + + private List extractLayers(String[] layers) { + if (layers == null) { + return Collections.emptyList(); + } + return Collections.unmodifiableList(Arrays.stream(layers).map(LayerId::of).collect(Collectors.toList())); + } + + /** + * Return the digests of the image. + * @return the image digests + */ + public List getDigests() { + return this.digests; + } + + /** + * Return image config information. + * @return the image config + */ + public ImageConfig getConfig() { + return this.config; + } + + /** + * Return the layer IDs contained in the image. + * @return the layer IDs. + */ + public List getLayers() { + return this.layers; + } + + /** + * Return the OS of the image. + * @return the image OS + */ + public String getOs() { + return (this.os != null) ? this.os : "linux"; + } + + /** + * Create a new {@link Image} instance from the specified JSON content. + * @param content the JSON content + * @return a new {@link Image} instace + * @throws IOException on IO error + */ + public static Image of(InputStream content) throws IOException { + return of(content, Image::new); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/ImageArchive.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/ImageArchive.java new file mode 100644 index 0000000000..ef043f8117 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/ImageArchive.java @@ -0,0 +1,293 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker.type; + +import java.io.IOException; +import java.io.OutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import org.springframework.boot.cloudnativebuildpack.io.Content; +import org.springframework.boot.cloudnativebuildpack.io.IOConsumer; +import org.springframework.boot.cloudnativebuildpack.io.InspectedContent; +import org.springframework.boot.cloudnativebuildpack.io.Layout; +import org.springframework.boot.cloudnativebuildpack.io.Owner; +import org.springframework.boot.cloudnativebuildpack.io.TarArchive; +import org.springframework.boot.cloudnativebuildpack.json.SharedObjectMapper; +import org.springframework.util.Assert; + +/** + * An image archive that can be loaded into Docker. + * + * @author Phillip Webb + * @since 2.3.0 + * @see #from(Image, IOConsumer) + * @see Docker Image + * Specification + */ +public class ImageArchive implements TarArchive { + + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ISO_ZONED_DATE_TIME + .withZone(ZoneOffset.UTC); + + private static final IOConsumer NO_UPDDATES = (update) -> { + }; + + private final ObjectMapper objectMapper; + + private final ImageConfig imageConfig; + + private final Instant createDate; + + private final ImageReference tag; + + private final String os; + + private final List existingLayers; + + private final List newLayers; + + ImageArchive(ObjectMapper objectMapper, ImageConfig imageConfig, Instant createDate, ImageReference tag, String os, + List existingLayers, List newLayers) { + this.objectMapper = objectMapper; + this.imageConfig = imageConfig; + this.createDate = createDate; + this.tag = tag; + this.os = os; + this.existingLayers = existingLayers; + this.newLayers = newLayers; + } + + /** + * Return the image config for the archive. + * @return the image config + */ + public ImageConfig getImageConfig() { + return this.imageConfig; + } + + /** + * Return the create data of the archive. + * @return the create date + */ + public Instant getCreateDate() { + return this.createDate; + } + + /** + * Return the tag of the archive. + * @return the tag + */ + public ImageReference getTag() { + return this.tag; + } + + @Override + public void writeTo(OutputStream outputStream) throws IOException { + TarArchive.of(this::write).writeTo(outputStream); + } + + private void write(Layout writer) throws IOException { + List writtenLayers = writeLayers(writer); + String config = writeConfig(writer, writtenLayers); + writeManifest(writer, config, writtenLayers); + } + + private List writeLayers(Layout writer) throws IOException { + List writtenLayers = new ArrayList<>(); + for (Layer layer : this.newLayers) { + writtenLayers.add(writeLayer(writer, layer)); + } + return Collections.unmodifiableList(writtenLayers); + } + + private LayerId writeLayer(Layout writer, Layer layer) throws IOException { + LayerId id = layer.getId(); + writer.file("/" + id.getHash() + ".tar", Owner.ROOT, layer); + return id; + } + + private String writeConfig(Layout writer, List writtenLayers) throws IOException { + try { + ObjectNode config = createConfig(writtenLayers); + String json = this.objectMapper.writeValueAsString(config); + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + InspectedContent content = InspectedContent.of(Content.of(json), digest::update); + String name = "/" + LayerId.ofSha256Digest(digest.digest()).getHash() + ".json"; + writer.file(name, Owner.ROOT, content); + return name; + } + catch (NoSuchAlgorithmException ex) { + throw new IllegalStateException(ex); + } + } + + private ObjectNode createConfig(List writtenLayers) { + ObjectNode config = this.objectMapper.createObjectNode(); + config.set("config", this.imageConfig.getNodeCopy()); + config.set("created", config.textNode(getCreatedDate())); + config.set("history", createHistory(writtenLayers)); + config.set("os", config.textNode(this.os)); + config.set("rootfs", createRootFs(writtenLayers)); + return config; + } + + private String getCreatedDate() { + return DATE_FORMATTER.format(this.createDate); + } + + private JsonNode createHistory(List writtenLayers) { + ArrayNode history = this.objectMapper.createArrayNode(); + int size = this.existingLayers.size() + writtenLayers.size(); + for (int i = 0; i < size; i++) { + history.addObject(); + } + return history; + } + + private JsonNode createRootFs(List writtenLayers) { + ObjectNode rootFs = this.objectMapper.createObjectNode(); + ArrayNode diffIds = rootFs.putArray("diff_ids"); + this.existingLayers.stream().map(Object::toString).forEach(diffIds::add); + writtenLayers.stream().map(Object::toString).forEach(diffIds::add); + return rootFs; + } + + private void writeManifest(Layout writer, String config, List writtenLayers) throws IOException { + ArrayNode manifest = createManifest(config, writtenLayers); + String manifestJson = this.objectMapper.writeValueAsString(manifest); + writer.file("/manifest.json", Owner.ROOT, Content.of(manifestJson)); + } + + private ArrayNode createManifest(String config, List writtenLayers) { + ArrayNode manifest = this.objectMapper.createArrayNode(); + ObjectNode entry = manifest.addObject(); + entry.set("Config", entry.textNode(config)); + entry.set("Layers", getManfiestLayers(writtenLayers)); + if (this.tag != null) { + entry.set("RepoTags", entry.arrayNode().add(this.tag.toString())); + } + return manifest; + } + + private ArrayNode getManfiestLayers(List writtenLayers) { + ArrayNode layers = this.objectMapper.createArrayNode(); + for (int i = 0; i < this.existingLayers.size(); i++) { + layers.add(""); + } + writtenLayers.stream().map((id) -> id.getHash() + ".tar").forEach(layers::add); + return layers; + } + + /** + * Create a new {@link ImageArchive} based on an existing {@link Image}. + * @param image the image that this archive is based on + * @return the new image archive. + * @throws IOException on IO error + */ + public static ImageArchive from(Image image) throws IOException { + return from(image, NO_UPDDATES); + } + + /** + * Create a new {@link ImageArchive} based on an existing {@link Image}. + * @param image the image that this archive is based on + * @param update consumer to apply updates + * @return the new image archive. + * @throws IOException on IO error + */ + public static ImageArchive from(Image image, IOConsumer update) throws IOException { + return new Update(image).applyTo(update); + } + + /** + * Update class used to change data when creating an image archive. + */ + public static final class Update { + + private final Image image; + + private ImageConfig config; + + private Instant createDate; + + private ImageReference tag; + + private final List newLayers = new ArrayList<>(); + + private Update(Image image) { + this.image = image; + this.config = image.getConfig(); + } + + private ImageArchive applyTo(IOConsumer update) throws IOException { + update.accept(this); + Instant createDate = (this.createDate != null) ? this.createDate : Instant.now(); + return new ImageArchive(SharedObjectMapper.get(), this.config, createDate, this.tag, this.image.getOs(), + this.image.getLayers(), Collections.unmodifiableList(this.newLayers)); + } + + /** + * Apply updates to the {@link ImageConfig}. + * @param update consumer to apply updates + */ + public void withUpdatedConfig(Consumer update) { + this.config = this.config.copy(update); + } + + /** + * Add a new layer to the image archive. + * @param layer the layer to add + */ + public void withNewLayer(Layer layer) { + Assert.notNull(layer, "Layer must not be null"); + this.newLayers.add(layer); + } + + /** + * Set the create date for the image archive. + * @param createDate the create date + */ + public void withCreateDate(Instant createDate) { + Assert.notNull(createDate, "CreateDate must not be null"); + this.createDate = createDate; + } + + /** + * Set the tag for the image archive. + * @param tag the tag + */ + public void withTag(ImageReference tag) { + Assert.notNull(tag, "Tag must not be null"); + this.tag = tag.inTaggedForm(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/ImageConfig.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/ImageConfig.java new file mode 100644 index 0000000000..2ac1e451af --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/ImageConfig.java @@ -0,0 +1,122 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker.type; + +import java.lang.invoke.MethodHandles; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Consumer; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import org.springframework.boot.cloudnativebuildpack.json.MappedObject; + +/** + * Image configuration information. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public class ImageConfig extends MappedObject { + + private Map labels; + + private final Map configEnv; + + @SuppressWarnings("unchecked") + ImageConfig(JsonNode node) { + super(node, MethodHandles.lookup()); + this.labels = valueAt("/Labels", Map.class); + this.configEnv = parseConfigEnv(); + } + + private Map parseConfigEnv() { + Map env = new LinkedHashMap<>(); + String[] entries = valueAt("/Env", String[].class); + for (String entry : entries) { + int i = entry.indexOf('='); + String name = (i != -1) ? entry.substring(0, i) : entry; + String value = (i != -1) ? entry.substring(i + 1) : null; + env.put(name, value); + } + return Collections.unmodifiableMap(env); + } + + JsonNode getNodeCopy() { + return super.getNode().deepCopy(); + } + + /** + * Return the image labels. + * @return the image labels + */ + public Map getLabels() { + return this.labels; + } + + /** + * Return the image environment variables. + * @return the env + */ + public Map getEnv() { + return this.configEnv; + } + + /** + * Create an updated copy of this image config. + * @param update consumer to apply updates + * @return an updated image config + */ + public ImageConfig copy(Consumer update) { + return new Update(this).run(update); + + } + + /** + * Update class used to change data when creating a copy. + */ + public static final class Update { + + private ObjectNode copy; + + private Update(ImageConfig source) { + this.copy = source.getNode().deepCopy(); + } + + private ImageConfig run(Consumer update) { + update.accept(this); + return new ImageConfig(this.copy); + } + + /** + * Update the image config with an additional label. + * @param label the label name + * @param value the label value + */ + public void withLabel(String label, String value) { + JsonNode labels = this.copy.at("/Labels"); + if (labels.isMissingNode()) { + labels = this.copy.putObject("Labels"); + } + ((ObjectNode) labels).put(label, value); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/ImageName.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/ImageName.java new file mode 100644 index 0000000000..f2eaa129b5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/ImageName.java @@ -0,0 +1,137 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker.type; + +import org.springframework.util.Assert; + +/** + * A Docker image name of the form {@literal "docker.io/library/ubuntu"}. + * + * @author Phillip Webb + * @since 2.3.0 + * @see ImageReference + * @see #of(String) + */ +public class ImageName { + + private static final String DEFAULT_DOMAIN = "docker.io"; + + private static final String OFFICAL_REPOSITORY_NAME = "library"; + + private static final String LEGACY_DOMAIN = "index.docker.io"; + + private final String domain; + + private final String name; + + private final String string; + + ImageName(String domain, String name) { + Assert.hasText(domain, "Domain must not be empty"); + Assert.hasText(name, "Name must not be empty"); + this.domain = domain; + this.name = name; + this.string = domain + "/" + name; + } + + /** + * Return the domain for this image name. + * @return the domain + */ + public String getDomain() { + return this.domain; + } + + /** + * Return the name of this image. + * @return the image name + */ + public String getName() { + return this.name; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ImageName other = (ImageName) obj; + boolean result = true; + result = result && this.domain.equals(other.domain); + result = result && this.name.equals(other.name); + return result; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + this.domain.hashCode(); + result = prime * result + this.name.hashCode(); + return result; + } + + @Override + public String toString() { + return this.string; + } + + public String toLegacyString() { + if (DEFAULT_DOMAIN.equals(this.domain)) { + return LEGACY_DOMAIN + "/" + this.name; + } + return this.string; + } + + /** + * Create a new {@link ImageName} from the given value. The following value forms can + * be used: + *

    + *
  • {@code name} (maps to {@code docker.io/library/name})
  • + *
  • {@code domain/name}
  • + *
  • {@code domain:port/name}
  • + *
+ * @param value the value to parse + * @return an {@link ImageName} instance + */ + public static ImageName of(String value) { + String[] split = split(value); + return new ImageName(split[0], split[1]); + } + + static String[] split(String value) { + Assert.hasText(value, "Value must not be empty"); + String domain = DEFAULT_DOMAIN; + int firstSlash = value.indexOf('/'); + if (firstSlash != -1) { + String firstSegment = value.substring(0, firstSlash); + if (firstSegment.contains(".") || firstSegment.contains(":") || "localhost".equals(firstSegment)) { + domain = LEGACY_DOMAIN.equals(firstSegment) ? DEFAULT_DOMAIN : firstSegment; + value = value.substring(firstSlash + 1); + } + } + if (DEFAULT_DOMAIN.equals(domain) && !value.contains("/")) { + value = OFFICAL_REPOSITORY_NAME + "/" + value; + } + return new String[] { domain, value }; + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/ImageReference.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/ImageReference.java new file mode 100644 index 0000000000..c53749e455 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/ImageReference.java @@ -0,0 +1,268 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker.type; + +import java.io.File; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * A reference to a Docker image of the form {@code "imagename[:tag|@digest]"}. + * + * @author Phillip Webb + * @since 2.3.0 + * @see ImageName + * @see How + * are Docker image names parsed? + */ +public final class ImageReference { + + private static final String LATEST = "latest"; + + private static final Pattern TRAILING_VERSION_PATTERN = Pattern.compile("^(.*)(\\-\\d+)$"); + + private final ImageName name; + + private final String tag; + + private final String digest; + + private final String string; + + private ImageReference(ImageName name, String tag, String digest) { + Assert.notNull(name, "Name must not be null"); + this.name = name; + this.tag = tag; + this.digest = digest; + this.string = buildString(name.toString(), tag, digest); + } + + /** + * Return the domain for this image name. + * @return the domain + * @see ImageName#getDomain() + */ + public String getDomain() { + return this.name.getDomain(); + } + + /** + * Return the name of this image. + * @return the image name + * @see ImageName#getName() + */ + public String getName() { + return this.name.getName(); + } + + /** + * Return the tag from the reference or {@code null}. + * @return the referenced tag + */ + public String getTag() { + return this.tag; + } + + /** + * Return the digest from the reference or {@code null}. + * @return the referenced digest + */ + public String getDigest() { + return this.digest; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ImageReference other = (ImageReference) obj; + boolean result = true; + result = result && this.name.equals(other.name); + result = result && ObjectUtils.nullSafeEquals(this.tag, other.tag); + result = result && ObjectUtils.nullSafeEquals(this.digest, other.digest); + return result; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + this.name.hashCode(); + result = prime * result + ObjectUtils.nullSafeHashCode(this.tag); + result = prime * result + ObjectUtils.nullSafeHashCode(this.digest); + return result; + } + + @Override + public String toString() { + return this.string; + } + + public String toLegacyString() { + return buildString(this.name.toLegacyString(), this.tag, this.digest); + } + + private String buildString(String name, String tag, String digest) { + StringBuilder string = new StringBuilder(name); + if (tag != null) { + string.append(":").append(tag); + } + if (digest != null) { + string.append("@").append(digest); + } + return string.toString(); + } + + /** + * Create a new {@link ImageReference} with an updated digest. + * @param digest the new digest + * @return an updated image reference + */ + public ImageReference withDigest(String digest) { + return new ImageReference(this.name, null, digest); + } + + /** + * Return an {@link ImageReference} in the form {@code "imagename:tag"}. If the tag + * has not been defined then {@code latest} is used. + * @return the image reference in tagged form + * @throws IllegalStateException if the image reference contains a digest + */ + public ImageReference inTaggedForm() { + Assert.state(this.digest == null, "Image reference '" + this + "' cannot contain a digest"); + return new ImageReference(this.name, (this.tag != null) ? this.tag : LATEST, this.digest); + } + + /** + * Create a new {@link ImageReference} instance deduced from a source JAR file that + * follows common Java naming conventions. + * @param jarFile the source jar file + * @return an {@link ImageName} for the jar file. + */ + public static ImageReference forJarFile(File jarFile) { + String filename = jarFile.getName(); + Assert.isTrue(filename.toLowerCase().endsWith(".jar"), "File '" + jarFile + "' is not a JAR"); + filename = filename.substring(0, filename.length() - 4); + int firstDot = filename.indexOf('.'); + if (firstDot == -1) { + return ImageReference.of(filename); + } + String name = filename.substring(0, firstDot); + String version = filename.substring(firstDot + 1); + Matcher matcher = TRAILING_VERSION_PATTERN.matcher(name); + if (matcher.matches()) { + name = matcher.group(1); + version = matcher.group(2).substring(1) + "." + version; + } + return of(ImageName.of(name), version); + } + + /** + * Generate an image name with a random suffix. + * @param prefix the name prefix + * @return a random image reference + */ + public static ImageReference random(String prefix) { + return ImageReference.random(prefix, 10); + } + + /** + * Generate an image name with a random suffix. + * @param prefix the name prefix + * @param randomLength the number of chars in the random part of the name + * @return a random image reference + */ + public static ImageReference random(String prefix, int randomLength) { + return of(RandomString.generate(prefix, randomLength)); + } + + /** + * Create a new {@link ImageReference} from the given value. The following value forms + * can be used: + *
    + *
  • {@code name} (maps to {@code docker.io/library/name})
  • + *
  • {@code domain/name}
  • + *
  • {@code domain:port/name}
  • + *
  • {@code domain:port/name:tag}
  • + *
  • {@code domain:port/name@digest}
  • + *
+ * @param value the value to parse + * @return an {@link ImageName} instance + */ + public static ImageReference of(String value) { + Assert.hasText(value, "Value must not be null"); + String[] domainAndValue = ImageName.split(value); + return of(domainAndValue[0], domainAndValue[1]); + } + + /** + * Create a new {@link ImageReference} from the given {@link ImageName}. + * @param name the image name + * @return a new image reference + */ + public static ImageReference of(ImageName name) { + return new ImageReference(name, null, null); + } + + /** + * Create a new {@link ImageReference} from the given {@link ImageName} and tag. + * @param name the image name + * @param tag the referenced tag + * @return a new image reference + */ + public static ImageReference of(ImageName name, String tag) { + return new ImageReference(name, tag, null); + } + + /** + * Create a new {@link ImageReference} from the given {@link ImageName}, tag and + * digest. + * @param name the image name + * @param tag the referenced tag + * @param digest the referenced digest + * @return a new image reference + */ + public static ImageReference of(ImageName name, String tag, String digest) { + return new ImageReference(name, tag, digest); + } + + private static ImageReference of(String domain, String value) { + String digest = null; + int lastAt = value.indexOf('@'); + if (lastAt != -1) { + digest = value.substring(lastAt + 1); + value = value.substring(0, lastAt); + } + String tag = null; + int firstColon = value.indexOf(':'); + if (firstColon != -1) { + tag = value.substring(firstColon + 1); + value = value.substring(0, firstColon); + } + ImageName name = new ImageName(domain, value); + return new ImageReference(name, tag, digest); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/Layer.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/Layer.java new file mode 100644 index 0000000000..2bf4ae0436 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/Layer.java @@ -0,0 +1,94 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker.type; + +import java.io.IOException; +import java.io.OutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import org.springframework.boot.cloudnativebuildpack.io.Content; +import org.springframework.boot.cloudnativebuildpack.io.IOConsumer; +import org.springframework.boot.cloudnativebuildpack.io.InspectedContent; +import org.springframework.boot.cloudnativebuildpack.io.Layout; +import org.springframework.boot.cloudnativebuildpack.io.TarArchive; +import org.springframework.util.Assert; + +/** + * A layer that can be written to an {@link ImageArchive}. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public class Layer implements Content { + + private final Content content; + + private final LayerId id; + + Layer(TarArchive tarArchive) throws NoSuchAlgorithmException, IOException { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + this.content = InspectedContent.of(tarArchive::writeTo, digest::update); + this.id = LayerId.ofSha256Digest(digest.digest()); + } + + /** + * Return the ID of the layer. + * @return the layer ID + */ + public LayerId getId() { + return this.id; + } + + @Override + public int size() { + return this.content.size(); + } + + @Override + public void writeTo(OutputStream outputStream) throws IOException { + this.content.writeTo(outputStream); + } + + /** + * Factory method to create a new {@link Layer} with a specific {@link Layout}. + * @param layout the layer layout + * @return a new layer instance + * @throws IOException on IO error + */ + public static Layer of(IOConsumer layout) throws IOException { + Assert.notNull(layout, "Layout must not be null"); + return fromTarArchive(TarArchive.of(layout)); + } + + /** + * Factory method to create a new {@link Layer} from a {@link TarArchive}. + * @param tarArchive the contents of the layer + * @return a new layer instance + * @throws IOException on error + */ + public static Layer fromTarArchive(TarArchive tarArchive) throws IOException { + Assert.notNull(tarArchive, "TarArchive must not be null"); + try { + return new Layer(tarArchive); + } + catch (NoSuchAlgorithmException ex) { + throw new IllegalStateException(ex); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/LayerId.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/LayerId.java new file mode 100644 index 0000000000..be089d83a2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/LayerId.java @@ -0,0 +1,105 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker.type; + +import java.math.BigInteger; + +import org.springframework.util.Assert; + +/** + * A layer ID as used inside a Docker image of the form {@code algorithm: hash}. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public final class LayerId { + + private final String value; + + private final String algorithm; + + private final String hash; + + private LayerId(String value, String algorithm, String hash) { + this.value = value; + this.algorithm = algorithm; + this.hash = hash; + } + + /** + * Return the algorithm of layer. + * @return the algorithm + */ + public String getAlgorithm() { + return this.algorithm; + } + + /** + * Return the hash of the layer. + * @return the layer hash + */ + public String getHash() { + return this.hash; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + return this.value.equals(((LayerId) obj).value); + } + + @Override + public int hashCode() { + return this.value.hashCode(); + } + + @Override + public String toString() { + return this.value; + } + + /** + * Create a new {@link LayerId} with the specified value. + * @param value the layer ID value of the form {@code algorithm: hash} + * @return a new layer ID instance + */ + public static LayerId of(String value) { + Assert.hasText(value, "Value must not be empty"); + int i = value.indexOf(':'); + Assert.isTrue(i >= 0, "Invalid layer ID '" + value + "'"); + return new LayerId(value, value.substring(0, i), value.substring(i + 1)); + } + + /** + * Create a new {@link LayerId} from a SHA-256 digest. + * @param digest the digest + * @return a new layer ID instance + */ + public static LayerId ofSha256Digest(byte[] digest) { + Assert.notNull(digest, "Digest must not be null"); + Assert.isTrue(digest.length == 32, "Digest must be exactly 32 bytes"); + String algorithm = "sha256"; + String hash = String.format("%32x", new BigInteger(1, digest)); + return new LayerId(algorithm + ":" + hash, algorithm, hash); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/RandomString.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/RandomString.java new file mode 100644 index 0000000000..37880609c8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/RandomString.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker.type; + +import java.util.Random; +import java.util.stream.IntStream; + +import org.springframework.util.Assert; + +/** + * Utility class used to generate random strings. + * + * @author Phillip Webb + */ +final class RandomString { + + private static final Random random = new Random(); + + private RandomString() { + } + + static String generate(String prefix, int randomLength) { + Assert.notNull(prefix, "Prefix must not be null"); + return prefix + generateRandom(randomLength); + } + + static CharSequence generateRandom(int length) { + IntStream chars = random.ints('a', 'z' + 1).limit(length); + return chars.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/VolumeName.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/VolumeName.java new file mode 100644 index 0000000000..c42728e306 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/VolumeName.java @@ -0,0 +1,143 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker.type; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.function.Function; + +import org.springframework.util.Assert; + +/** + * A Docker volume name. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public final class VolumeName { + + private final String value; + + private VolumeName(String value) { + this.value = value; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + return this.value.equals(((VolumeName) obj).value); + } + + @Override + public int hashCode() { + return this.value.hashCode(); + } + + @Override + public String toString() { + return this.value; + } + + /** + * Factory method to create a new {@link VolumeName} with a random name. + * @param prefix the prefix to use with the random name + * @return a randomly named volume + */ + public static VolumeName random(String prefix) { + return random(prefix, 10); + } + + /** + * Factory method to create a new {@link VolumeName} with a random name. + * @param prefix the prefix to use with the random name + * @param randomLength the number of chars in the random part of the name + * @return a randomly volume reference + */ + public static VolumeName random(String prefix, int randomLength) { + return of(RandomString.generate(prefix, randomLength)); + } + + /** + * Factory method to create a new {@link VolumeName} based on an object. The resulting + * name will be based off a SHA-256 digest of the given object's {@code toString()} + * method. + * @param the source object type + * @param source the source object + * @param prefix the prefix to use with the volume name + * @param suffix the suffix to use with the volume name + * @param digestLength the number of chars in the digest part of the name + * @return a name based off the image reference + */ + public static VolumeName basedOn(S source, String prefix, String suffix, int digestLength) { + return basedOn(source, Object::toString, prefix, suffix, digestLength); + } + + /** + * Factory method to create a new {@link VolumeName} based on an object. The resulting + * name will be based off a SHA-256 digest of the given object's name. + * @param the source object type + * @param source the source object + * @param nameExtractor a method to extract the name of the object + * @param prefix the prefix to use with the volume name + * @param suffix the suffix to use with the volume name + * @param digestLength the number of chars in the digest part of the name + * @return a name based off the image reference + */ + public static VolumeName basedOn(S source, Function nameExtractor, String prefix, String suffix, + int digestLength) { + Assert.notNull(source, "Source must not be null"); + Assert.notNull(nameExtractor, "NameExtractor must not be null"); + Assert.notNull(prefix, "Prefix must not be null"); + Assert.notNull(suffix, "Suffix must not be null"); + return of(prefix + getDigest(nameExtractor.apply(source), digestLength) + suffix); + } + + private static String getDigest(String name, int length) { + try { + MessageDigest digest = MessageDigest.getInstance("sha-256"); + return asHexString(digest.digest(name.getBytes(StandardCharsets.UTF_8)), length); + } + catch (NoSuchAlgorithmException ex) { + throw new IllegalStateException(ex); + } + } + + private static String asHexString(byte[] digest, int digestLength) { + Assert.isTrue(digestLength <= digest.length, "DigestLength must be less than or equal to " + digest.length); + byte[] shortDigest = new byte[6]; + System.arraycopy(digest, 0, shortDigest, 0, shortDigest.length); + return String.format("%0" + digestLength + "x", new BigInteger(1, shortDigest)); + } + + /** + * Factory method to create a {@link VolumeName} with a specific value. + * @param value the volme reference value + * @return a new {@link VolumeName} instance + */ + public static VolumeName of(String value) { + Assert.notNull(value, "Value must not be null"); + return new VolumeName(value); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/package-info.java new file mode 100644 index 0000000000..20c6549d2c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/docker/type/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 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. + */ + +/** + * Docker types. + */ +package org.springframework.boot.cloudnativebuildpack.docker.type; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/Content.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/Content.java new file mode 100644 index 0000000000..39ffc5ac0d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/Content.java @@ -0,0 +1,101 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.io; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +import org.springframework.util.Assert; +import org.springframework.util.FileCopyUtils; + +/** + * Content with a known size that can be written to an {@link OutputStream}. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public interface Content { + + /** + * The size of the content in bytes. + * @return the content size + */ + int size(); + + /** + * Write the content to the given output stream. + * @param outputStream the output stream to write to + * @throws IOException on IO error + */ + void writeTo(OutputStream outputStream) throws IOException; + + /** + * Create a new {@link Content} from the given UTF-8 string. + * @param string the string to write + * @return a new {@link Content} instance + */ + static Content of(String string) { + Assert.notNull(string, "String must not be null"); + return of(string.getBytes(StandardCharsets.UTF_8)); + } + + /** + * Create a new {@link Content} from the given input stream. + * @param bytes the bytes to write + * @return a new {@link Content} instance + */ + static Content of(byte[] bytes) { + Assert.notNull(bytes, "Bytes must not be null"); + return of(bytes.length, () -> new ByteArrayInputStream(bytes)); + } + + static Content of(File file) { + Assert.notNull(file, "File must not be null"); + return of((int) file.length(), () -> new FileInputStream(file)); + } + + /** + * Create a new {@link Content} from the given input stream. The stream will be closed + * after it has been written. + * @param size the size of the supplied input stream + * @param supplier the input stream supplier + * @return a new {@link Content} instance + */ + static Content of(int size, IOSupplier supplier) { + Assert.isTrue(size >= 0, "Size must not be negative"); + Assert.notNull(supplier, "Supplier must not be null"); + return new Content() { + + @Override + public int size() { + return size; + } + + @Override + public void writeTo(OutputStream outputStream) throws IOException { + FileCopyUtils.copy(supplier.get(), outputStream); + } + + }; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/DefaultOwner.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/DefaultOwner.java new file mode 100644 index 0000000000..cc49597ee1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/DefaultOwner.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.io; + +/** + * Default {@link Owner} implementation. + * + * @author Phillip Webb + * @see Owner#of(long, long) + */ +class DefaultOwner implements Owner { + + private final long uid; + + private final long gid; + + DefaultOwner(long uid, long gid) { + this.uid = uid; + this.gid = gid; + } + + @Override + public long getUid() { + return this.uid; + } + + @Override + public long getGid() { + return this.gid; + } + + @Override + public String toString() { + return this.uid + "/" + this.gid; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/IOConsumer.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/IOConsumer.java new file mode 100644 index 0000000000..9ca8f891da --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/IOConsumer.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.io; + +import java.io.IOException; + +/** + * Consumer that can safely throw {@link IOException IO exceptions}. + * + * @param the consumed type + * @author Phillip Webb + * @since 2.3.0 + */ +@FunctionalInterface +public interface IOConsumer { + + /** + * Performs this operation on the given argument. + * @param t the instance to consume + * @throws IOException on IO error + */ + void accept(T t) throws IOException; + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/IOSupplier.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/IOSupplier.java new file mode 100644 index 0000000000..20b71ea994 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/IOSupplier.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.io; + +import java.io.IOException; + +/** + * Supplier that can safely throw {@link IOException IO exceptions}. + * + * @param the supplied type + * @author Phillip Webb + * @since 2.3.0 + */ +@FunctionalInterface +public interface IOSupplier { + + /** + * Gets the supplied value. + * @return the supplied value + * @throws IOException on IO error + */ + T get() throws IOException; + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/InspectedContent.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/InspectedContent.java new file mode 100644 index 0000000000..d3f945860e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/InspectedContent.java @@ -0,0 +1,187 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.io; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.springframework.util.Assert; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.StreamUtils; + +/** + * {@link Content} that is reads and inspects a source of data only once but allows it to + * be consumed multiple times. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public class InspectedContent implements Content { + + static final int MEMORY_LIMIT = 4 * 1024 + 3; + + private final int size; + + private final Object content; + + InspectedContent(int size, Object content) { + this.size = size; + this.content = content; + } + + @Override + public int size() { + return this.size; + } + + @Override + public void writeTo(OutputStream outputStream) throws IOException { + if (this.content instanceof byte[]) { + FileCopyUtils.copy((byte[]) this.content, outputStream); + } + else if (this.content instanceof File) { + FileCopyUtils.copy(new FileInputStream((File) this.content), outputStream); + } + else { + throw new IllegalStateException("Unknown content type"); + } + } + + /** + * Factory method to create an {@link InspectedContent} instance from a source input + * stream. + * @param inputStream the content input stream + * @param inspectors any inspectors to apply + * @return a new inspected content instance + * @throws IOException on IO error + */ + public static InspectedContent of(InputStream inputStream, Inspector... inspectors) throws IOException { + Assert.notNull(inputStream, "InputStream must not be null"); + return of((outputStream) -> FileCopyUtils.copy(inputStream, outputStream), inspectors); + } + + /** + * Factory method to create an {@link InspectedContent} instance from source content. + * @param content the content + * @param inspectors any inspectors to apply + * @return a new inspected content instance + * @throws IOException on IO error + */ + public static InspectedContent of(Content content, Inspector... inspectors) throws IOException { + Assert.notNull(content, "Content must not be null"); + return of(content::writeTo, inspectors); + } + + /** + * Factory method to create an {@link InspectedContent} instance from a source write + * method. + * @param writer a consumer representing the write method + * @param inspectors any inspectors to apply + * @return a new inspected content instance + * @throws IOException on IO error + */ + public static InspectedContent of(IOConsumer writer, Inspector... inspectors) throws IOException { + Assert.notNull(writer, "Writer must not be null"); + InspectingOutputStream outputStream = new InspectingOutputStream(inspectors); + try { + writer.accept(outputStream); + } + finally { + outputStream.close(); + } + return new InspectedContent(outputStream.getSize(), outputStream.getContent()); + } + + /** + * Interface that can be used to inspect content as it is initially read. + */ + public interface Inspector { + + /** + * Update inspected information based on the provided bytes. + * @param input the array of bytes. + * @param offset the offset to start from in the array of bytes. + * @param len the number of bytes to use, starting at {@code offset}. + * @throws IOException on IO error + */ + void update(byte[] input, int offset, int len) throws IOException; + + } + + /** + * Internal {@link OutputStream} used to capture the content either as bytes, or to a + * File if the content is too large. + */ + private static final class InspectingOutputStream extends OutputStream { + + private final Inspector[] inspectors; + + private int size; + + private OutputStream delegate; + + private File tempFile; + + private byte[] singleByteBuffer = new byte[0]; + + private InspectingOutputStream(Inspector[] inspectors) { + this.inspectors = inspectors; + this.delegate = new ByteArrayOutputStream(); + } + + @Override + public void write(int b) throws IOException { + this.singleByteBuffer[0] = (byte) (b & 0xFF); + write(this.singleByteBuffer); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + int size = len - off; + if (this.tempFile == null && (this.size + size) > MEMORY_LIMIT) { + convertToTempFile(); + } + this.delegate.write(b, off, len); + for (Inspector inspector : this.inspectors) { + inspector.update(b, off, len); + } + this.size += size; + } + + private void convertToTempFile() throws IOException { + this.tempFile = File.createTempFile("buildpack", ".tmp"); + byte[] bytes = ((ByteArrayOutputStream) this.delegate).toByteArray(); + this.delegate = new FileOutputStream(this.tempFile); + StreamUtils.copy(bytes, this.delegate); + } + + private Object getContent() { + return (this.tempFile != null) ? this.tempFile : ((ByteArrayOutputStream) this.delegate).toByteArray(); + } + + private int getSize() { + return this.size; + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/Layout.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/Layout.java new file mode 100644 index 0000000000..66406efaa3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/Layout.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.io; + +import java.io.IOException; + +/** + * Interface that can be used to write a file/folder layout. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public interface Layout { + + /** + * Add a folder to the content. + * @param name the full name of the folder to add. + * @param owner the owner of the folder + * @throws IOException on IO error + */ + void folder(String name, Owner owner) throws IOException; + + /** + * Write a file to the content. + * @param name the full name of the file to add. + * @param owner the owner of the folder + * @param content the content to add + * @throws IOException on IO error + */ + void file(String name, Owner owner, Content content) throws IOException; + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/Owner.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/Owner.java new file mode 100644 index 0000000000..95c4382f57 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/Owner.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.io; + +/** + * A user and group ID that can be used to indicate file ownership. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public interface Owner { + + /** + * Owner for root ownership. + */ + Owner ROOT = Owner.of(0, 0); + + /** + * Return the user identifier (UID) of the owner. + * @return the user identifier + */ + long getUid(); + + /** + * Return the group identifier (GID) of the owner. + * @return the group identifier + */ + long getGid(); + + /** + * Factory method to create a new {@link Owner} with specified user/group identifier. + * @param uid the user identifier + * @param gid the group identifier + * @return a new {@link Owner} instance + */ + static Owner of(long uid, long gid) { + return new DefaultOwner(uid, gid); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/TarArchive.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/TarArchive.java new file mode 100644 index 0000000000..af822f8d44 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/TarArchive.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.io; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; + +/** + * A TAR archive that can be written to an output stream. + * + * @author Phillip Webb + * @since 2.3.0 + */ +@FunctionalInterface +public interface TarArchive { + + /** + * {@link Instant} that can be used to normalize TAR files so all entries have the + * same modification time. + */ + Instant NORMALIZED_TIME = OffsetDateTime.of(1980, 1, 1, 0, 0, 1, 0, ZoneOffset.UTC).toInstant(); + + /** + * Write the TAR archive to the given output stream. + * @param outputStream the output stream to write to + * @throws IOException on IO error + */ + void writeTo(OutputStream outputStream) throws IOException; + + /** + * Factory method to create a new {@link TarArchive} instance with a specific layout. + * @param layout the TAR layout + * @return a new {@link TarArchive} instance + */ + static TarArchive of(IOConsumer layout) { + return (outputStream) -> { + TarLayoutWriter writer = new TarLayoutWriter(outputStream); + layout.accept(writer); + writer.finish(); + }; + } + + /** + * Factory method to adapt a ZIP file to {@link TarArchive}. + * @param zip the source zip file + * @param owner the owner of the entries in the TAR + * @return a new {@link TarArchive} instance + */ + static TarArchive fromZip(File zip, Owner owner) { + return new ZipFileTarArchive(zip, owner); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/TarLayoutWriter.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/TarLayoutWriter.java new file mode 100644 index 0000000000..4fc0ca84a8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/TarLayoutWriter.java @@ -0,0 +1,84 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.io; + +import java.io.Closeable; +import java.io.IOException; +import java.io.OutputStream; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; +import org.apache.commons.compress.archivers.tar.TarConstants; + +import org.springframework.util.StreamUtils; + +/** + * {@link Layout} for writing TAR archive content directly to an {@link OutputStream}. + * + * @author Phillip Webb + */ +class TarLayoutWriter implements Layout, Closeable { + + static final long NORMALIZED_MOD_TIME = TarArchive.NORMALIZED_TIME.toEpochMilli(); + + private TarArchiveOutputStream outputStream; + + TarLayoutWriter(OutputStream outputStream) { + this.outputStream = new TarArchiveOutputStream(outputStream); + } + + @Override + public void folder(String name, Owner owner) throws IOException { + this.outputStream.putArchiveEntry(createFolderEntry(name, owner)); + this.outputStream.closeArchiveEntry(); + } + + @Override + public void file(String name, Owner owner, Content content) throws IOException { + this.outputStream.putArchiveEntry(createFileEntry(name, owner, content.size())); + content.writeTo(StreamUtils.nonClosing(this.outputStream)); + this.outputStream.closeArchiveEntry(); + } + + private TarArchiveEntry createFolderEntry(String name, Owner owner) { + return createEntry(name, owner, TarConstants.LF_DIR, 0755, 0); + } + + private TarArchiveEntry createFileEntry(String name, Owner owner, int size) { + return createEntry(name, owner, TarConstants.LF_NORMAL, 0644, size); + } + + private TarArchiveEntry createEntry(String name, Owner owner, byte linkFlag, int mode, int size) { + TarArchiveEntry entry = new TarArchiveEntry(name, linkFlag, true); + entry.setUserId(owner.getUid()); + entry.setGroupId(owner.getGid()); + entry.setMode(mode); + entry.setModTime(NORMALIZED_MOD_TIME); + entry.setSize(size); + return entry; + } + + void finish() throws IOException { + this.outputStream.finish(); + } + + @Override + public void close() throws IOException { + this.outputStream.close(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/ZipFileTarArchive.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/ZipFileTarArchive.java new file mode 100644 index 0000000000..b49e041b03 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/ZipFileTarArchive.java @@ -0,0 +1,88 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.io; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Enumeration; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; +import org.apache.commons.compress.archivers.tar.TarConstants; +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipFile; + +import org.springframework.util.Assert; +import org.springframework.util.StreamUtils; + +/** + * Adapter class to convert a ZIP file to a {@link TarArchive}. + * + * @author Phillip Webb + */ +class ZipFileTarArchive implements TarArchive { + + static final long NORMALIZED_MOD_TIME = TarArchive.NORMALIZED_TIME.toEpochMilli(); + + private final File zip; + + private final Owner owner; + + ZipFileTarArchive(File zip, Owner owner) { + Assert.notNull(zip, "Zip must not be null"); + Assert.notNull(owner, "Owner must not be null"); + this.zip = zip; + this.owner = owner; + } + + @Override + public void writeTo(OutputStream outputStream) throws IOException { + TarArchiveOutputStream tar = new TarArchiveOutputStream(outputStream); + try (ZipFile zipFile = new ZipFile(this.zip)) { + Enumeration entries = zipFile.getEntries(); + while (entries.hasMoreElements()) { + ZipArchiveEntry zipEntry = entries.nextElement(); + copy(zipEntry, zipFile.getInputStream(zipEntry), tar); + } + } + tar.finish(); + } + + private void copy(ZipArchiveEntry zipEntry, InputStream zip, TarArchiveOutputStream tar) throws IOException { + TarArchiveEntry tarEntry = convert(zipEntry); + tar.putArchiveEntry(tarEntry); + if (tarEntry.isFile()) { + StreamUtils.copyRange(zip, tar, 0, tarEntry.getSize()); + } + tar.closeArchiveEntry(); + } + + private TarArchiveEntry convert(ZipArchiveEntry zipEntry) { + byte linkFlag = (zipEntry.isDirectory()) ? TarConstants.LF_DIR : TarConstants.LF_NORMAL; + TarArchiveEntry tarEntry = new TarArchiveEntry(zipEntry.getName(), linkFlag, true); + tarEntry.setUserId(this.owner.getUid()); + tarEntry.setGroupId(this.owner.getGid()); + tarEntry.setModTime(NORMALIZED_MOD_TIME); + if (!zipEntry.isDirectory()) { + tarEntry.setSize(zipEntry.getSize()); + } + return tarEntry; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/package-info.java new file mode 100644 index 0000000000..fdfa72917c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/io/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 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. + */ + +/** + * IO classes and utilities. + */ +package org.springframework.boot.cloudnativebuildpack.io; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/json/JsonStream.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/json/JsonStream.java new file mode 100644 index 0000000000..b3579577e4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/json/JsonStream.java @@ -0,0 +1,91 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.json; + +import java.io.IOException; +import java.io.InputStream; +import java.util.function.Consumer; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * Utility class that allows JSON to be parsed and processed as it's received. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public class JsonStream { + + private final ObjectMapper objectMapper; + + /** + * Create a new {@link JsonStream} backed by the given object mapper. + * @param objectMapper the object mapper to use + */ + public JsonStream(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + /** + * Stream {@link ObjectNode object nodes} from the content as they become available. + * @param content the source content + * @param consumer the {@link ObjectNode} consumer + * @throws IOException on IO error + */ + public void get(InputStream content, Consumer consumer) throws IOException { + get(content, ObjectNode.class, consumer); + } + + /** + * Stream objects from the content as they become available. + * @param the object type + * @param content the source content + * @param type the object type + * @param consumer the {@link ObjectNode} consumer + * @throws IOException on IO error + */ + public void get(InputStream content, Class type, Consumer consumer) throws IOException { + JsonFactory jsonFactory = this.objectMapper.getFactory(); + JsonParser parser = jsonFactory.createParser(content); + while (!parser.isClosed()) { + JsonToken token = parser.nextToken(); + if (token != null && token != JsonToken.END_OBJECT) { + T node = read(parser, type); + if (node != null) { + consumer.accept(node); + } + } + } + } + + @SuppressWarnings("unchecked") + private T read(JsonParser parser, Class type) throws IOException { + if (ObjectNode.class.isAssignableFrom(type)) { + ObjectNode node = this.objectMapper.readTree(parser); + if (node == null || node.isMissingNode() || node.isEmpty()) { + return null; + } + return (T) node; + } + return this.objectMapper.readValue(parser, type); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/json/MappedObject.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/json/MappedObject.java new file mode 100644 index 0000000000..23a3441517 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/json/MappedObject.java @@ -0,0 +1,228 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.json; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.function.Function; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.springframework.util.Assert; + +/** + * Base class for mapped JSON objects. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public class MappedObject { + + private final JsonNode node; + + private final Lookup lookup; + + /** + * Create a new {@link MappedObject} instance. + * @param node the source node + * @param lookup method handle lookup + */ + protected MappedObject(JsonNode node, Lookup lookup) { + this.node = node; + this.lookup = lookup; + } + + /** + * Return the source node of the mapped object. + * @return the source node + */ + protected final JsonNode getNode() { + return this.node; + } + + /** + * Get the value at the given JSON path expression as a specific type. + * @param the data type + * @param expression the JSON path expression + * @param type the desired type. May be a simple JSON type or an interface + * @return the value + */ + protected T valueAt(String expression, Class type) { + return valueAt(this, this.node, this.lookup, expression, type); + } + + @SuppressWarnings("unchecked") + protected static T getRoot(Object proxy) { + MappedInvocationHandler handler = (MappedInvocationHandler) Proxy.getInvocationHandler(proxy); + return (T) handler.root; + } + + protected static T valueAt(Object proxy, String expression, Class type) { + MappedInvocationHandler handler = (MappedInvocationHandler) Proxy.getInvocationHandler(proxy); + return valueAt(handler.root, handler.node, handler.lookup, expression, type); + } + + @SuppressWarnings("unchecked") + private static T valueAt(MappedObject root, JsonNode node, Lookup lookup, String expression, Class type) { + JsonNode result = node.at(expression); + if (result.isMissingNode() && expression.startsWith("/") && expression.length() > 1 + && Character.isLowerCase(expression.charAt(1))) { + StringBuilder alternative = new StringBuilder(expression); + alternative.setCharAt(1, Character.toUpperCase(alternative.charAt(1))); + result = node.at(alternative.toString()); + } + if (type.isInterface() && !type.getName().startsWith("java")) { + return (T) Proxy.newProxyInstance(MappedObject.class.getClassLoader(), new Class[] { type }, + new MappedInvocationHandler(root, result, lookup)); + } + if (result.isMissingNode()) { + return null; + } + try { + return SharedObjectMapper.get().treeToValue(result, type); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + /** + * Factory method to create a new {@link MappedObject} instance. + * @param the mapped object type + * @param content the JSON content for the object + * @param factory a factory to create the mapped object from a {@link JsonNode} + * @return the mapped object + * @throws IOException on IO error + */ + protected static T of(String content, Function factory) throws IOException { + return of(content, ObjectMapper::readTree, factory); + } + + /** + * Factory method to create a new {@link MappedObject} instance. + * @param the mapped object type + * @param content the JSON content for the object + * @param factory a factory to create the mapped object from a {@link JsonNode} + * @return the mapped object + * @throws IOException on IO error + */ + protected static T of(InputStream content, Function factory) + throws IOException { + return of(content, ObjectMapper::readTree, factory); + } + + /** + * Factory method to create a new {@link MappedObject} instance. + * @param the mapped object type + * @param the content type + * @param content the JSON content for the object + * @param reader the content reader + * @param factory a factory to create the mapped object from a {@link JsonNode} + * @return the mapped object + * @throws IOException on IO error + */ + protected static T of(C content, ContentReader reader, Function factory) + throws IOException { + ObjectMapper objectMapper = SharedObjectMapper.get(); + JsonNode node = reader.read(objectMapper, content); + return factory.apply(node); + } + + /** + * Strategy used to read JSON content. + * + * @param the content type + */ + @FunctionalInterface + protected interface ContentReader { + + /** + * Read JSON content as a {@link JsonNode}. + * @param objectMapper the source object mapper + * @param content the content to read + * @return a {@link JsonNode} + * @throws IOException on IO error + */ + JsonNode read(ObjectMapper objectMapper, C content) throws IOException; + + } + + /** + * {@link InvocationHandler} used to support + * {@link MappedObject#valueAt(String, Class) valueAt} with {@code interface} types. + */ + private static class MappedInvocationHandler implements InvocationHandler { + + private static final String GET = "get"; + + private static final String IS = "is"; + + private final MappedObject root; + + private final JsonNode node; + + private final Lookup lookup; + + MappedInvocationHandler(MappedObject root, JsonNode node, Lookup lookup) { + this.root = root; + this.node = node; + this.lookup = lookup; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + Class declaringClass = method.getDeclaringClass(); + if (method.isDefault()) { + Lookup lookup = this.lookup.in(declaringClass); + MethodHandle methodHandle = lookup.unreflectSpecial(method, declaringClass).bindTo(proxy); + return methodHandle.invokeWithArguments(); + } + if (declaringClass == Object.class) { + method.invoke(proxy, args); + } + Assert.state(args == null || args.length == 0, "Unsupported method " + method); + String name = getName(method.getName()); + Class type = method.getReturnType(); + return valueForProperty(name, type); + } + + private String getName(String name) { + StringBuilder result = new StringBuilder(name); + if (name.startsWith(GET)) { + result = new StringBuilder(name.substring(GET.length())); + } + if (name.startsWith(IS)) { + result = new StringBuilder(name.substring(IS.length())); + } + Assert.state(result.length() >= 0, "Missing name"); + result.setCharAt(0, Character.toLowerCase(result.charAt(0))); + return result.toString(); + } + + private Object valueForProperty(String name, Class type) { + return valueAt(this.root, this.node, this.lookup, "/" + name, type); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/json/SharedObjectMapper.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/json/SharedObjectMapper.java new file mode 100644 index 0000000000..58ab1ea708 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/json/SharedObjectMapper.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.json; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; + +/** + * Provides access to a shared pre-configured {@link ObjectMapper}. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public final class SharedObjectMapper { + + private static final ObjectMapper INSTANCE; + + static { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.registerModule(new ParameterNamesModule()); + objectMapper.enable(SerializationFeature.INDENT_OUTPUT); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.LOWER_CAMEL_CASE); + INSTANCE = objectMapper; + } + + private SharedObjectMapper() { + } + + public static ObjectMapper get() { + return INSTANCE; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/json/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/json/package-info.java new file mode 100644 index 0000000000..f75c1472d9 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/json/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 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. + */ + +/** + * Utilities and classes for JSON processing. + */ +package org.springframework.boot.cloudnativebuildpack.json; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/socket/AbstractSocket.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/socket/AbstractSocket.java new file mode 100644 index 0000000000..f4d89dceb0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/socket/AbstractSocket.java @@ -0,0 +1,87 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.socket; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.SocketAddress; + +/** + * Abstract base class for custom socket implementation. + * + * @author Phillip Webb + */ +class AbstractSocket extends Socket { + + @Override + public void connect(SocketAddress endpoint) throws IOException { + } + + @Override + public void connect(SocketAddress endpoint, int timeout) throws IOException { + } + + @Override + public boolean isConnected() { + return true; + } + + @Override + public boolean isBound() { + return true; + } + + @Override + public void shutdownInput() throws IOException { + throw new UnsupportedSocketOperationException(); + } + + @Override + public void shutdownOutput() throws IOException { + throw new UnsupportedSocketOperationException(); + } + + @Override + public InetAddress getInetAddress() { + return null; + } + + @Override + public InetAddress getLocalAddress() { + return null; + } + + @Override + public SocketAddress getLocalSocketAddress() { + return null; + } + + @Override + public SocketAddress getRemoteSocketAddress() { + return null; + } + + private static class UnsupportedSocketOperationException extends UnsupportedOperationException { + + UnsupportedSocketOperationException() { + super("Unsupported socket operation"); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/socket/BsdDomainSocket.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/socket/BsdDomainSocket.java new file mode 100644 index 0000000000..2732bdbc67 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/socket/BsdDomainSocket.java @@ -0,0 +1,83 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.socket; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; + +import com.sun.jna.LastErrorException; +import com.sun.jna.Native; +import com.sun.jna.Platform; +import com.sun.jna.Structure; + +import org.springframework.util.Assert; + +/** + * {@link DomainSocket} implementation for BSD based platforms. + * + * @author Phillip Webb + */ +class BsdDomainSocket extends DomainSocket { + + private static final int MAX_PATH_LENGTH = 104; + + static { + Native.register(Platform.C_LIBRARY_NAME); + } + + BsdDomainSocket(String path) throws IOException { + super(path); + } + + @Override + protected void connect(String path, int handle) { + SockaddrUn address = new SockaddrUn(AF_LOCAL, path.getBytes(StandardCharsets.UTF_8)); + connect(handle, address, address.size()); + } + + private native int connect(int fd, SockaddrUn address, int addressLen) throws LastErrorException; + + /** + * Native {@code sockaddr_un} structure as defined in {@code sys/un.h}. + */ + public static class SockaddrUn extends Structure implements Structure.ByReference { + + public byte sunLen; + + public byte sunFamily; + + public byte[] sunPath = new byte[MAX_PATH_LENGTH]; + + private SockaddrUn(byte sunFamily, byte[] path) { + Assert.isTrue(path.length < MAX_PATH_LENGTH, "Path cannot exceed " + MAX_PATH_LENGTH + " bytes"); + System.arraycopy(path, 0, this.sunPath, 0, path.length); + this.sunPath[path.length] = 0; + this.sunLen = (byte) (fieldOffset("sunPath") + path.length); + this.sunFamily = sunFamily; + allocateMemory(); + } + + @Override + protected List getFieldOrder() { + return Arrays.asList(new String[] { "sunLen", "sunFamily", "sunPath" }); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/socket/DomainSocket.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/socket/DomainSocket.java new file mode 100644 index 0000000000..3a6610d1d3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/socket/DomainSocket.java @@ -0,0 +1,195 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.socket; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.nio.ByteBuffer; + +import com.sun.jna.LastErrorException; +import com.sun.jna.Native; +import com.sun.jna.Platform; + +import org.springframework.boot.cloudnativebuildpack.socket.FileDescriptor.Handle; + +/** + * A {@link Socket} implementation for Linux of BSD domain sockets. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public abstract class DomainSocket extends AbstractSocket { + + private static final int SHUT_RD = 0; + + private static final int SHUT_WR = 1; + + protected static final int PF_LOCAL = 1; + + protected static final byte AF_LOCAL = 1; + + protected static final int SOCK_STREAM = 1; + + private final FileDescriptor fileDescriptor; + + private final InputStream inputStream; + + private final OutputStream outputStream; + + static { + Native.register(Platform.C_LIBRARY_NAME); + } + + DomainSocket(String path) throws IOException { + try { + this.fileDescriptor = open(path); + this.inputStream = new DomainSocketInputStream(); + this.outputStream = new DomainSocketOutputStream(); + } + catch (LastErrorException ex) { + throw new IOException(ex); + } + } + + private FileDescriptor open(String path) { + int handle = socket(PF_LOCAL, SOCK_STREAM, 0); + connect(path, handle); + return new FileDescriptor(handle, this::close); + } + + private int read(ByteBuffer buffer) throws IOException { + try (Handle handle = this.fileDescriptor.acquire()) { + if (handle.isClosed()) { + return -1; + } + try { + return read(handle.intValue(), buffer, buffer.remaining()); + } + catch (LastErrorException ex) { + throw new IOException(ex); + } + } + } + + public void write(ByteBuffer buffer) throws IOException { + try (Handle handle = this.fileDescriptor.acquire()) { + if (!handle.isClosed()) { + try { + write(handle.intValue(), buffer, buffer.remaining()); + } + catch (LastErrorException ex) { + throw new IOException(ex); + } + } + } + } + + @Override + public InputStream getInputStream() { + return this.inputStream; + } + + @Override + public OutputStream getOutputStream() { + return this.outputStream; + } + + @Override + public void close() throws IOException { + super.close(); + try { + this.fileDescriptor.close(); + } + catch (LastErrorException ex) { + throw new IOException(ex); + } + } + + protected abstract void connect(String path, int handle); + + private native int socket(int domain, int type, int protocol) throws LastErrorException; + + private native int read(int fd, ByteBuffer buffer, int count) throws LastErrorException; + + private native int write(int fd, ByteBuffer buffer, int count) throws LastErrorException; + + private native int close(int fd) throws LastErrorException; + + /** + * Return a new {@link DomainSocket} for the given path. + * @param path the path to the domain socket + * @return a {@link DomainSocket} instance + * @throws IOException if the socket cannot be opened + */ + public static DomainSocket get(String path) throws IOException { + if (Platform.isMac() || isBsdPlatform()) { + return new BsdDomainSocket(path); + } + return new LinuxDomainSocket(path); + } + + private static boolean isBsdPlatform() { + return Platform.isFreeBSD() || Platform.iskFreeBSD() || Platform.isNetBSD() || Platform.isOpenBSD(); + } + + /** + * {@link InputStream} returned from the {@link DomainSocket}. + */ + private class DomainSocketInputStream extends InputStream { + + @Override + public int read() throws IOException { + ByteBuffer buffer = ByteBuffer.allocate(1); + int amountRead = DomainSocket.this.read(buffer); + return (amountRead != 1) ? -1 : buffer.get() & 0xFF; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (len == 0) { + return 0; + } + int amountRead = DomainSocket.this.read(ByteBuffer.wrap(b, off, len)); + return (amountRead > 0) ? amountRead : -1; + } + + } + + /** + * {@link OutputStream} returned from the {@link DomainSocket}. + */ + private class DomainSocketOutputStream extends OutputStream { + + @Override + public void write(int b) throws IOException { + ByteBuffer buffer = ByteBuffer.allocate(1); + buffer.put(0, (byte) (b & 0xFF)); + DomainSocket.this.write(buffer); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + if (len != 0) { + DomainSocket.this.write(ByteBuffer.wrap(b, off, len)); + } + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/socket/FileDescriptor.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/socket/FileDescriptor.java new file mode 100644 index 0000000000..b260c2bde7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/socket/FileDescriptor.java @@ -0,0 +1,123 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.socket; + +import java.io.Closeable; +import java.io.IOException; +import java.util.function.IntConsumer; + +/** + * Provides access to an opaque to the underling file system representation of an open + * file. + * + * @author Phillip Webb + * @see #acquire() + */ +class FileDescriptor { + + private final Handle openHandle; + + private final Handle closedHandler; + + private final IntConsumer closer; + + private Status status = Status.OPEN; + + private int referenceCount; + + FileDescriptor(int handle, IntConsumer closer) { + this.openHandle = new Handle(handle); + this.closedHandler = new Handle(-1); + this.closer = closer; + } + + @Override + protected void finalize() throws Throwable { + close(); + } + + /** + * Acquire an instance of the actual {@link Handle}. The caller must + * {@link Handle#close() close} the resulting handle when done. + * @return the handle + */ + synchronized Handle acquire() { + this.referenceCount++; + return (this.status != Status.OPEN) ? this.closedHandler : this.openHandle; + } + + private synchronized void release() { + this.referenceCount--; + if (this.referenceCount == 0 && this.status == Status.CLOSE_PENDING) { + this.closer.accept(this.openHandle.value); + this.status = Status.CLOSED; + } + } + + /** + * Close the underlying file when all handles have been released. + */ + synchronized void close() { + if (this.status == Status.OPEN) { + if (this.referenceCount == 0) { + this.closer.accept(this.openHandle.value); + this.status = Status.CLOSED; + } + else { + this.status = Status.CLOSE_PENDING; + } + } + } + + /** + * The status of the file descriptor. + */ + private enum Status { + + OPEN, CLOSE_PENDING, CLOSED; + + } + + /** + * Provides access to the actual file descriptor handle. + */ + final class Handle implements Closeable { + + private final int value; + + private Handle(int value) { + this.value = value; + } + + boolean isClosed() { + return this.value == -1; + } + + int intValue() { + return this.value; + } + + @Override + public void close() throws IOException { + if (!isClosed()) { + release(); + } + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/socket/LinuxDomainSocket.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/socket/LinuxDomainSocket.java new file mode 100644 index 0000000000..69d8928379 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/socket/LinuxDomainSocket.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.socket; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; + +import com.sun.jna.LastErrorException; +import com.sun.jna.Structure; + +import org.springframework.util.Assert; + +/** + * {@link DomainSocket} implementation for Linux based platforms. + * + * @author Phillip Webb + */ +class LinuxDomainSocket extends DomainSocket { + + LinuxDomainSocket(String path) throws IOException { + super(path); + } + + private static final int MAX_PATH_LENGTH = 108; + + @Override + protected void connect(String path, int handle) { + SockaddrUn address = new SockaddrUn(AF_LOCAL, path.getBytes(StandardCharsets.UTF_8)); + connect(handle, address, address.size()); + } + + private native int connect(int fd, SockaddrUn address, int addressLen) throws LastErrorException; + + /** + * Native {@code sockaddr_un} structure as defined in {@code sys/un.h}. + */ + public static class SockaddrUn extends Structure implements Structure.ByReference { + + public short sunFamily; + + public byte[] sunPath = new byte[MAX_PATH_LENGTH]; + + private SockaddrUn(byte sunFamily, byte[] path) { + Assert.isTrue(path.length < MAX_PATH_LENGTH, "Path cannot exceed " + MAX_PATH_LENGTH + " bytes"); + System.arraycopy(path, 0, this.sunPath, 0, path.length); + this.sunPath[path.length] = 0; + this.sunFamily = sunFamily; + allocateMemory(); + } + + @Override + protected List getFieldOrder() { + return Arrays.asList(new String[] { "sunLen", "sunFamily", "sunPath" }); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/socket/NamedPipeSocket.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/socket/NamedPipeSocket.java new file mode 100644 index 0000000000..032e75ec88 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/socket/NamedPipeSocket.java @@ -0,0 +1,162 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.socket; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.net.Socket; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import com.sun.jna.Platform; +import com.sun.jna.platform.win32.Kernel32; + +/** + * A {@link Socket} implementation for named pipes. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public class NamedPipeSocket extends Socket { + + private static final long TIMEOUT = TimeUnit.MILLISECONDS.toNanos(1000); + + private final RandomAccessFile file; + + private final InputStream inputStream; + + private final OutputStream outputStream; + + private final Consumer awaiter; + + NamedPipeSocket(String path) throws IOException { + this.file = open(path); + this.inputStream = new NamedPipeInputStream(); + this.outputStream = new NamedPipeOutputStream(); + this.awaiter = Platform.isWindows() ? new WindowsAwaiter() : new SleepAwaiter(); + } + + private RandomAccessFile open(String path) throws IOException { + long startTime = System.nanoTime(); + while (true) { + try { + return new RandomAccessFile(path, "rw"); + } + catch (FileNotFoundException ex) { + if (System.nanoTime() - startTime > TIMEOUT) { + throw ex; + } + this.awaiter.accept(path); + } + } + } + + @Override + public InputStream getInputStream() { + return this.inputStream; + } + + @Override + public OutputStream getOutputStream() { + return this.outputStream; + } + + @Override + public void close() throws IOException { + this.file.close(); + } + + protected final RandomAccessFile getFile() { + return this.file; + } + + /** + * Return a new {@link NamedPipeSocket} for the given path. + * @param path the path to the domain socket + * @return a {@link NamedPipeSocket} instance + * @throws IOException if the socket cannot be opened + */ + public static NamedPipeSocket get(String path) throws IOException { + return new NamedPipeSocket(path); + } + + /** + * {@link InputStream} returned from the {@link NamedPipeSocket}. + */ + private class NamedPipeInputStream extends InputStream { + + @Override + public int read() throws IOException { + return getFile().read(); + } + + @Override + public int read(byte[] bytes, int off, int len) throws IOException { + return getFile().read(bytes, off, len); + } + + } + + /** + * {@link InputStream} returned from the {@link NamedPipeSocket}. + */ + private class NamedPipeOutputStream extends OutputStream { + + @Override + public void write(int value) throws IOException { + NamedPipeSocket.this.file.write(value); + } + + @Override + public void write(byte[] bytes, int off, int len) throws IOException { + NamedPipeSocket.this.file.write(bytes, off, len); + } + + } + + /** + * Waits for the name pipe file using a simple sleep. + */ + private class SleepAwaiter implements Consumer { + + @Override + public void accept(String path) { + try { + Thread.sleep(100); + } + catch (InterruptedException ex) { + } + } + + } + + /** + * Waits for the name pipe file using Windows specific logic. + */ + private class WindowsAwaiter implements Consumer { + + @Override + public void accept(String path) { + Kernel32.INSTANCE.WaitNamedPipe(path, 100); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/socket/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/socket/package-info.java new file mode 100644 index 0000000000..18a44ee8cf --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/socket/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 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. + */ + +/** + * Low-level {@link java.net.Socket} implementations required for local Docker access. + */ +package org.springframework.boot.cloudnativebuildpack.socket; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/toml/Toml.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/toml/Toml.java new file mode 100644 index 0000000000..c0b26a3a6e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/toml/Toml.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.toml; + +import java.util.Arrays; +import java.util.stream.Collectors; + +/** + * Very simple TOML markup builder. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public class Toml { + + private final StringBuilder toml = new StringBuilder(); + + public void table(String name) { + append("[" + name + "]"); + } + + public void string(String name, String value) { + append(name + " = " + quote(value)); + } + + public void array(String name, String... value) { + if (value != null && value.length > 0) { + append(name + " = " + Arrays.stream(value).map(this::quote).collect(Collectors.toList())); + } + } + + private void append(String line) { + this.toml.append(line).append('\n'); + } + + private String quote(String string) { + return "\"" + string + "\""; + } + + @Override + public String toString() { + return this.toml.toString(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/toml/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/toml/package-info.java new file mode 100644 index 0000000000..35bb4e7366 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/main/java/org/springframework/boot/cloudnativebuildpack/toml/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 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. + */ + +/** + * Support for writing TOML content. + */ +package org.springframework.boot.cloudnativebuildpack.toml; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/ApiVersionTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/ApiVersionTests.java new file mode 100644 index 0000000000..486fb4f955 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/ApiVersionTests.java @@ -0,0 +1,117 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.build; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link ApiVersion}. + * + * @author Phillip Webb + */ +class ApiVersionTests { + + @Test + void parseWhenVersionIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> ApiVersion.parse(null)) + .withMessage("Value must not be empty"); + } + + @Test + void parseWhenVersionIsEmptyThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> ApiVersion.parse("")) + .withMessage("Value must not be empty"); + } + + @Test + void parseWhenVersionDoesNotMatchPatternThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> ApiVersion.parse("bad")) + .withMessage("Malformed version number 'bad'"); + } + + @Test + void parseReturnsVersion() { + ApiVersion version = ApiVersion.parse("1.2"); + assertThat(version.getMajor()).isEqualTo(1); + assertThat(version.getMinor()).isEqualTo(2); + } + + @Test + void assertSupportsWhenSupports() { + ApiVersion.parse("1.2").assertSupports(ApiVersion.parse("1.0")); + } + + @Test + void assertSupportsWhenDoesNotSupportThrowsException() { + assertThatIllegalStateException() + .isThrownBy(() -> ApiVersion.parse("1.2").assertSupports(ApiVersion.parse("1.3"))) + .withMessage("Version 'v1.3' is not supported by this version ('v1.2')"); + } + + @Test + void supportWhenSame() { + assertThat(supports("0.0", "0.0")).isTrue(); + assertThat(supports("0.1", "0.1")).isTrue(); + assertThat(supports("1.0", "1.0")).isTrue(); + assertThat(supports("1.1", "1.1")).isTrue(); + } + + @Test + void supportsWhenDifferentMajor() { + assertThat(supports("0.0", "1.0")).isFalse(); + assertThat(supports("1.0", "0.0")).isFalse(); + assertThat(supports("1.0", "2.0")).isFalse(); + assertThat(supports("2.0", "1.0")).isFalse(); + assertThat(supports("1.1", "2.1")).isFalse(); + assertThat(supports("2.1", "1.1")).isFalse(); + } + + @Test + void supportsWhenDifferentMinor() { + assertThat(supports("1.2", "1.1")).isTrue(); + assertThat(supports("1.2", "1.3")).isFalse(); + } + + @Test + void supportWhenMajorZeroAndDifferentMinor() { + assertThat(supports("0.2", "0.1")).isFalse(); + assertThat(supports("0.2", "0.3")).isFalse(); + } + + @Test + void toStringReturnsString() { + assertThat(ApiVersion.parse("1.2").toString()).isEqualTo("v1.2"); + } + + @Test + void equalsAndHashCode() { + ApiVersion v12a = ApiVersion.parse("1.2"); + ApiVersion v12b = ApiVersion.parse("1.2"); + ApiVersion v13 = ApiVersion.parse("1.3"); + assertThat(v12a.hashCode()).isEqualTo(v12b.hashCode()); + assertThat(v12a).isEqualTo(v12a).isEqualTo(v12b).isNotEqualTo(v13); + } + + private boolean supports(String v1, String v2) { + return ApiVersion.parse(v1).supports(ApiVersion.parse(v2)); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/BuildLogTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/BuildLogTests.java new file mode 100644 index 0000000000..53e77531ad --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/BuildLogTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.build; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link BuildLog}. + * + * @author Phillip Webb + */ +class BuildLogTests { + + @Test + void toSystemOutPrintsToSystemOut() { + BuildLog log = BuildLog.toSystemOut(); + assertThat(log).isInstanceOf(PrintStreamBuildLog.class); + assertThat(log).extracting("out").isSameAs(System.out); + } + + @Test + void toPrintsToOutput() { + BuildLog log = BuildLog.to(System.err); + assertThat(log).isInstanceOf(PrintStreamBuildLog.class); + assertThat(log).extracting("out").isSameAs(System.err); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/BuildOwnerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/BuildOwnerTests.java new file mode 100644 index 0000000000..beb021fe42 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/BuildOwnerTests.java @@ -0,0 +1,86 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.build; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link BuildOwner}. + * + * @author Phillip Webb + */ +class BuildOwnerTests { + + @Test + void fromEnvReturnsOwner() { + Map env = new LinkedHashMap<>(); + env.put("CNB_USER_ID", "123"); + env.put("CNB_GROUP_ID", "456"); + BuildOwner owner = BuildOwner.fromEnv(env); + assertThat(owner.getUid()).isEqualTo(123); + assertThat(owner.getGid()).isEqualTo(456); + assertThat(owner.toString()).isEqualTo("123/456"); + } + + @Test + void fromEnvWhenEnvIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> BuildOwner.fromEnv(null)) + .withMessage("Env must not be null"); + } + + @Test + void fromEnvWhenUserPropertyIsMissingThrowsException() { + Map env = new LinkedHashMap<>(); + env.put("CNB_GROUP_ID", "456"); + assertThatIllegalStateException().isThrownBy(() -> BuildOwner.fromEnv(env)) + .withMessage("Missing 'CNB_USER_ID' value from the builder environment"); + } + + @Test + void fromEnvWhenGroupPropertyIsMissingThrowsException() { + Map env = new LinkedHashMap<>(); + env.put("CNB_USER_ID", "123"); + assertThatIllegalStateException().isThrownBy(() -> BuildOwner.fromEnv(env)) + .withMessage("Missing 'CNB_GROUP_ID' value from the builder environment"); + } + + @Test + void fromEnvWhenUserPropertyIsMalformedThrowsException() { + Map env = new LinkedHashMap<>(); + env.put("CNB_USER_ID", "nope"); + env.put("CNB_GROUP_ID", "456"); + assertThatIllegalStateException().isThrownBy(() -> BuildOwner.fromEnv(env)) + .withMessage("Malformed 'CNB_USER_ID' value 'nope' in the builder environment"); + } + + @Test + void fromEnvWhenGroupPropertyIsMalformedThrowsException() { + Map env = new LinkedHashMap<>(); + env.put("CNB_USER_ID", "123"); + env.put("CNB_GROUP_ID", "nope"); + assertThatIllegalStateException().isThrownBy(() -> BuildOwner.fromEnv(env)) + .withMessage("Malformed 'CNB_GROUP_ID' value 'nope' in the builder environment"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/BuildRequestTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/BuildRequestTests.java new file mode 100644 index 0000000000..23fc47d5e4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/BuildRequestTests.java @@ -0,0 +1,167 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.build; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.cloudnativebuildpack.docker.type.ImageReference; +import org.springframework.boot.cloudnativebuildpack.io.Owner; +import org.springframework.boot.cloudnativebuildpack.io.TarArchive; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.entry; + +/** + * Tests for {@link BuildRequest}. + * + * @author Phillip Webb + */ +public class BuildRequestTests { + + @TempDir + File tempDir; + + @Test + void forJarFileReturnsRequest() throws IOException { + File jarFile = new File(this.tempDir, "my-app-0.0.1.jar"); + writeTestJarFile(jarFile); + BuildRequest request = BuildRequest.forJarFile(jarFile); + assertThat(request.getName().toString()).isEqualTo("docker.io/library/my-app:0.0.1"); + assertThat(request.getBuilder().toString()).isEqualTo("docker.io/cloudfoundry/cnb:0.0.43-bionic"); + assertThat(request.getApplicationContent(Owner.ROOT)).satisfies(this::hasExpectedJarContent); + assertThat(request.getEnv()).isEmpty(); + } + + @Test + void forJarFileWithNameReturnsRequest() throws IOException { + File jarFile = new File(this.tempDir, "my-app-0.0.1.jar"); + writeTestJarFile(jarFile); + BuildRequest request = BuildRequest.forJarFile(ImageReference.of("test-app"), jarFile); + assertThat(request.getName().toString()).isEqualTo("docker.io/library/test-app:latest"); + assertThat(request.getBuilder().toString()).isEqualTo("docker.io/cloudfoundry/cnb:0.0.43-bionic"); + assertThat(request.getApplicationContent(Owner.ROOT)).satisfies(this::hasExpectedJarContent); + assertThat(request.getEnv()).isEmpty(); + } + + @Test + void forJarFileWhenJarFileIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> BuildRequest.forJarFile(null)) + .withMessage("JarFile must not be null"); + } + + @Test + void forJarFileWhenJarFileIsMissingThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> BuildRequest.forJarFile(new File(this.tempDir, "missing.jar"))) + .withMessage("JarFile must exist"); + + } + + @Test + void forJarFileWhenJarFileIsFolderThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> BuildRequest.forJarFile(this.tempDir)) + .withMessage("JarFile must be a file"); + } + + @Test + void withBuilderUpdatesBuilder() throws IOException { + BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")) + .withBuilder(ImageReference.of("spring/builder")); + assertThat(request.getBuilder().toString()).isEqualTo("docker.io/spring/builder:latest"); + } + + @Test + void withEnvAddsEnvEntry() throws IOException { + BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); + BuildRequest withEnv = request.withEnv("spring", "boot"); + assertThat(request.getEnv()).isEmpty(); + assertThat(withEnv.getEnv()).containsExactly(entry("spring", "boot")); + } + + @Test + void withEnvMapAddsEnvEntries() throws IOException { + BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); + Map env = new LinkedHashMap<>(); + env.put("spring", "boot"); + env.put("test", "test"); + BuildRequest withEnv = request.withEnv(env); + assertThat(request.getEnv()).isEmpty(); + assertThat(withEnv.getEnv()).containsExactly(entry("spring", "boot"), entry("test", "test")); + } + + @Test + void withEnvWhenKeyIsNullThrowsException() throws IOException { + BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); + assertThatIllegalArgumentException().isThrownBy(() -> request.withEnv(null, "test")) + .withMessage("Name must not be empty"); + } + + @Test + void withEnvWhenValueIsNullThrowsException() throws IOException { + BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); + assertThatIllegalArgumentException().isThrownBy(() -> request.withEnv("test", null)) + .withMessage("Value must not be empty"); + } + + private void hasExpectedJarContent(TarArchive archive) { + try { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + archive.writeTo(outputStream); + try (TarArchiveInputStream tar = new TarArchiveInputStream( + new ByteArrayInputStream(outputStream.toByteArray()))) { + assertThat(tar.getNextEntry().getName()).isEqualTo("spring/"); + assertThat(tar.getNextEntry().getName()).isEqualTo("spring/boot"); + assertThat(tar.getNextEntry()).isNull(); + } + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + private File writeTestJarFile(String name) throws IOException { + File file = new File(this.tempDir, name); + writeTestJarFile(file); + return file; + } + + private void writeTestJarFile(File file) throws IOException { + try (ZipArchiveOutputStream zip = new ZipArchiveOutputStream(file)) { + ZipArchiveEntry dirEntry = new ZipArchiveEntry("spring/"); + zip.putArchiveEntry(dirEntry); + zip.closeArchiveEntry(); + ZipArchiveEntry fileEntry = new ZipArchiveEntry("spring/boot"); + zip.putArchiveEntry(fileEntry); + zip.write("test".getBytes(StandardCharsets.UTF_8)); + zip.closeArchiveEntry(); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/BuilderMetadataTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/BuilderMetadataTests.java new file mode 100644 index 0000000000..d8b1a0cb8a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/BuilderMetadataTests.java @@ -0,0 +1,97 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.build; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.cloudnativebuildpack.docker.type.Image; +import org.springframework.boot.cloudnativebuildpack.docker.type.ImageConfig; +import org.springframework.boot.cloudnativebuildpack.json.AbstractJsonTests; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link BuilderMetadata}. + * + * @author Phillip Webb + */ +class BuilderMetadataTests extends AbstractJsonTests { + + @Test + void fromImageLoadsMetadata() throws IOException { + Image image = Image.of(getContent("image.json")); + BuilderMetadata metadata = BuilderMetadata.fromImage(image); + assertThat(metadata.getStack().getRunImage().getImage()).isEqualTo("cloudfoundry/run:full-cnb"); + assertThat(metadata.getStack().getRunImage().getMirrors()).isEmpty(); + assertThat(metadata.getLifecycle().getVersion()).isEqualTo("0.5.0"); + assertThat(metadata.getLifecycle().getApi().getBuildpack()).isEqualTo("0.2"); + assertThat(metadata.getLifecycle().getApi().getPlatform()).isEqualTo("0.1"); + assertThat(metadata.getCreatedBy().getName()).isEqualTo("Pack CLI"); + assertThat(metadata.getCreatedBy().getVersion()) + .isEqualTo("v0.5.0 (git sha: c9cfac75b49609524e1ea33f809c12071406547c)"); + } + + @Test + void fromImageWhenImageIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> BuilderMetadata.fromImage(null)) + .withMessage("Image must not be null"); + } + + @Test + void fromImageWhenImageConfigIsNullThrowsException() { + Image image = mock(Image.class); + assertThatIllegalArgumentException().isThrownBy(() -> BuilderMetadata.fromImage(image)) + .withMessage("ImageConfig must not be null"); + } + + @Test + void fromImageConfigWhenLabelIsMissingThrowsException() { + Image image = mock(Image.class); + ImageConfig imageConfig = mock(ImageConfig.class); + given(image.getConfig()).willReturn(imageConfig); + assertThatIllegalArgumentException().isThrownBy(() -> BuilderMetadata.fromImage(image)) + .withMessage("No 'io.buildpacks.builder.metadata' label found in image config"); + } + + @Test + void copyWithUpdatedCreatedByReturnsNewMetadata() throws IOException { + Image image = Image.of(getContent("image.json")); + BuilderMetadata metadata = BuilderMetadata.fromImage(image); + BuilderMetadata copy = metadata.copy((update) -> update.withCreatedBy("test123", "test456")); + assertThat(copy).isNotSameAs(metadata); + assertThat(copy.getCreatedBy().getName()).isEqualTo("test123"); + assertThat(copy.getCreatedBy().getVersion()).isEqualTo("test456"); + } + + @Test + void attachToUpdatesMetadata() throws IOException { + Image image = Image.of(getContent("image.json")); + ImageConfig imageConfig = image.getConfig(); + BuilderMetadata metadata = BuilderMetadata.fromImage(image); + ImageConfig imageConfigCopy = imageConfig.copy(metadata::attachTo); + String label = imageConfigCopy.getLabels().get("io.buildpacks.builder.metadata"); + BuilderMetadata metadataCopy = BuilderMetadata.fromJson(label); + assertThat(metadataCopy.getStack().getRunImage().getImage()) + .isEqualTo(metadata.getStack().getRunImage().getImage()); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/BuilderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/BuilderTests.java new file mode 100644 index 0000000000..f8884e7cbb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/BuilderTests.java @@ -0,0 +1,151 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.build; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; + +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.stubbing.Answer; + +import org.springframework.boot.cloudnativebuildpack.docker.DockerApi; +import org.springframework.boot.cloudnativebuildpack.docker.DockerApi.ContainerApi; +import org.springframework.boot.cloudnativebuildpack.docker.DockerApi.ImageApi; +import org.springframework.boot.cloudnativebuildpack.docker.DockerApi.VolumeApi; +import org.springframework.boot.cloudnativebuildpack.docker.TotalProgressPullListener; +import org.springframework.boot.cloudnativebuildpack.docker.type.Image; +import org.springframework.boot.cloudnativebuildpack.docker.type.ImageArchive; +import org.springframework.boot.cloudnativebuildpack.docker.type.ImageReference; +import org.springframework.boot.cloudnativebuildpack.io.TarArchive; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link Builder}. + * + * @author Phillip Webb + */ +class BuilderTests { + + @Test + void createWhenLogIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new Builder(null)).withMessage("Log must not be null"); + } + + @Test + void buildWhenRequestIsNullThrowsException() { + Builder builder = new Builder(); + assertThatIllegalArgumentException().isThrownBy(() -> builder.build(null)) + .withMessage("Request must not be null"); + } + + @Test + void buildInvokesBuildpack() throws Exception { + TestPrintStream out = new TestPrintStream(); + DockerApi docker = mockDockerApi(); + Image builderImage = loadImage("image.json"); + Image runImage = loadImage("run-image.json"); + given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/cnb:0.0.43-bionic")), any())) + .willAnswer(withPulledImage(builderImage)); + given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:full-cnb")), any())) + .willAnswer(withPulledImage(runImage)); + Builder builder = new Builder(BuildLog.to(out), docker); + BuildRequest request = getTestRequest(); + builder.build(request); + assertThat(out.toString()).contains("Running detector"); + assertThat(out.toString()).contains("Running restorer"); + assertThat(out.toString()).contains("Running analyzer"); + assertThat(out.toString()).contains("Running builder"); + assertThat(out.toString()).contains("Running exporter"); + assertThat(out.toString()).contains("Running cacher"); + assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); + ArgumentCaptor archive = ArgumentCaptor.forClass(ImageArchive.class); + verify(docker.image()).load(archive.capture(), any()); + verify(docker.image()).remove(archive.getValue().getTag(), true); + } + + @Test + void buildWhenStackIdDoesNotMatchThrowsException() throws Exception { + TestPrintStream out = new TestPrintStream(); + DockerApi docker = mockDockerApi(); + Image builderImage = loadImage("image.json"); + Image runImage = loadImage("run-image-with-bad-stack.json"); + given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/cnb:0.0.43-bionic")), any())) + .willAnswer(withPulledImage(builderImage)); + given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:full-cnb")), any())) + .willAnswer(withPulledImage(runImage)); + Builder builder = new Builder(BuildLog.to(out), docker); + BuildRequest request = getTestRequest(); + assertThatIllegalStateException().isThrownBy(() -> builder.build(request)).withMessage( + "Run image stack 'org.cloudfoundry.stacks.cfwindowsfs3' does not match builder stack 'org.cloudfoundry.stacks.cflinuxfs3'"); + } + + private DockerApi mockDockerApi() { + DockerApi docker = mock(DockerApi.class); + ImageApi imageApi = mock(ImageApi.class); + ContainerApi containerApi = mock(ContainerApi.class); + VolumeApi volumeApi = mock(VolumeApi.class); + given(docker.image()).willReturn(imageApi); + given(docker.container()).willReturn(containerApi); + given(docker.volume()).willReturn(volumeApi); + return docker; + } + + private BuildRequest getTestRequest() { + TarArchive content = mock(TarArchive.class); + ImageReference name = ImageReference.of("my-application"); + BuildRequest request = BuildRequest.of(name, (owner) -> content); + return request; + } + + private Image loadImage(String name) throws IOException { + return Image.of(getClass().getResourceAsStream(name)); + } + + private Answer withPulledImage(Image image) { + return (invocation) -> { + TotalProgressPullListener listener = invocation.getArgument(1, TotalProgressPullListener.class); + listener.onStart(); + listener.onFinish(); + return image; + }; + + } + + static class TestPrintStream extends PrintStream { + + TestPrintStream() { + super(new ByteArrayOutputStream()); + } + + @Override + public String toString() { + return this.out.toString(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/EphemeralBuilderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/EphemeralBuilderTests.java new file mode 100644 index 0000000000..fad47b7f1c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/EphemeralBuilderTests.java @@ -0,0 +1,164 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.build; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneOffset; +import java.util.Collections; +import java.util.Map; + +import org.apache.commons.compress.archivers.ArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.utils.IOUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.cloudnativebuildpack.docker.type.Image; +import org.springframework.boot.cloudnativebuildpack.docker.type.ImageArchive; +import org.springframework.boot.cloudnativebuildpack.docker.type.ImageConfig; +import org.springframework.boot.cloudnativebuildpack.docker.type.ImageReference; +import org.springframework.boot.cloudnativebuildpack.json.AbstractJsonTests; +import org.springframework.util.FileCopyUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link EphemeralBuilder}. + * + * @author Phillip Webb + */ +class EphemeralBuilderTests extends AbstractJsonTests { + + @TempDir + File temp; + + private final BuildOwner owner = BuildOwner.of(123, 456); + + private Image image; + + private BuilderMetadata metadata; + + private Map env; + + @BeforeEach + void setup() throws Exception { + this.image = Image.of(getContent("image.json")); + this.metadata = BuilderMetadata.fromImage(this.image); + this.env = Collections.singletonMap("spring", "boot"); + } + + @Test + void getNameHasRandomName() throws Exception { + EphemeralBuilder b1 = new EphemeralBuilder(this.owner, this.image, this.metadata, this.env); + EphemeralBuilder b2 = new EphemeralBuilder(this.owner, this.image, this.metadata, this.env); + assertThat(b1.getName().toString()).startsWith("pack.local/builder/").endsWith(":latest"); + assertThat(b1.getName().toString()).isNotEqualTo(b2.getName().toString()); + } + + @Test + void getArchiveHasCreatedByConfig() throws Exception { + EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.metadata, this.env); + ImageConfig config = builder.getArchive().getImageConfig(); + BuilderMetadata ephemeralMetadata = BuilderMetadata.fromImageConfig(config); + assertThat(ephemeralMetadata.getCreatedBy().getName()).isEqualTo("Spring Boot"); + } + + @Test + void getArchiveHasTag() throws Exception { + EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.metadata, this.env); + ImageReference tag = builder.getArchive().getTag(); + assertThat(tag.toString()).startsWith("pack.local/builder/").endsWith(":latest"); + } + + @Test + void getArchiveHasCreateDate() throws Exception { + Clock clock = Clock.fixed(Instant.now(), ZoneOffset.UTC); + EphemeralBuilder builder = new EphemeralBuilder(clock, this.owner, this.image, this.metadata, this.env); + assertThat(builder.getArchive().getCreateDate()).isEqualTo(Instant.now(clock)); + } + + @Test + void getArchiveContainsDefaultDirsLayer() throws Exception { + EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.metadata, this.env); + File folder = unpack(getLayer(builder.getArchive(), 0), "dirs"); + assertThat(new File(folder, "workspace")).isDirectory(); + assertThat(new File(folder, "layers")).isDirectory(); + assertThat(new File(folder, "cnb")).isDirectory(); + assertThat(new File(folder, "cnb/buildpacks")).isDirectory(); + assertThat(new File(folder, "platform")).isDirectory(); + assertThat(new File(folder, "platform/env")).isDirectory(); + } + + @Test + void getArchiveContainsStackLayer() throws Exception { + EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.metadata, this.env); + File folder = unpack(getLayer(builder.getArchive(), 1), "stack"); + File tomlFile = new File(folder, "cnb/stack.toml"); + assertThat(tomlFile).exists(); + String toml = FileCopyUtils + .copyToString(new InputStreamReader(new FileInputStream(tomlFile), StandardCharsets.UTF_8)); + assertThat(toml).contains("[run-image]").contains("image = "); + } + + @Test + void getArchiveContainsEnvLayer() throws Exception { + EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.metadata, this.env); + File folder = unpack(getLayer(builder.getArchive(), 2), "env"); + assertThat(new File(folder, "platform/env/spring")).usingCharset(StandardCharsets.UTF_8).hasContent("boot"); + } + + private TarArchiveInputStream getLayer(ImageArchive archive, int index) throws Exception { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + archive.writeTo(outputStream); + TarArchiveInputStream tar = new TarArchiveInputStream(new ByteArrayInputStream(outputStream.toByteArray())); + for (int i = 0; i <= index; i++) { + tar.getNextEntry(); + } + return new TarArchiveInputStream(tar); + } + + private File unpack(TarArchiveInputStream archive, String name) throws Exception { + File folder = new File(this.temp, name); + folder.mkdirs(); + ArchiveEntry entry = archive.getNextEntry(); + while (entry != null) { + File file = new File(folder, entry.getName()); + if (entry.isDirectory()) { + file.mkdirs(); + } + else { + file.getParentFile().mkdirs(); + try (OutputStream out = new FileOutputStream(file)) { + IOUtils.copy(archive, out); + } + } + entry = archive.getNextEntry(); + } + return folder; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/LifecycleTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/LifecycleTests.java new file mode 100644 index 0000000000..5b2cf55b6c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/LifecycleTests.java @@ -0,0 +1,225 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.build; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.Map; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.stubbing.Answer; + +import org.springframework.boot.cloudnativebuildpack.docker.DockerApi; +import org.springframework.boot.cloudnativebuildpack.docker.DockerApi.ContainerApi; +import org.springframework.boot.cloudnativebuildpack.docker.DockerApi.ImageApi; +import org.springframework.boot.cloudnativebuildpack.docker.DockerApi.VolumeApi; +import org.springframework.boot.cloudnativebuildpack.docker.type.ContainerConfig; +import org.springframework.boot.cloudnativebuildpack.docker.type.ContainerContent; +import org.springframework.boot.cloudnativebuildpack.docker.type.ContainerReference; +import org.springframework.boot.cloudnativebuildpack.docker.type.ImageReference; +import org.springframework.boot.cloudnativebuildpack.docker.type.VolumeName; +import org.springframework.boot.cloudnativebuildpack.io.IOConsumer; +import org.springframework.boot.cloudnativebuildpack.io.TarArchive; +import org.springframework.boot.cloudnativebuildpack.json.SharedObjectMapper; +import org.springframework.util.FileCopyUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link Lifecycle}. + * + * @author Phillip Webb + */ +class LifecycleTests { + + private TestPrintStream out; + + private DockerApi docker; + + private Lifecycle lifecycle; + + private Map configs = new LinkedHashMap<>(); + + private Map content = new LinkedHashMap<>(); + + @BeforeEach + void setup() throws Exception { + this.out = new TestPrintStream(); + this.docker = mockDockerApi(); + BuildRequest request = getTestRequest(); + this.lifecycle = createLifecycle(request); + } + + private DockerApi mockDockerApi() { + DockerApi docker = mock(DockerApi.class); + ImageApi imageApi = mock(ImageApi.class); + ContainerApi containerApi = mock(ContainerApi.class); + VolumeApi volumeApi = mock(VolumeApi.class); + given(docker.image()).willReturn(imageApi); + given(docker.container()).willReturn(containerApi); + given(docker.volume()).willReturn(volumeApi); + return docker; + } + + @Test + void executeExecutesPhases() throws Exception { + given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); + this.lifecycle.execute(); + assertPhaseWasRun("detector", withExpectedConfig("lifecycle-detector.json")); + assertPhaseWasRun("restorer", withExpectedConfig("lifecycle-restorer.json")); + assertPhaseWasRun("analyzer", withExpectedConfig("lifecycle-analyzer.json")); + assertPhaseWasRun("builder", withExpectedConfig("lifecycle-builder.json")); + assertPhaseWasRun("exporter", withExpectedConfig("lifecycle-exporter.json")); + assertPhaseWasRun("cacher", withExpectedConfig("lifecycle-cacher.json")); + assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); + } + + @Test + void executeOnlyUploadsContentOnce() throws Exception { + given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); + this.lifecycle.execute(); + assertThat(this.content).hasSize(1); + } + + @Test + void executeWhenAleadyRunThrowsException() throws Exception { + given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); + this.lifecycle.execute(); + assertThatIllegalStateException().isThrownBy(this.lifecycle::execute) + .withMessage("Lifecycle has already been executed"); + } + + @Test + void executeWhenCleanCacheClearsCache() throws Exception { + given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); + BuildRequest request = getTestRequest().withCleanCache(true); + createLifecycle(request).execute(); + VolumeName name = VolumeName.of("pack-cache-b35197ac41ea.build"); + verify(this.docker.volume()).delete(name, true); + } + + @Test + void closeClearsVolumes() throws Exception { + this.lifecycle.close(); + verify(this.docker.volume()).delete(VolumeName.of("pack-layers-aaaaaaaaaa"), true); + verify(this.docker.volume()).delete(VolumeName.of("pack-app-aaaaaaaaaa"), true); + } + + private BuildRequest getTestRequest() { + TarArchive content = mock(TarArchive.class); + ImageReference name = ImageReference.of("my-application"); + BuildRequest request = BuildRequest.of(name, (owner) -> content); + return request; + } + + private Lifecycle createLifecycle(BuildRequest request) throws IOException { + EphemeralBuilder builder = mockEphemeralBuilder(); + return new TestLifecycle(BuildLog.to(this.out), this.docker, request, ImageReference.of("cloudfoundry/run"), + builder); + } + + private EphemeralBuilder mockEphemeralBuilder() throws IOException { + EphemeralBuilder builder = mock(EphemeralBuilder.class); + byte[] metadataContent = FileCopyUtils.copyToByteArray(getClass().getResourceAsStream("builder-metadata.json")); + BuilderMetadata metadata = BuilderMetadata.fromJson(new String(metadataContent, StandardCharsets.UTF_8)); + given(builder.getName()).willReturn(ImageReference.of("pack.local/ephemeral-builder")); + given(builder.getBuilderMetadata()).willReturn(metadata); + return builder; + } + + private Answer answerWithGeneratedContainerId() { + return (invocation) -> { + ContainerConfig config = invocation.getArgument(0, ContainerConfig.class); + ArrayNode command = getCommand(config); + String name = command.get(0).asText().substring(1).replaceAll("/", "-"); + this.configs.put(name, config); + if (invocation.getArguments().length > 1) { + this.content.put(name, invocation.getArgument(1, ContainerContent.class)); + } + return ContainerReference.of(name); + }; + } + + private ArrayNode getCommand(ContainerConfig config) throws JsonProcessingException, JsonMappingException { + JsonNode node = SharedObjectMapper.get().readTree(config.toString()); + return (ArrayNode) node.at("/Cmd"); + } + + private void assertPhaseWasRun(String name, IOConsumer configConsumer) throws IOException { + ContainerReference containerReference = ContainerReference.of("lifecycle-" + name); + verify(this.docker.container()).start(containerReference); + verify(this.docker.container()).logs(eq(containerReference), any()); + verify(this.docker.container()).remove(containerReference, true); + configConsumer.accept(this.configs.get(containerReference.toString())); + } + + private IOConsumer withExpectedConfig(String name) { + return (config) -> { + InputStream in = getClass().getResourceAsStream(name); + String json = FileCopyUtils.copyToString(new InputStreamReader(in, StandardCharsets.UTF_8)); + assertThat(config.toString()).isEqualToIgnoringWhitespace(json); + }; + } + + static class TestLifecycle extends Lifecycle { + + TestLifecycle(BuildLog log, DockerApi docker, BuildRequest request, ImageReference runImageReferece, + EphemeralBuilder builder) { + super(log, docker, request, runImageReferece, builder); + } + + @Override + protected VolumeName createRandomVolumeName(String prefix) { + return VolumeName.of(prefix + "aaaaaaaaaa"); + } + + } + + static class TestPrintStream extends PrintStream { + + TestPrintStream() { + super(new ByteArrayOutputStream()); + } + + @Override + public String toString() { + return this.out.toString(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/LifecycleVersionTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/LifecycleVersionTests.java new file mode 100644 index 0000000000..3033e1e2a8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/LifecycleVersionTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.build; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link LifecycleVersion}. + * + * @author Phillip Webb + */ +class LifecycleVersionTests { + + @Test + void parseWhenValueIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> LifecycleVersion.parse(null)) + .withMessage("Value must not be empty"); + } + + @Test + void parseWhenTooLongThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> LifecycleVersion.parse("v1.2.3.4")) + .withMessage("Malformed version number '1.2.3.4'"); + } + + @Test + void parseWhenNonNumericThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> LifecycleVersion.parse("v1.2.3a")) + .withMessage("Malformed version number '1.2.3a'"); + } + + @Test + void compareTo() { + LifecycleVersion v4 = LifecycleVersion.parse("0.0.4"); + assertThat(LifecycleVersion.parse("0.0.3").compareTo(v4)).isNegative(); + assertThat(LifecycleVersion.parse("0.0.4").compareTo(v4)).isZero(); + assertThat(LifecycleVersion.parse("0.0.5").compareTo(v4)).isPositive(); + } + + @Test + void isEqualOrGreaterThan() { + LifecycleVersion v4 = LifecycleVersion.parse("0.0.4"); + assertThat(LifecycleVersion.parse("0.0.3").isEqualOrGreaterThan(v4)).isFalse(); + assertThat(LifecycleVersion.parse("0.0.4").isEqualOrGreaterThan(v4)).isTrue(); + assertThat(LifecycleVersion.parse("0.0.5").isEqualOrGreaterThan(v4)).isTrue(); + } + + @Test + void parseReturnsVersion() { + assertThat(LifecycleVersion.parse("1.2.3").toString()).isEqualTo("v1.2.3"); + assertThat(LifecycleVersion.parse("1.2").toString()).isEqualTo("v1.2.0"); + assertThat(LifecycleVersion.parse("1").toString()).isEqualTo("v1.0.0"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/PhaseTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/PhaseTests.java new file mode 100644 index 0000000000..0ca0e63cf6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/PhaseTests.java @@ -0,0 +1,119 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.build; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.cloudnativebuildpack.docker.type.ContainerConfig.Update; +import org.springframework.boot.cloudnativebuildpack.docker.type.VolumeName; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +/** + * Tests for {@link Phase}. + * + * @author Phillip Webb + */ +class PhaseTests { + + private static final String[] NO_ARGS = {}; + + @Test + void getNameReturnsName() { + Phase phase = new Phase("test", false); + assertThat(phase.getName()).isEqualTo("test"); + } + + @Test + void toStringReturnsName() { + Phase phase = new Phase("test", false); + assertThat(phase).hasToString("test"); + } + + @Test + void applyUpdatesConfiguration() { + Phase phase = new Phase("test", false); + Update update = mock(Update.class); + phase.apply(update); + verify(update).withCommand("/lifecycle/test", NO_ARGS); + verify(update).withLabel("author", "spring-boot"); + verifyNoMoreInteractions(update); + } + + @Test + void applyWhenWithDaemonAccessUpdatesConfigurationWithRootUserAndDomainSocketBinding() { + Phase phase = new Phase("test", false); + phase.withDaemonAccess(); + Update update = mock(Update.class); + phase.apply(update); + verify(update).withUser("root"); + verify(update).withBind("/var/run/docker.sock", "/var/run/docker.sock"); + verify(update).withCommand("/lifecycle/test", NO_ARGS); + verify(update).withLabel("author", "spring-boot"); + verifyNoMoreInteractions(update); + } + + @Test + void applyWhenWithLogLevelArgAndVerboseLoggingUpdatesConfigurationWithLogLevel() { + Phase phase = new Phase("test", true); + phase.withLogLevelArg(); + Update update = mock(Update.class); + phase.apply(update); + verify(update).withCommand("/lifecycle/test", "-log-level", "debug"); + verify(update).withLabel("author", "spring-boot"); + verifyNoMoreInteractions(update); + } + + @Test + void applyWhenWithLogLevelArgAndNonVerboseLoggingDoesNotUpdateLogLevel() { + Phase phase = new Phase("test", false); + phase.withLogLevelArg(); + Update update = mock(Update.class); + phase.apply(update); + verify(update).withCommand("/lifecycle/test"); + verify(update).withLabel("author", "spring-boot"); + verifyNoMoreInteractions(update); + } + + @Test + void applyWhenWithArgsUpdatesConfigurationWithArguments() { + Phase phase = new Phase("test", false); + phase.withArgs("a", "b", "c"); + Update update = mock(Update.class); + phase.apply(update); + verify(update).withCommand("/lifecycle/test", "a", "b", "c"); + verify(update).withLabel("author", "spring-boot"); + verifyNoMoreInteractions(update); + } + + @Test + void applyWhenWithBindsUpdatesConfigurationWithBinds() { + Phase phase = new Phase("test", false); + VolumeName volumeName = VolumeName.of("test"); + phase.withBinds(volumeName, "/test"); + Update update = mock(Update.class); + phase.apply(update); + verify(update).withCommand("/lifecycle/test"); + verify(update).withLabel("author", "spring-boot"); + verify(update).withBind(volumeName, "/test"); + verifyNoMoreInteractions(update); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/PrintStreamBuildLogTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/PrintStreamBuildLogTests.java new file mode 100644 index 0000000000..be0fc39ffe --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/PrintStreamBuildLogTests.java @@ -0,0 +1,99 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.build; + +import java.io.ByteArrayOutputStream; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.cloudnativebuildpack.docker.LogUpdateEvent; +import org.springframework.boot.cloudnativebuildpack.docker.TotalProgressEvent; +import org.springframework.boot.cloudnativebuildpack.docker.type.Image; +import org.springframework.boot.cloudnativebuildpack.docker.type.ImageReference; +import org.springframework.boot.cloudnativebuildpack.docker.type.VolumeName; +import org.springframework.util.FileCopyUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link PrintStreamBuildLog}. + * + * @author Phillip Webb + */ +class PrintStreamBuildLogTests { + + @Test + void printsExpectedOutput() throws Exception { + TestPrintStream out = new TestPrintStream(); + PrintStreamBuildLog log = new PrintStreamBuildLog(out); + BuildRequest request = mock(BuildRequest.class); + ImageReference name = ImageReference.of("my-app:latest"); + ImageReference builderImageReference = ImageReference.of("cnb/builder"); + Image builderImage = mock(Image.class); + given(builderImage.getDigests()).willReturn(Collections.singletonList("00000001")); + ImageReference runImageReference = ImageReference.of("cnb/runner"); + Image runImage = mock(Image.class); + given(runImage.getDigests()).willReturn(Collections.singletonList("00000002")); + given(request.getName()).willReturn(name); + log.start(request); + Consumer pullBuildImageConsumer = log.pullingBuilder(request, builderImageReference); + pullBuildImageConsumer.accept(new TotalProgressEvent(100)); + log.pulledBulder(request, builderImage); + Consumer pullRunImageConsumer = log.pullingRunImage(request, runImageReference); + pullRunImageConsumer.accept(new TotalProgressEvent(100)); + log.pulledRunImage(request, runImage); + log.executingLifecycle(request, LifecycleVersion.parse("0.5"), VolumeName.of("pack-abc.cache")); + Consumer phase1Consumer = log.runningPhase(request, "alphabet"); + phase1Consumer.accept(mockLogEvent("one")); + phase1Consumer.accept(mockLogEvent("two")); + phase1Consumer.accept(mockLogEvent("three")); + Consumer phase2Consumer = log.runningPhase(request, "basket"); + phase2Consumer.accept(mockLogEvent("spring")); + phase2Consumer.accept(mockLogEvent("boot")); + log.executedLifecycle(request); + String expected = FileCopyUtils.copyToString(new InputStreamReader( + getClass().getResourceAsStream("print-stream-build-log.txt"), StandardCharsets.UTF_8)); + assertThat(out.toString()).isEqualToIgnoringNewLines(expected); + } + + private LogUpdateEvent mockLogEvent(String string) { + LogUpdateEvent event = mock(LogUpdateEvent.class); + given(event.toString()).willReturn(string); + return event; + } + + static class TestPrintStream extends PrintStream { + + TestPrintStream() { + super(new ByteArrayOutputStream()); + } + + @Override + public String toString() { + return this.out.toString(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/StackIdTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/StackIdTests.java new file mode 100644 index 0000000000..19ef2f77e0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/build/StackIdTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.build; + +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.cloudnativebuildpack.docker.type.Image; +import org.springframework.boot.cloudnativebuildpack.docker.type.ImageConfig; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link StackId}. + * + * @author Phillip Webb + */ +class StackIdTests { + + @Test + void fromImageWhenImageIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> StackId.fromImage(null)) + .withMessage("Image must not be null"); + } + + @Test + void fromImageWhenLabelIsMissingThrowsException() { + Image image = mock(Image.class); + ImageConfig imageConfig = mock(ImageConfig.class); + given(image.getConfig()).willReturn(imageConfig); + assertThatIllegalStateException().isThrownBy(() -> StackId.fromImage(image)) + .withMessage("Missing 'io.buildpacks.stack.id' stack label"); + } + + @Test + void fromImageCreatesStackId() { + Image image = mock(Image.class); + ImageConfig imageConfig = mock(ImageConfig.class); + given(image.getConfig()).willReturn(imageConfig); + given(imageConfig.getLabels()).willReturn(Collections.singletonMap("io.buildpacks.stack.id", "test")); + StackId stackId = StackId.fromImage(image); + assertThat(stackId.toString()).isEqualTo("test"); + } + + @Test + void ofCreatesStackId() { + StackId stackId = StackId.of("test"); + assertThat(stackId.toString()).isEqualTo("test"); + } + + @Test + void equalsAndHashCode() { + StackId s1 = StackId.of("a"); + StackId s2 = StackId.of("a"); + StackId s3 = StackId.of("b"); + assertThat(s1.hashCode()).isEqualTo(s2.hashCode()); + assertThat(s1).isEqualTo(s1).isEqualTo(s2).isNotEqualTo(s3); + } + + @Test + void toStringReturnsValue() { + StackId stackId = StackId.of("test"); + assertThat(stackId.toString()).isEqualTo("test"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/DockerApiIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/DockerApiIntegrationTests.java new file mode 100644 index 0000000000..fd77bfa1ff --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/DockerApiIntegrationTests.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.cloudnativebuildpack.docker.type.ImageReference; +import org.springframework.boot.testsupport.testcontainers.DisabledIfDockerUnavailable; + +/** + * Integration tests for {@link DockerApi}. + * + * @author Phillip Webb + */ +@DisabledIfDockerUnavailable +class DockerApiIntegrationTests { + + private final DockerApi docker = new DockerApi(); + + @Test + void pullImage() throws IOException { + this.docker.image().pull(ImageReference.of("cloudfoundry/cnb:bionic"), + new TotalProgressPullListener(new TotalProgressBar("Pulling: "))); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/DockerApiTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/DockerApiTests.java new file mode 100644 index 0000000000..b67656e076 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/DockerApiTests.java @@ -0,0 +1,393 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import org.springframework.boot.cloudnativebuildpack.docker.DockerApi.ContainerApi; +import org.springframework.boot.cloudnativebuildpack.docker.DockerApi.ImageApi; +import org.springframework.boot.cloudnativebuildpack.docker.DockerApi.VolumeApi; +import org.springframework.boot.cloudnativebuildpack.docker.Http.Response; +import org.springframework.boot.cloudnativebuildpack.docker.type.ContainerConfig; +import org.springframework.boot.cloudnativebuildpack.docker.type.ContainerContent; +import org.springframework.boot.cloudnativebuildpack.docker.type.ContainerReference; +import org.springframework.boot.cloudnativebuildpack.docker.type.Image; +import org.springframework.boot.cloudnativebuildpack.docker.type.ImageArchive; +import org.springframework.boot.cloudnativebuildpack.docker.type.ImageReference; +import org.springframework.boot.cloudnativebuildpack.docker.type.VolumeName; +import org.springframework.boot.cloudnativebuildpack.io.Content; +import org.springframework.boot.cloudnativebuildpack.io.IOConsumer; +import org.springframework.boot.cloudnativebuildpack.io.Owner; +import org.springframework.boot.cloudnativebuildpack.io.TarArchive; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link DockerApi}. + * + * @author Phillip Webb + */ +class DockerApiTests { + + private static final String API_URL = "docker://localhost/v1.40"; + + private static final String IMAGES_URL = API_URL + "/images"; + + private static final String CONTAINERS_URL = API_URL + "/containers"; + + private static final String VOLUMES_URL = API_URL + "/volumes"; + + @Mock + private HttpClientHttp httpClient; + + private DockerApi dockerApi; + + @BeforeEach + void setup() { + MockitoAnnotations.initMocks(this); + this.dockerApi = new DockerApi(this.httpClient); + } + + private HttpClientHttp httpClient() { + return this.httpClient; + } + + private Response emptyResponse() throws IOException { + return responseOf(null); + } + + private Response responseOf(String name) throws IOException { + return new Response() { + + @Override + public void close() throws IOException { + } + + @Override + public InputStream getContent() throws IOException { + if (name == null) { + return null; + } + return getClass().getResourceAsStream(name); + } + + }; + } + + @Nested + class ImageDockerApiTests { + + private ImageApi api; + + @Mock + private UpdateListener pullListener; + + @Mock + private UpdateListener loadListener; + + @Captor + private ArgumentCaptor> writer; + + @BeforeEach + void setup() { + MockitoAnnotations.initMocks(this); + this.api = DockerApiTests.this.dockerApi.image(); + } + + @Test + void pullWhenReferenceIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.api.pull(null, this.pullListener)) + .withMessage("Reference must not be null"); + } + + @Test + void pullWhenListenerIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.api.pull(ImageReference.of("ubuntu"), null)) + .withMessage("Listener must not be null"); + } + + @Test + void pullPullsImageAndProducesEvents() throws Exception { + ImageReference reference = ImageReference.of("cloudfoundry/cnb:bionic"); + URI createUri = new URI(IMAGES_URL + "/create?fromImage=docker.io%2Fcloudfoundry%2Fcnb%3Abionic"); + String imageHash = "4acb6bfd6c4f0cabaf7f3690e444afe51f1c7de54d51da7e63fac709c56f1c30"; + URI imageUri = new URI(IMAGES_URL + "/docker.io/cloudfoundry/cnb@sha256:" + imageHash + "/json"); + given(httpClient().post(createUri)).willReturn(responseOf("pull-stream.json")); + given(httpClient().get(imageUri)).willReturn(responseOf("type/image.json")); + Image image = this.api.pull(reference, this.pullListener); + assertThat(image.getLayers()).hasSize(46); + InOrder ordered = inOrder(this.pullListener); + ordered.verify(this.pullListener).onStart(); + ordered.verify(this.pullListener, times(595)).onUpdate(any()); + ordered.verify(this.pullListener).onFinish(); + } + + @Test + void loadWhenArchiveIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.api.load(null, UpdateListener.none())) + .withMessage("Archive must not be null"); + } + + @Test + void loadWhenListenerIsNullThrowsException() { + ImageArchive archive = mock(ImageArchive.class); + assertThatIllegalArgumentException().isThrownBy(() -> this.api.load(archive, null)) + .withMessage("Listener must not be null"); + } + + @Test + void loadLoadsImage() throws Exception { + Image image = Image.of(getClass().getResourceAsStream("type/image.json")); + ImageArchive archive = ImageArchive.from(image); + URI loadUri = new URI(IMAGES_URL + "/load"); + given(httpClient().post(eq(loadUri), eq("application/x-tar"), any())) + .willReturn(responseOf("load-stream.json")); + this.api.load(archive, this.loadListener); + InOrder ordered = inOrder(this.loadListener); + ordered.verify(this.loadListener).onStart(); + ordered.verify(this.loadListener).onUpdate(any()); + ordered.verify(this.loadListener).onFinish(); + verify(httpClient()).post(any(), any(), this.writer.capture()); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + this.writer.getValue().accept(out); + assertThat(out.toByteArray()).hasSizeGreaterThan(21000); + } + + @Test + void removeWhenReferenceIsNulllThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.api.remove(null, true)) + .withMessage("Reference must not be null"); + } + + @Test + void removeRemovesContainer() throws Exception { + ImageReference reference = ImageReference + .of("ubuntu@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); + URI removeUri = new URI(IMAGES_URL + + "/docker.io/library/ubuntu@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); + given(httpClient().delete(removeUri)).willReturn(emptyResponse()); + this.api.remove(reference, false); + verify(httpClient()).delete(removeUri); + } + + @Test + void removeWhenForceIsTrueRemovesContainer() throws Exception { + ImageReference reference = ImageReference + .of("ubuntu@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); + URI removeUri = new URI(IMAGES_URL + + "/docker.io/library/ubuntu@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d?force=1"); + given(httpClient().delete(removeUri)).willReturn(emptyResponse()); + this.api.remove(reference, true); + verify(httpClient()).delete(removeUri); + } + + } + + @Nested + class ContainerDockerApiTests { + + private ContainerApi api; + + @Captor + private ArgumentCaptor> writer; + + @Mock + private UpdateListener logListener; + + @BeforeEach + void setup() { + MockitoAnnotations.initMocks(this); + this.api = DockerApiTests.this.dockerApi.container(); + } + + @Test + void createWhenConfigIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.api.create(null)) + .withMessage("Config must not be null"); + } + + @Test + void createCreatesContainer() throws Exception { + ImageReference imageReference = ImageReference.of("ubuntu:bionic"); + ContainerConfig config = ContainerConfig.of(imageReference, (update) -> update.withCommand("/bin/bash")); + URI createUri = new URI(CONTAINERS_URL + "/create"); + given(httpClient().post(eq(createUri), eq("application/json"), any())) + .willReturn(responseOf("create-container-response.json")); + ContainerReference containerReference = this.api.create(config); + assertThat(containerReference.toString()).isEqualTo("e90e34656806"); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + verify(httpClient()).post(any(), any(), this.writer.capture()); + this.writer.getValue().accept(out); + assertThat(out.toByteArray()).hasSizeGreaterThan(130); + } + + @Test + void createWhenHasContentContainerWithContent() throws Exception { + ImageReference imageReference = ImageReference.of("ubuntu:bionic"); + ContainerConfig config = ContainerConfig.of(imageReference, (update) -> update.withCommand("/bin/bash")); + TarArchive archive = TarArchive.of((layout) -> { + layout.folder("/test", Owner.ROOT); + layout.file("/test/file", Owner.ROOT, Content.of("test")); + }); + ContainerContent content = ContainerContent.of(archive); + URI createUri = new URI(CONTAINERS_URL + "/create"); + given(httpClient().post(eq(createUri), eq("application/json"), any())) + .willReturn(responseOf("create-container-response.json")); + URI uploadUri = new URI(CONTAINERS_URL + "/e90e34656806/archive?path=%2F"); + given(httpClient().put(eq(uploadUri), eq("application/x-tar"), any())).willReturn(emptyResponse()); + ContainerReference containerReference = this.api.create(config, content); + assertThat(containerReference.toString()).isEqualTo("e90e34656806"); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + verify(httpClient()).post(any(), any(), this.writer.capture()); + this.writer.getValue().accept(out); + assertThat(out.toByteArray()).hasSizeGreaterThan(130); + verify(httpClient()).put(any(), any(), this.writer.capture()); + this.writer.getValue().accept(out); + assertThat(out.toByteArray()).hasSizeGreaterThan(2000); + } + + @Test + void startWhenReferenceIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.api.start(null)) + .withMessage("Reference must not be null"); + } + + @Test + void startStartsContainer() throws Exception { + ContainerReference reference = ContainerReference.of("e90e34656806"); + URI startContainerUri = new URI(CONTAINERS_URL + "/e90e34656806/start"); + given(httpClient().post(startContainerUri)).willReturn(emptyResponse()); + this.api.start(reference); + verify(httpClient()).post(startContainerUri); + } + + @Test + void logsWhenReferenceIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.api.logs(null, UpdateListener.none())) + .withMessage("Reference must not be null"); + } + + @Test + void logsWhenListenerIsNullThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.api.logs(ContainerReference.of("e90e34656806"), null)) + .withMessage("Listener must not be null"); + } + + @Test + void logsProducesEvents() throws Exception { + ContainerReference reference = ContainerReference.of("e90e34656806"); + URI logsUri = new URI(CONTAINERS_URL + "/e90e34656806/logs?stdout=1&stderr=1&follow=1"); + given(httpClient().get(logsUri)).willReturn(responseOf("log-update-event.stream")); + this.api.logs(reference, this.logListener); + InOrder ordered = inOrder(this.logListener); + ordered.verify(this.logListener).onStart(); + ordered.verify(this.logListener, times(7)).onUpdate(any()); + ordered.verify(this.logListener).onFinish(); + } + + @Test + void removeWhenReferenceIsNulllThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.api.remove(null, true)) + .withMessage("Reference must not be null"); + } + + @Test + void removeRemovesContainer() throws Exception { + ContainerReference reference = ContainerReference.of("e90e34656806"); + URI removeUri = new URI(CONTAINERS_URL + "/e90e34656806"); + given(httpClient().delete(removeUri)).willReturn(emptyResponse()); + this.api.remove(reference, false); + verify(httpClient()).delete(removeUri); + } + + @Test + void removeWhenForceIsTrueRemovesContainer() throws Exception { + ContainerReference reference = ContainerReference.of("e90e34656806"); + URI removeUri = new URI(CONTAINERS_URL + "/e90e34656806?force=1"); + given(httpClient().delete(removeUri)).willReturn(emptyResponse()); + this.api.remove(reference, true); + verify(httpClient()).delete(removeUri); + } + + } + + @Nested + class VolumeDockerApiTests { + + private VolumeApi api; + + @Captor + private ArgumentCaptor> writer; + + @Mock + private UpdateListener logListener; + + @BeforeEach + void setup() { + MockitoAnnotations.initMocks(this); + this.api = DockerApiTests.this.dockerApi.volume(); + } + + @Test + void deleteWhenNameIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.api.delete(null, false)) + .withMessage("Name must not be null"); + } + + @Test + void deleteDeletesContainer() throws Exception { + VolumeName name = VolumeName.of("test"); + URI removeUri = new URI(VOLUMES_URL + "/test"); + given(httpClient().delete(removeUri)).willReturn(emptyResponse()); + this.api.delete(name, false); + verify(httpClient()).delete(removeUri); + } + + @Test + void deleteWhenForceIsTrueDeletesContainer() throws Exception { + VolumeName name = VolumeName.of("test"); + URI removeUri = new URI(VOLUMES_URL + "/test?force=1"); + given(httpClient().delete(removeUri)).willReturn(emptyResponse()); + this.api.delete(name, true); + verify(httpClient()).delete(removeUri); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/DockerExceptionTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/DockerExceptionTests.java new file mode 100644 index 0000000000..c83904c4b4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/DockerExceptionTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link DockerException}. + * + * @author Phillip Webb + */ +class DockerExceptionTests { + + private static final URI URI; + static { + try { + URI = new URI("docker://localhost"); + } + catch (URISyntaxException ex) { + throw new IllegalStateException(ex); + } + } + + private static final Errors NO_ERRORS = new Errors(Collections.emptyList()); + + private static final Errors ERRORS = new Errors(Collections.singletonList(new Errors.Error("code", "message"))); + + @Test + void createWhenUriIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new DockerException(null, 404, null, NO_ERRORS)) + .withMessage("URI must not be null"); + } + + @Test + void create() { + DockerException exception = new DockerException(URI, 404, "missing", ERRORS); + assertThat(exception.getMessage()).isEqualTo( + "Docker API call to 'docker://localhost' failed with status code 404 \"missing\" [code: message]"); + assertThat(exception.getStatusCode()).isEqualTo(404); + assertThat(exception.getReasonPhrase()).isEqualTo("missing"); + assertThat(exception.getErrors()).isSameAs(ERRORS); + } + + @Test + void createWhenReasonPhraseIsNull() { + DockerException exception = new DockerException(URI, 404, null, ERRORS); + assertThat(exception.getMessage()) + .isEqualTo("Docker API call to 'docker://localhost' failed with status code 404 [code: message]"); + assertThat(exception.getStatusCode()).isEqualTo(404); + assertThat(exception.getReasonPhrase()).isNull(); + assertThat(exception.getErrors()).isSameAs(ERRORS); + } + + @Test + void createWhenErrorsIsNull() { + DockerException exception = new DockerException(URI, 404, "missing", null); + assertThat(exception.getErrors()).isNull(); + } + + @Test + void createWhenErrorsIsEmpty() { + DockerException exception = new DockerException(URI, 404, "missing", NO_ERRORS); + assertThat(exception.getMessage()) + .isEqualTo("Docker API call to 'docker://localhost' failed with status code 404 \"missing\""); + assertThat(exception.getStatusCode()).isEqualTo(404); + assertThat(exception.getReasonPhrase()).isEqualTo("missing"); + assertThat(exception.getErrors()).isSameAs(NO_ERRORS); + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/ErrorsTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/ErrorsTests.java new file mode 100644 index 0000000000..f7f3e84de8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/ErrorsTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker; + +import java.util.Iterator; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.cloudnativebuildpack.docker.Errors.Error; +import org.springframework.boot.cloudnativebuildpack.json.AbstractJsonTests; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link Errors}. + * + * @author Phillip Webb + */ +class ErrorsTests extends AbstractJsonTests { + + @Test + void readValueDeserializesJson() throws Exception { + Errors errors = this.getObjectMapper().readValue(getContent("errors.json"), Errors.class); + Iterator iterator = errors.iterator(); + Error error1 = iterator.next(); + Error error2 = iterator.next(); + assertThat(iterator.hasNext()).isFalse(); + assertThat(error1.getCode()).isEqualTo("TEST1"); + assertThat(error1.getMessage()).isEqualTo("Test One"); + assertThat(error2.getCode()).isEqualTo("TEST2"); + assertThat(error2.getMessage()).isEqualTo("Test Two"); + } + + @Test + void toStringHasErrorDetails() throws Exception { + Errors errors = this.getObjectMapper().readValue(getContent("errors.json"), Errors.class); + assertThat(errors.toString()).isEqualTo("[TEST1: Test One, TEST2: Test Two]"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/HttpClientHttpTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/HttpClientHttpTests.java new file mode 100644 index 0000000000..bf93f5c432 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/HttpClientHttpTests.java @@ -0,0 +1,194 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.charset.StandardCharsets; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpEntityEnclosingRequest; +import org.apache.http.HttpHeaders; +import org.apache.http.StatusLine; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.impl.client.CloseableHttpClient; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import org.springframework.boot.cloudnativebuildpack.docker.Http.Response; +import org.springframework.util.StreamUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link HttpClientHttp}. + * + * @author Phillip Webb + */ +class HttpClientHttpTests { + + private static final String APPLICATION_JSON = "application/json"; + + @Mock + private CloseableHttpClient client; + + @Mock + private CloseableHttpResponse response; + + @Mock + private StatusLine statusLine; + + @Mock + private HttpEntity entity; + + @Mock + private InputStream content; + + @Captor + private ArgumentCaptor requestCaptor; + + private HttpClientHttp http; + + private URI uri; + + @BeforeEach + void setup() throws Exception { + MockitoAnnotations.initMocks(this); + given(this.client.execute(any())).willReturn(this.response); + given(this.response.getEntity()).willReturn(this.entity); + given(this.response.getStatusLine()).willReturn(this.statusLine); + this.http = new HttpClientHttp(this.client); + this.uri = new URI("docker://localhost/example"); + } + + @Test + void getShouldExecuteHttpGet() throws Exception { + given(this.entity.getContent()).willReturn(this.content); + given(this.statusLine.getStatusCode()).willReturn(200); + Response response = this.http.get(this.uri); + verify(this.client).execute(this.requestCaptor.capture()); + HttpUriRequest request = this.requestCaptor.getValue(); + assertThat(request).isInstanceOf(HttpGet.class); + assertThat(request.getURI()).isEqualTo(this.uri); + assertThat(request.getFirstHeader(HttpHeaders.CONTENT_TYPE)).isNull(); + assertThat(response.getContent()).isSameAs(this.content); + } + + @Test + void postShouldExecuteHttpPost() throws Exception { + given(this.entity.getContent()).willReturn(this.content); + given(this.statusLine.getStatusCode()).willReturn(200); + Response response = this.http.post(this.uri); + verify(this.client).execute(this.requestCaptor.capture()); + HttpUriRequest request = this.requestCaptor.getValue(); + assertThat(request).isInstanceOf(HttpPost.class); + assertThat(request.getURI()).isEqualTo(this.uri); + assertThat(request.getFirstHeader(HttpHeaders.CONTENT_TYPE)).isNull(); + assertThat(response.getContent()).isSameAs(this.content); + } + + @Test + void postWithContentShouldExecuteHttpPost() throws Exception { + given(this.entity.getContent()).willReturn(this.content); + given(this.statusLine.getStatusCode()).willReturn(200); + Response response = this.http.post(this.uri, APPLICATION_JSON, + (out) -> StreamUtils.copy("test", StandardCharsets.UTF_8, out)); + verify(this.client).execute(this.requestCaptor.capture()); + HttpUriRequest request = this.requestCaptor.getValue(); + HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity(); + assertThat(request).isInstanceOf(HttpPost.class); + assertThat(request.getURI()).isEqualTo(this.uri); + assertThat(request.getFirstHeader(HttpHeaders.CONTENT_TYPE).getValue()).isEqualTo(APPLICATION_JSON); + assertThat(entity.isRepeatable()).isFalse(); + assertThat(entity.getContentLength()).isEqualTo(-1); + assertThat(entity.isStreaming()).isTrue(); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> entity.getContent()); + assertThat(writeToString(entity)).isEqualTo("test"); + assertThat(response.getContent()).isSameAs(this.content); + } + + @Test + void putWithContentShouldExecuteHttpPut() throws Exception { + given(this.entity.getContent()).willReturn(this.content); + given(this.statusLine.getStatusCode()).willReturn(200); + Response response = this.http.put(this.uri, APPLICATION_JSON, + (out) -> StreamUtils.copy("test", StandardCharsets.UTF_8, out)); + verify(this.client).execute(this.requestCaptor.capture()); + HttpUriRequest request = this.requestCaptor.getValue(); + HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity(); + assertThat(request).isInstanceOf(HttpPut.class); + assertThat(request.getURI()).isEqualTo(this.uri); + assertThat(request.getFirstHeader(HttpHeaders.CONTENT_TYPE).getValue()).isEqualTo(APPLICATION_JSON); + assertThat(entity.isRepeatable()).isFalse(); + assertThat(entity.getContentLength()).isEqualTo(-1); + assertThat(entity.isStreaming()).isTrue(); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> entity.getContent()); + assertThat(writeToString(entity)).isEqualTo("test"); + assertThat(response.getContent()).isSameAs(this.content); + } + + @Test + void deleteShouldExecuteHttpDelete() throws IOException { + given(this.entity.getContent()).willReturn(this.content); + given(this.statusLine.getStatusCode()).willReturn(200); + Response response = this.http.delete(this.uri); + verify(this.client).execute(this.requestCaptor.capture()); + HttpUriRequest request = this.requestCaptor.getValue(); + assertThat(request).isInstanceOf(HttpDelete.class); + assertThat(request.getURI()).isEqualTo(this.uri); + assertThat(request.getFirstHeader(HttpHeaders.CONTENT_TYPE)).isNull(); + assertThat(response.getContent()).isSameAs(this.content); + } + + @Test + void executeWhenResposeIsIn400RangeShouldThrowDockerException() throws ClientProtocolException, IOException { + given(this.entity.getContent()).willReturn(getClass().getResourceAsStream("errors.json")); + given(this.statusLine.getStatusCode()).willReturn(404); + assertThatExceptionOfType(DockerException.class).isThrownBy(() -> this.http.get(this.uri)) + .satisfies((ex) -> assertThat(ex.getErrors()).hasSize(2)); + } + + @Test + void executeWhenResposeIsIn500RangeShouldThrowDockerException() throws ClientProtocolException, IOException { + given(this.statusLine.getStatusCode()).willReturn(500); + assertThatExceptionOfType(DockerException.class).isThrownBy(() -> this.http.get(this.uri)) + .satisfies((ex) -> assertThat(ex.getErrors()).isNull()); + } + + private String writeToString(HttpEntity entity) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + entity.writeTo(out); + return new String(out.toByteArray(), StandardCharsets.UTF_8); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/LoadImageUpdateEventTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/LoadImageUpdateEventTests.java new file mode 100644 index 0000000000..d0a9e093f4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/LoadImageUpdateEventTests.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.cloudnativebuildpack.docker.ProgressUpdateEvent.ProgressDetail; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link LoadImageUpdateEvent}. + * + * @author Phillip Webb + */ +class LoadImageUpdateEventTests extends ProgressUpdateEventTests { + + @Test + void getStreamReturnsStream() { + LoadImageUpdateEvent event = (LoadImageUpdateEvent) createEvent(); + assertThat(event.getStream()).isEqualTo("stream"); + } + + @Override + protected ProgressUpdateEvent createEvent(String status, ProgressDetail progressDetail, String progress) { + return new LoadImageUpdateEvent("stream", status, progressDetail, progress); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/LogUpdateEventTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/LogUpdateEventTests.java new file mode 100644 index 0000000000..64389d81cc --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/LogUpdateEventTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link LogUpdateEvent}. + * + * @author Phillip Webb + */ +class LogUpdateEventTests { + + @Test + void readAllWhenSimpleStreamReturnsEvents() throws Exception { + List events = readAll("log-update-event.stream"); + assertThat(events).hasSize(7); + assertThat(events.get(0).toString()) + .isEqualTo("Analyzing image '307c032c4ceaa6330b6c02af945a1fe56a8c3c27c28268574b217c1d38b093cf'"); + assertThat(events.get(1).toString()) + .isEqualTo("Writing metadata for uncached layer 'org.cloudfoundry.openjdk:openjdk-jre'"); + assertThat(events.get(2).toString()) + .isEqualTo("Using cached launch layer 'org.cloudfoundry.jvmapplication:executable-jar'"); + } + + @Test + void readAllWhenAnsiStreamReturnsEvents() throws Exception { + List events = readAll("log-update-event-ansi.stream"); + assertThat(events).hasSize(20); + assertThat(events.get(0).toString()).isEqualTo(""); + assertThat(events.get(1).toString()).isEqualTo("Cloud Foundry OpenJDK Buildpack v1.0.64"); + assertThat(events.get(2).toString()).isEqualTo(" OpenJDK JRE 11.0.5: Reusing cached layer"); + } + + private List readAll(String name) throws IOException { + List events = new ArrayList<>(); + try (InputStream inputStream = getClass().getResourceAsStream(name)) { + LogUpdateEvent.readAll(inputStream, events::add); + } + return events; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/ProgressUpdateEventTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/ProgressUpdateEventTests.java new file mode 100644 index 0000000000..badca775e3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/ProgressUpdateEventTests.java @@ -0,0 +1,75 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.cloudnativebuildpack.docker.ProgressUpdateEvent.ProgressDetail; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ProgressUpdateEvent}. + * + * @author Phillip Webb + */ +abstract class ProgressUpdateEventTests { + + @Test + void getStatusReturnsStatus() { + ProgressUpdateEvent event = createEvent(); + assertThat(event.getStatus()).isEqualTo("status"); + } + + @Test + void getProgressDetailsReturnsProgresssDetails() { + ProgressUpdateEvent event = createEvent(); + assertThat(event.getProgressDetail().getCurrent()).isEqualTo(1); + assertThat(event.getProgressDetail().getTotal()).isEqualTo(2); + } + + @Test + void getProgressReturnsProgress() { + ProgressUpdateEvent event = createEvent(); + assertThat(event.getProgress()).isEqualTo("progress"); + } + + @Test + void progressDetailIsEmptyWhenCurrentIsNullReturnsTrue() { + ProgressDetail detail = new ProgressDetail(null, 2); + assertThat(ProgressDetail.isEmpty(detail)).isTrue(); + } + + @Test + void progressDetailIsEmptyWhenTotalIsNullReturnsTrue() { + ProgressDetail detail = new ProgressDetail(1, null); + assertThat(ProgressDetail.isEmpty(detail)).isTrue(); + } + + @Test + void progressDetailIsEmptyWhenTotalAndCurrentAreNotNullReturnsFalse() { + ProgressDetail detail = new ProgressDetail(1, 2); + assertThat(ProgressDetail.isEmpty(detail)).isFalse(); + } + + protected ProgressUpdateEvent createEvent() { + return createEvent("status", new ProgressDetail(1, 2), "progress"); + } + + protected abstract ProgressUpdateEvent createEvent(String status, ProgressDetail progressDetail, String progress); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/PullImageUpdateEventTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/PullImageUpdateEventTests.java new file mode 100644 index 0000000000..cc373dd100 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/PullImageUpdateEventTests.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.cloudnativebuildpack.docker.ProgressUpdateEvent.ProgressDetail; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link PullImageUpdateEvent}. + * + * @author Phillip Webb + */ +class PullImageUpdateEventTests extends ProgressUpdateEventTests { + + @Test + void getIdReturnsId() { + PullImageUpdateEvent event = (PullImageUpdateEvent) createEvent(); + assertThat(event.getId()).isEqualTo("id"); + } + + @Override + protected ProgressUpdateEvent createEvent(String status, ProgressDetail progressDetail, String progress) { + return new PullImageUpdateEvent("id", status, progressDetail, progress); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/PullUpdateEventTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/PullUpdateEventTests.java new file mode 100644 index 0000000000..9b3e94c4ec --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/PullUpdateEventTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.cloudnativebuildpack.json.AbstractJsonTests; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link PullImageUpdateEvent}. + * + * @author Phillip Webb + */ +class PullUpdateEventTests extends AbstractJsonTests { + + @Test + void readValueWhenFullDeserializesJson() throws Exception { + PullImageUpdateEvent event = getObjectMapper().readValue(getContent("pull-update-full.json"), + PullImageUpdateEvent.class); + assertThat(event.getId()).isEqualTo("4f4fb700ef54"); + assertThat(event.getStatus()).isEqualTo("Extracting"); + assertThat(event.getProgressDetail().getCurrent()).isEqualTo(16); + assertThat(event.getProgressDetail().getTotal()).isEqualTo(32); + assertThat(event.getProgress()).isEqualTo("[==================================================>] 32B/32B"); + } + + @Test + void readValueWhenMinimalDeserializesJson() throws Exception { + PullImageUpdateEvent event = getObjectMapper().readValue(getContent("pull-update-minimal.json"), + PullImageUpdateEvent.class); + assertThat(event.getId()).isNull(); + assertThat(event.getStatus()).isEqualTo("Status: Downloaded newer image for cloudfoundry/cnb:bionic"); + assertThat(event.getProgressDetail()).isNull(); + assertThat(event.getProgress()).isNull(); + } + + @Test + void readValueWhenEmptyDetailsDeserializesJson() throws Exception { + PullImageUpdateEvent event = getObjectMapper().readValue(getContent("pull-with-empty-details.json"), + PullImageUpdateEvent.class); + assertThat(event.getId()).isEqualTo("d837a2a1365e"); + assertThat(event.getStatus()).isEqualTo("Pulling fs layer"); + assertThat(event.getProgressDetail()).isNull(); + assertThat(event.getProgress()).isNull(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/TotalProgressBarTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/TotalProgressBarTests.java new file mode 100644 index 0000000000..edcb731e55 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/TotalProgressBarTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link TotalProgressBar}. + * + * @author Phillip Webb + */ +class TotalProgressBarTests { + + @Test + void withPrefixAndBookends() { + TestPrintStream out = new TestPrintStream(); + TotalProgressBar bar = new TotalProgressBar("prefix:", '#', true, out); + assertThat(out).hasToString("prefix: [ "); + bar.accept(new TotalProgressEvent(10)); + assertThat(out.toString()).isEqualTo("prefix: [ #####"); + bar.accept(new TotalProgressEvent(50)); + assertThat(out.toString()).isEqualTo("prefix: [ #########################"); + bar.accept(new TotalProgressEvent(100)); + assertThat(out.toString()).isEqualTo("prefix: [ ################################################## ]\n"); + } + + @Test + void withoutPrefix() { + TestPrintStream out = new TestPrintStream(); + TotalProgressBar bar = new TotalProgressBar(null, '#', true, out); + assertThat(out).hasToString("[ "); + bar.accept(new TotalProgressEvent(10)); + assertThat(out.toString()).isEqualTo("[ #####"); + bar.accept(new TotalProgressEvent(50)); + assertThat(out.toString()).isEqualTo("[ #########################"); + bar.accept(new TotalProgressEvent(100)); + assertThat(out.toString()).isEqualTo("[ ################################################## ]\n"); + } + + @Test + void withoutBookends() { + TestPrintStream out = new TestPrintStream(); + TotalProgressBar bar = new TotalProgressBar("", '.', false, out); + assertThat(out).hasToString(""); + bar.accept(new TotalProgressEvent(10)); + assertThat(out.toString()).isEqualTo("....."); + bar.accept(new TotalProgressEvent(50)); + assertThat(out.toString()).isEqualTo("........................."); + bar.accept(new TotalProgressEvent(100)); + assertThat(out.toString()).isEqualTo("..................................................\n"); + } + + static class TestPrintStream extends PrintStream { + + TestPrintStream() { + super(new ByteArrayOutputStream()); + } + + @Override + public String toString() { + return this.out.toString(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/TotalProgressEventTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/TotalProgressEventTests.java new file mode 100644 index 0000000000..a33c701f0b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/TotalProgressEventTests.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link TotalProgressEvent}. + * + * @author Phillip Webb + */ +class TotalProgressEventTests { + + @Test + void create() { + assertThat(new TotalProgressEvent(0).getPercent()).isEqualTo(0); + assertThat(new TotalProgressEvent(10).getPercent()).isEqualTo(10); + assertThat(new TotalProgressEvent(100).getPercent()).isEqualTo(100); + } + + @Test + void createWhenPercentLessThanZeroThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new TotalProgressEvent(-1)) + .withMessage("Percent must be in the range 0 to 100"); + } + + @Test + void createWhenEventMoreThanOneHundredThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new TotalProgressEvent(101)) + .withMessage("Percent must be in the range 0 to 100"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/TotalProgressPullListenerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/TotalProgressPullListenerTests.java new file mode 100644 index 0000000000..d6b7fa5d63 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/TotalProgressPullListenerTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.cloudnativebuildpack.json.AbstractJsonTests; +import org.springframework.boot.cloudnativebuildpack.json.JsonStream; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link TotalProgressPullListener}. + * + * @author Phillip Webb + */ +class TotalProgressPullListenerTests extends AbstractJsonTests { + + @Test + void totalProgress() throws Exception { + List progress = new ArrayList<>(); + TotalProgressPullListener listener = new TotalProgressPullListener((event) -> progress.add(event.getPercent())); + run(listener); + int last = 0; + for (Integer update : progress) { + assertThat(update).isGreaterThanOrEqualTo(last); + last = update; + } + assertThat(last).isEqualTo(100); + } + + @Test + @Disabled("For visual inspection") + void totalProgressUpdatesSmoothly() throws Exception { + TestTotalProgressPullListener listener = new TestTotalProgressPullListener( + new TotalProgressBar("Pulling layers:")); + run(listener); + } + + private void run(TotalProgressPullListener listener) throws IOException { + JsonStream jsonStream = new JsonStream(getObjectMapper()); + listener.onStart(); + jsonStream.get(getContent("pull-stream.json"), PullImageUpdateEvent.class, listener::onUpdate); + listener.onFinish(); + } + + private static class TestTotalProgressPullListener extends TotalProgressPullListener { + + TestTotalProgressPullListener(Consumer consumer) { + super(consumer); + } + + @Override + public void onUpdate(PullImageUpdateEvent event) { + super.onUpdate(event); + try { + Thread.sleep(10); + } + catch (InterruptedException ex) { + } + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/ContainerConfigTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/ContainerConfigTests.java new file mode 100644 index 0000000000..b3c6ed5b5e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/ContainerConfigTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker.type; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import org.springframework.boot.cloudnativebuildpack.json.AbstractJsonTests; +import org.springframework.util.StreamUtils; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link ContainerConfig}. + * + * @author Phillip Webb + */ +class ContainerConfigTests extends AbstractJsonTests { + + @Test + void ofWhenImageReferenceIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> ContainerConfig.of(null, (update) -> { + })).withMessage("ImageReference must not be null"); + } + + @Test + void ofWhenUpdateIsNullThrowsException() { + ImageReference imageReference = ImageReference.of("ubuntu:bionic"); + assertThatIllegalArgumentException().isThrownBy(() -> ContainerConfig.of(imageReference, null)) + .withMessage("Update must not be null"); + } + + @Test + void writeToWritesJson() throws Exception { + ImageReference imageReference = ImageReference.of("ubuntu:bionic"); + ContainerConfig containerConfig = ContainerConfig.of(imageReference, (update) -> { + update.withUser("root"); + update.withCommand("ls", "-l"); + update.withArgs("-h"); + update.withLabel("spring", "boot"); + update.withBind("bind-source", "bind-dest"); + }); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + containerConfig.writeTo(outputStream); + String actualJson = new String(outputStream.toByteArray(), StandardCharsets.UTF_8); + String expectedJson = StreamUtils.copyToString(getContent("container-config.json"), StandardCharsets.UTF_8); + JSONAssert.assertEquals(expectedJson, actualJson, false); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/ContainerContentTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/ContainerContentTests.java new file mode 100644 index 0000000000..35d01ad17a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/ContainerContentTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker.type; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.cloudnativebuildpack.io.TarArchive; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link ContainerContent}. + * + * @author Phillip Webb + */ +class ContainerContentTests { + + @Test + void ofWhenArchiveIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> ContainerContent.of(null)) + .withMessage("Archive must not be null"); + } + + @Test + void ofWhenDestinationPathIsNullThrowsException() { + TarArchive archive = mock(TarArchive.class); + assertThatIllegalArgumentException().isThrownBy(() -> ContainerContent.of(archive, null)) + .withMessage("DestinationPath must not be empty"); + } + + @Test + void ofWhenDestinationPathIsEmptyThrowsException() { + TarArchive archive = mock(TarArchive.class); + assertThatIllegalArgumentException().isThrownBy(() -> ContainerContent.of(archive, "")) + .withMessage("DestinationPath must not be empty"); + } + + @Test + void ofCreatesContainerContent() { + TarArchive archive = mock(TarArchive.class); + ContainerContent content = ContainerContent.of(archive); + assertThat(content.getArchive()).isSameAs(archive); + assertThat(content.getDestinationPath()).isEqualTo("/"); + } + + @Test + void ofWithDestinationPathCreatesContainerContent() { + TarArchive archive = mock(TarArchive.class); + ContainerContent content = ContainerContent.of(archive, "/test"); + assertThat(content.getArchive()).isSameAs(archive); + assertThat(content.getDestinationPath()).isEqualTo("/test"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/ContainerReferenceTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/ContainerReferenceTests.java new file mode 100644 index 0000000000..0c04369db4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/ContainerReferenceTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker.type; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link ContainerReference}. + * + * @author Phillip Webb + */ +class ContainerReferenceTests { + + @Test + void ofCreatesInstance() { + ContainerReference reference = ContainerReference + .of("92691aec176333f7ae890de9aaeeafef11166efcaa3908edf83eb44a5c943781"); + assertThat(reference.toString()).isEqualTo("92691aec176333f7ae890de9aaeeafef11166efcaa3908edf83eb44a5c943781"); + } + + @Test + void ofWhenNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> ContainerReference.of(null)) + .withMessage("Value must not be empty"); + } + + @Test + void ofWhenEmptyThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> ContainerReference.of("")) + .withMessage("Value must not be empty"); + } + + @Test + void hashCodeAndEquals() { + ContainerReference r1 = ContainerReference + .of("92691aec176333f7ae890de9aaeeafef11166efcaa3908edf83eb44a5c943781"); + ContainerReference r2 = ContainerReference + .of("92691aec176333f7ae890de9aaeeafef11166efcaa3908edf83eb44a5c943781"); + ContainerReference r3 = ContainerReference + .of("02691aec176333f7ae890de9aaeeafef11166efcaa3908edf83eb44a5c943781"); + assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); + assertThat(r1).isEqualTo(r1).isEqualTo(r2).isNotEqualTo(r3); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/ImageArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/ImageArchiveTests.java new file mode 100644 index 0000000000..bc626ba169 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/ImageArchiveTests.java @@ -0,0 +1,101 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker.type; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import org.springframework.boot.cloudnativebuildpack.io.Owner; +import org.springframework.boot.cloudnativebuildpack.json.AbstractJsonTests; +import org.springframework.util.StreamUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ImageArchive}. + * + * @author Phillip Webb + */ +class ImageArchiveTests extends AbstractJsonTests { + + static final Instant CREATE_DATE = OffsetDateTime.of(1906, 12, 9, 11, 30, 0, 0, ZoneOffset.UTC).toInstant(); + + @Test + void fromImageWritesToValidArchiveTar() throws Exception { + Image image = Image.of(getContent("image.json")); + ImageArchive archive = ImageArchive.from(image, (update) -> { + update.withNewLayer(Layer.of((layout) -> layout.folder("/spring", Owner.ROOT))); + update.withCreateDate(CREATE_DATE); + update.withTag(ImageReference.of("pack.local/builder/6b7874626575656b6162")); + }); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + archive.writeTo(outputStream); + try (TarArchiveInputStream tar = new TarArchiveInputStream( + new ByteArrayInputStream(outputStream.toByteArray()))) { + TarArchiveEntry layerEntry = tar.getNextTarEntry(); + byte[] layerContent = read(tar, layerEntry.getSize()); + TarArchiveEntry configEntry = tar.getNextTarEntry(); + byte[] configContent = read(tar, configEntry.getSize()); + TarArchiveEntry manifestEntry = tar.getNextTarEntry(); + byte[] manifestContent = read(tar, manifestEntry.getSize()); + assertThat(tar.getNextTarEntry()).isNull(); + assertExpectedLayer(layerEntry, layerContent); + assertExpectedConfig(configEntry, configContent); + assertExpectedManifest(manifestEntry, manifestContent); + } + } + + private void assertExpectedLayer(TarArchiveEntry entry, byte[] content) throws Exception { + assertThat(entry.getName()).isEqualTo("/bb09e17fd1bd2ee47155f1349645fcd9fff31e1247c7ed99cad469f1c16a4216.tar"); + try (TarArchiveInputStream tar = new TarArchiveInputStream(new ByteArrayInputStream(content))) { + TarArchiveEntry contentEntry = tar.getNextTarEntry(); + assertThat(contentEntry.getName()).isEqualTo("/spring/"); + } + } + + private void assertExpectedConfig(TarArchiveEntry entry, byte[] content) throws Exception { + assertThat(entry.getName()).isEqualTo("/d1872169d781cff5e1aa22d111f636bef0c57e1c358ca3861e3d33a5bdb1b4a5.json"); + String actualJson = new String(content, StandardCharsets.UTF_8); + String expectedJson = StreamUtils.copyToString(getContent("image-archive-config.json"), StandardCharsets.UTF_8); + JSONAssert.assertEquals(expectedJson, actualJson, false); + } + + private void assertExpectedManifest(TarArchiveEntry entry, byte[] content) throws Exception { + assertThat(entry.getName()).isEqualTo("/manifest.json"); + String actualJson = new String(content, StandardCharsets.UTF_8); + String expectedJson = StreamUtils.copyToString(getContent("image-archive-manifest.json"), + StandardCharsets.UTF_8); + JSONAssert.assertEquals(expectedJson, actualJson, false); + } + + private byte[] read(TarArchiveInputStream tar, long size) throws IOException { + byte[] content = new byte[(int) size]; + tar.read(content); + return content; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/ImageConfigTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/ImageConfigTests.java new file mode 100644 index 0000000000..2a64f2ca11 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/ImageConfigTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker.type; + +import java.io.IOException; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.cloudnativebuildpack.json.AbstractJsonTests; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +/** + * Tests for {@link ImageConfig}. + * + * @author Phillip Webb + */ +class ImageConfigTests extends AbstractJsonTests { + + @Test + void getEnvContainsParsedValues() throws Exception { + ImageConfig imageConfig = getImageConfig(); + Map env = imageConfig.getEnv(); + assertThat(env).contains(entry("PATH", "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"), + entry("CNB_USER_ID", "2000"), entry("CNB_GROUP_ID", "2000"), + entry("CNB_STACK_ID", "org.cloudfoundry.stacks.cflinuxfs3")); + } + + @Test + void getLabelsReturnsLabels() throws Exception { + ImageConfig imageConfig = getImageConfig(); + Map lables = imageConfig.getLabels(); + assertThat(lables).hasSize(4).contains(entry("io.buildpacks.stack.id", "org.cloudfoundry.stacks.cflinuxfs3")); + } + + @Test + void updateWithLabelUpdatesLabels() throws Exception { + ImageConfig imageConfig = getImageConfig(); + ImageConfig updatedImageConfig = imageConfig + .copy((update) -> update.withLabel("io.buildpacks.stack.id", "test")); + assertThat(imageConfig.getLabels()).hasSize(4) + .contains(entry("io.buildpacks.stack.id", "org.cloudfoundry.stacks.cflinuxfs3")); + assertThat(updatedImageConfig.getLabels()).hasSize(4).contains(entry("io.buildpacks.stack.id", "test")); + } + + private ImageConfig getImageConfig() throws IOException { + return new ImageConfig(getObjectMapper().readTree(getContent("image-config.json"))); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/ImageNameTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/ImageNameTests.java new file mode 100644 index 0000000000..ba17f3ef99 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/ImageNameTests.java @@ -0,0 +1,109 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker.type; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link ImageName}. + * + * @author Phillip Webb + */ +class ImageNameTests { + + @Test + void ofWhenNameOnlyCreatesImageName() { + ImageName imageName = ImageName.of("ubuntu"); + assertThat(imageName.toString()).isEqualTo("docker.io/library/ubuntu"); + assertThat(imageName.getDomain()).isEqualTo("docker.io"); + assertThat(imageName.getName()).isEqualTo("library/ubuntu"); + } + + @Test + void ofWhenSlashedNameCreatesImageName() { + ImageName imageName = ImageName.of("canonical/ubuntu"); + assertThat(imageName.toString()).isEqualTo("docker.io/canonical/ubuntu"); + assertThat(imageName.getDomain()).isEqualTo("docker.io"); + assertThat(imageName.getName()).isEqualTo("canonical/ubuntu"); + } + + @Test + void ofWhenLocalhostNameCreatesImageName() { + ImageName imageName = ImageName.of("localhost/canonical/ubuntu"); + assertThat(imageName.toString()).isEqualTo("localhost/canonical/ubuntu"); + assertThat(imageName.getDomain()).isEqualTo("localhost"); + assertThat(imageName.getName()).isEqualTo("canonical/ubuntu"); + } + + @Test + void ofWhenDomainAndNameCreatesImageName() { + ImageName imageName = ImageName.of("repo.spring.io/canonical/ubuntu"); + assertThat(imageName.toString()).isEqualTo("repo.spring.io/canonical/ubuntu"); + assertThat(imageName.getDomain()).isEqualTo("repo.spring.io"); + assertThat(imageName.getName()).isEqualTo("canonical/ubuntu"); + } + + @Test + void ofWhenDomainNameAndPortCreatesImageName() { + ImageName imageName = ImageName.of("repo.spring.io:8080/canonical/ubuntu"); + assertThat(imageName.toString()).isEqualTo("repo.spring.io:8080/canonical/ubuntu"); + assertThat(imageName.getDomain()).isEqualTo("repo.spring.io:8080"); + assertThat(imageName.getName()).isEqualTo("canonical/ubuntu"); + } + + @Test + void ofWhenSimpleNameAndPortCreatesImageName() { + ImageName imageName = ImageName.of("repo:8080/canonical/ubuntu"); + assertThat(imageName.toString()).isEqualTo("repo:8080/canonical/ubuntu"); + assertThat(imageName.getDomain()).isEqualTo("repo:8080"); + assertThat(imageName.getName()).isEqualTo("canonical/ubuntu"); + } + + @Test + void ofWhenLegacyDomainUsesNewDomain() { + ImageName imageName = ImageName.of("index.docker.io/ubuntu"); + assertThat(imageName.toString()).isEqualTo("docker.io/library/ubuntu"); + assertThat(imageName.getDomain()).isEqualTo("docker.io"); + assertThat(imageName.getName()).isEqualTo("library/ubuntu"); + } + + @Test + void ofWhenNameIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> ImageName.of(null)) + .withMessage("Value must not be empty"); + } + + @Test + void ofWhenNameIsEmptyThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> ImageName.of("")).withMessage("Value must not be empty"); + } + + @Test + void hashCodeAndEquals() { + ImageName n1 = ImageName.of("ubuntu"); + ImageName n2 = ImageName.of("library/ubuntu"); + ImageName n3 = ImageName.of("docker.io/library/ubuntu"); + ImageName n4 = ImageName.of("index.docker.io/library/ubuntu"); + ImageName n5 = ImageName.of("alpine"); + assertThat(n1.hashCode()).isEqualTo(n2.hashCode()).isEqualTo(n3.hashCode()).isEqualTo(n4.hashCode()); + assertThat(n1).isEqualTo(n1).isEqualTo(n2).isEqualTo(n3).isEqualTo(n4).isNotEqualTo(n5); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/ImageReferenceTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/ImageReferenceTests.java new file mode 100644 index 0000000000..652f640904 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/ImageReferenceTests.java @@ -0,0 +1,235 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker.type; + +import java.io.File; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link ImageReference}. + * + * @author Phillip Webb + */ +class ImageReferenceTests { + + @Test + void ofSimpleName() { + ImageReference reference = ImageReference.of("ubuntu"); + assertThat(reference.getDomain()).isEqualTo("docker.io"); + assertThat(reference.getName()).isEqualTo("library/ubuntu"); + assertThat(reference.getTag()).isNull(); + assertThat(reference.getDigest()).isNull(); + assertThat(reference.toString()).isEqualTo("docker.io/library/ubuntu"); + } + + @Test + void ofLibrarySlashName() { + ImageReference reference = ImageReference.of("library/ubuntu"); + assertThat(reference.getDomain()).isEqualTo("docker.io"); + assertThat(reference.getName()).isEqualTo("library/ubuntu"); + assertThat(reference.getTag()).isNull(); + assertThat(reference.getDigest()).isNull(); + assertThat(reference.toString()).isEqualTo("docker.io/library/ubuntu"); + } + + @Test + void ofSlashName() { + ImageReference reference = ImageReference.of("adoptopenjdk/openjdk11"); + assertThat(reference.getDomain()).isEqualTo("docker.io"); + assertThat(reference.getName()).isEqualTo("adoptopenjdk/openjdk11"); + assertThat(reference.getTag()).isNull(); + assertThat(reference.getDigest()).isNull(); + assertThat(reference.toString()).isEqualTo("docker.io/adoptopenjdk/openjdk11"); + } + + @Test + void ofCustomDomain() { + ImageReference reference = ImageReference.of("repo.example.com/java/jdk"); + assertThat(reference.getDomain()).isEqualTo("repo.example.com"); + assertThat(reference.getName()).isEqualTo("java/jdk"); + assertThat(reference.getTag()).isNull(); + assertThat(reference.getDigest()).isNull(); + assertThat(reference.toString()).isEqualTo("repo.example.com/java/jdk"); + } + + @Test + void ofCustomDomainAndPort() { + ImageReference reference = ImageReference.of("repo.example.com:8080/java/jdk"); + assertThat(reference.getDomain()).isEqualTo("repo.example.com:8080"); + assertThat(reference.getName()).isEqualTo("java/jdk"); + assertThat(reference.getTag()).isNull(); + assertThat(reference.getDigest()).isNull(); + assertThat(reference.toString()).isEqualTo("repo.example.com:8080/java/jdk"); + } + + @Test + void ofLegacyDomain() { + ImageReference reference = ImageReference.of("index.docker.io/ubuntu"); + assertThat(reference.getDomain()).isEqualTo("docker.io"); + assertThat(reference.getName()).isEqualTo("library/ubuntu"); + assertThat(reference.getTag()).isNull(); + assertThat(reference.getDigest()).isNull(); + assertThat(reference.toString()).isEqualTo("docker.io/library/ubuntu"); + } + + @Test + void ofNameAndTag() { + ImageReference reference = ImageReference.of("ubuntu:bionic"); + assertThat(reference.getDomain()).isEqualTo("docker.io"); + assertThat(reference.getName()).isEqualTo("library/ubuntu"); + assertThat(reference.getTag()).isEqualTo("bionic"); + assertThat(reference.getDigest()).isNull(); + assertThat(reference.toString()).isEqualTo("docker.io/library/ubuntu:bionic"); + } + + @Test + void ofNameAndDigest() { + ImageReference reference = ImageReference + .of("ubuntu@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); + assertThat(reference.getDomain()).isEqualTo("docker.io"); + assertThat(reference.getName()).isEqualTo("library/ubuntu"); + assertThat(reference.getTag()).isNull(); + assertThat(reference.getDigest()) + .isEqualTo("sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); + assertThat(reference.toString()).isEqualTo( + "docker.io/library/ubuntu@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); + } + + @Test + void ofNameAndTagAndDigest() { + ImageReference reference = ImageReference + .of("ubuntu:bionic@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); + assertThat(reference.getDomain()).isEqualTo("docker.io"); + assertThat(reference.getName()).isEqualTo("library/ubuntu"); + assertThat(reference.getTag()).isEqualTo("bionic"); + assertThat(reference.getDigest()) + .isEqualTo("sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); + assertThat(reference.toString()).isEqualTo( + "docker.io/library/ubuntu:bionic@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); + } + + @Test + void ofCustomDomainAndPortWithTag() { + ImageReference reference = ImageReference.of( + "example.com:8080/canonical/ubuntu:bionic@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); + assertThat(reference.getDomain()).isEqualTo("example.com:8080"); + assertThat(reference.getName()).isEqualTo("canonical/ubuntu"); + assertThat(reference.getTag()).isEqualTo("bionic"); + assertThat(reference.getDigest()) + .isEqualTo("sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); + assertThat(reference.toString()).isEqualTo( + "example.com:8080/canonical/ubuntu:bionic@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); + } + + @Test + void ofImageName() { + ImageReference reference = ImageReference.of(ImageName.of("ubuntu")); + assertThat(reference.toString()).isEqualTo("docker.io/library/ubuntu"); + } + + @Test + void ofImageNameAndTag() { + ImageReference reference = ImageReference.of(ImageName.of("ubuntu"), "bionic"); + assertThat(reference.toString()).isEqualTo("docker.io/library/ubuntu:bionic"); + } + + @Test + void ofImageNameTagAndDigest() { + ImageReference reference = ImageReference.of(ImageName.of("ubuntu"), "bionic", + "sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); + assertThat(reference.toString()).isEqualTo( + "docker.io/library/ubuntu:bionic@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); + } + + @Test + void forJarFile() { + assertForJarFile("spring-boot.2.0.0.BUILD-SNAPSHOT.jar", "library/spring-boot", "2.0.0.BUILD-SNAPSHOT"); + assertForJarFile("spring-boot.2.0.0.M1.jar", "library/spring-boot", "2.0.0.M1"); + assertForJarFile("spring-boot.2.0.0.RC1.jar", "library/spring-boot", "2.0.0.RC1"); + assertForJarFile("spring-boot.2.0.0.RELEASE.jar", "library/spring-boot", "2.0.0.RELEASE"); + assertForJarFile("sample-0.0.1-SNAPSHOT.jar", "library/sample", "0.0.1-SNAPSHOT"); + assertForJarFile("sample-0.0.1.jar", "library/sample", "0.0.1"); + } + + private void assertForJarFile(String jarFile, String expectedName, String expectedTag) { + ImageReference reference = ImageReference.forJarFile(new File(jarFile)); + assertThat(reference.getName()).isEqualTo(expectedName); + assertThat(reference.getTag()).isEqualTo(expectedTag); + } + + @Test + void randomGeneratesRandomName() { + String prefix = "pack.local/builder/"; + ImageReference random = ImageReference.random(prefix); + assertThat(random.toString()).startsWith(prefix).hasSize(prefix.length() + 10); + ImageReference another = ImageReference.random(prefix); + int attempts = 0; + while (another.equals(random)) { + assertThat(attempts).as("Duplicate results").isLessThan(10); + another = ImageReference.random(prefix); + attempts++; + } + } + + @Test + void randomWithLengthGeneratesRandomName() { + String prefix = "pack.local/builder/"; + ImageReference random = ImageReference.random(prefix, 20); + assertThat(random.toString()).startsWith(prefix).hasSize(prefix.length() + 20); + } + + @Test + void randomWherePrefixIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> ImageReference.random(null)) + .withMessage("Prefix must not be null"); + } + + @Test + void inTaggedFormWhenHasDigestThrowsException() { + ImageReference reference = ImageReference + .of("ubuntu@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); + assertThatIllegalStateException().isThrownBy(() -> reference.inTaggedForm()).withMessage( + "Image reference 'docker.io/library/ubuntu@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d' cannot contain a digest"); + } + + @Test + void inTaggedFormWhenHasNoTagUsesLatest() { + ImageReference reference = ImageReference.of("ubuntu"); + assertThat(reference.inTaggedForm().toString()).isEqualTo("docker.io/library/ubuntu:latest"); + } + + @Test + void inTaggedFormWhenHasTagUsesTag() { + ImageReference reference = ImageReference.of("ubuntu:bionic"); + assertThat(reference.inTaggedForm().toString()).isEqualTo("docker.io/library/ubuntu:bionic"); + } + + @Test + void equalsAndHashCode() { + ImageReference r1 = ImageReference.of("ubuntu:bionic"); + ImageReference r2 = ImageReference.of("docker.io/library/ubuntu:bionic"); + ImageReference r3 = ImageReference.of("docker.io/library/ubuntu:latest"); + assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); + assertThat(r1).isEqualTo(r1).isEqualTo(r2).isNotEqualTo(r3); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/ImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/ImageTests.java new file mode 100644 index 0000000000..931d197d4b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/ImageTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker.type; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.cloudnativebuildpack.json.AbstractJsonTests; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +/** + * Tests for {@link Image}. + * + * @author Phillip Webb + */ +class ImageTests extends AbstractJsonTests { + + @Test + void getConfigEnvContainsParsedValues() throws Exception { + Image image = getImage(); + Map env = image.getConfig().getEnv(); + assertThat(env).contains(entry("PATH", "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"), + entry("CNB_USER_ID", "2000"), entry("CNB_GROUP_ID", "2000"), + entry("CNB_STACK_ID", "org.cloudfoundry.stacks.cflinuxfs3")); + } + + @Test + void getConfigLabelsReturnsLabels() throws Exception { + Image image = getImage(); + Map lables = image.getConfig().getLabels(); + assertThat(lables).contains(entry("io.buildpacks.stack.id", "org.cloudfoundry.stacks.cflinuxfs3")); + } + + @Test + void getLayersReturnsImageLayers() throws Exception { + Image image = getImage(); + List layers = image.getLayers(); + assertThat(layers).hasSize(46); + assertThat(layers.get(0).toString()) + .isEqualTo("sha256:733a8e5ce32984099ef675fce04730f6e2a6dcfdf5bd292fea01a8f936265342"); + assertThat(layers.get(45).toString()) + .isEqualTo("sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"); + } + + @Test + void getOsReturnsOs() throws Exception { + Image image = getImage(); + assertThat(image.getOs()).isEqualTo("linux"); + } + + private Image getImage() throws IOException { + return Image.of(getContent("image.json")); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/LayerIdTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/LayerIdTests.java new file mode 100644 index 0000000000..5d50a63152 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/LayerIdTests.java @@ -0,0 +1,82 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker.type; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Test for {@link LayerId}. + * + * @author Phillip Webb + */ +class LayerIdTests { + + @Test + void ofReturnsLayerId() { + LayerId id = LayerId.of("sha256:9a183e56c86d376b408bdf922746d0a657f62b0e18c7c8f82a496b87710c576f"); + assertThat(id.getAlgorithm()).isEqualTo("sha256"); + assertThat(id.getHash()).isEqualTo("9a183e56c86d376b408bdf922746d0a657f62b0e18c7c8f82a496b87710c576f"); + assertThat(id.toString()).isEqualTo("sha256:9a183e56c86d376b408bdf922746d0a657f62b0e18c7c8f82a496b87710c576f"); + } + + @Test + void hashCodeAndEquals() { + LayerId id1 = LayerId.of("sha256:9a183e56c86d376b408bdf922746d0a657f62b0e18c7c8f82a496b87710c576f"); + LayerId id2 = LayerId.of("sha256:9a183e56c86d376b408bdf922746d0a657f62b0e18c7c8f82a496b87710c576f"); + LayerId id3 = LayerId.of("sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + assertThat(id1.hashCode()).isEqualTo(id2.hashCode()); + assertThat(id1).isEqualTo(id1).isEqualTo(id2).isNotEqualTo(id3); + } + + @Test + void ofWhenValueIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> LayerId.of((String) null)) + .withMessage("Value must not be empty"); + } + + @Test + void ofWhenValueIsEmptyThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> LayerId.of(" ")).withMessage("Value must not be empty"); + } + + @Test + void ofSha256Digest() throws Exception { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + digest.update("test".getBytes(StandardCharsets.UTF_8)); + LayerId id = LayerId.ofSha256Digest(digest.digest()); + assertThat(id.toString()).isEqualTo("sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"); + } + + @Test + void ofSha256DigestWhenNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> LayerId.ofSha256Digest((byte[]) null)) + .withMessage("Digest must not be null"); + } + + @Test + void ofSha256DigestWhenWrongLengthThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> LayerId.ofSha256Digest(new byte[31])) + .withMessage("Digest must be exactly 32 bytes"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/LayerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/LayerTests.java new file mode 100644 index 0000000000..3bd3c4f774 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/LayerTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker.type; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; + +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.cloudnativebuildpack.io.Content; +import org.springframework.boot.cloudnativebuildpack.io.IOConsumer; +import org.springframework.boot.cloudnativebuildpack.io.Layout; +import org.springframework.boot.cloudnativebuildpack.io.Owner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link Layer}. + * + * @author Phillip Webb + */ +class LayerTests { + + @Test + void ofWhenLayoutIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> Layer.of((IOConsumer) null)) + .withMessage("Layout must not be null"); + } + + @Test + void fromTarArchiveWhenTarArchiveIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> Layer.fromTarArchive(null)) + .withMessage("TarArchive must not be null"); + } + + @Test + void ofCreatesLayer() throws Exception { + Layer layer = Layer.of((layout) -> { + layout.folder("/folder", Owner.ROOT); + layout.file("/folder/file", Owner.ROOT, Content.of("test")); + }); + assertThat(layer.getId().toString()) + .isEqualTo("sha256:8b8a3cea2ba716da6bbb0a3bf7472f235fa08c71a27cec5fbf2de1cf1baa513f"); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + layer.writeTo(outputStream); + try (TarArchiveInputStream tarStream = new TarArchiveInputStream( + new ByteArrayInputStream(outputStream.toByteArray()))) { + assertThat(tarStream.getNextTarEntry().getName()).isEqualTo("/folder/"); + assertThat(tarStream.getNextTarEntry().getName()).isEqualTo("/folder/file"); + assertThat(tarStream.getNextTarEntry()).isNull(); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/RandomStringTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/RandomStringTests.java new file mode 100644 index 0000000000..b1804f2f00 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/RandomStringTests.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker.type; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link RandomString}. + * + * @author Phillip Webb + */ +class RandomStringTests { + + @Test + void generateWhenPrefixIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> RandomString.generate(null, 10)) + .withMessage("Prefix must not be null"); + } + + @Test + void generateGeneratesRandomString() { + String s1 = RandomString.generate("abc-", 10); + String s2 = RandomString.generate("abc-", 10); + String s3 = RandomString.generate("abc-", 20); + assertThat(s1).hasSize(14).startsWith("abc-").isNotEqualTo(s2); + assertThat(s3).hasSize(24).startsWith("abc-"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/VolumeNameTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/VolumeNameTests.java new file mode 100644 index 0000000000..e108800900 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/docker/type/VolumeNameTests.java @@ -0,0 +1,114 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.docker.type; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link VolumeName}. + * + * @author Phillip Webb + */ +class VolumeNameTests { + + @Test + void randomWhenPrefixIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> VolumeName.random(null)) + .withMessage("Prefix must not be null"); + } + + @Test + void randomGeneratesRandomString() { + VolumeName v1 = VolumeName.random("abc-"); + VolumeName v2 = VolumeName.random("abc-"); + assertThat(v1.toString()).startsWith("abc-").hasSize(14); + assertThat(v2.toString()).startsWith("abc-").hasSize(14); + assertThat(v1).isNotEqualTo(v2); + assertThat(v1.toString()).isNotEqualTo(v2.toString()); + } + + @Test + void randomStringWithLengthGeneratesRandomString() { + VolumeName v1 = VolumeName.random("abc-", 20); + VolumeName v2 = VolumeName.random("abc-", 20); + assertThat(v1.toString()).startsWith("abc-").hasSize(24); + assertThat(v2.toString()).startsWith("abc-").hasSize(24); + assertThat(v1).isNotEqualTo(v2); + assertThat(v1.toString()).isNotEqualTo(v2.toString()); + } + + @Test + void basedOnWhenSourceIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> VolumeName.basedOn(null, "prefix", "suffix", 6)) + .withMessage("Source must not be null"); + } + + @Test + void basedOnWhenNameExtractorIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> VolumeName.basedOn("test", null, "prefix", "suffix", 6)) + .withMessage("NameExtractor must not be null"); + } + + @Test + void basedOnWhenPrefixIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> VolumeName.basedOn("test", null, "suffix", 6)) + .withMessage("Prefix must not be null"); + } + + @Test + void basedOnWhenSuffixIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> VolumeName.basedOn("test", "prefix", null, 6)) + .withMessage("Suffix must not be null"); + } + + @Test + void basedOnGeneratesHashBasedName() { + VolumeName name = VolumeName.basedOn("index.docker.io/library/myapp:latest", "pack-cache-", ".build", 6); + assertThat(name.toString()).isEqualTo("pack-cache-40a311b545d7.build"); + } + + @Test + void basedOnWhenSizeIsTooBigThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> VolumeName.basedOn("name", "prefix", "suffix", 33)) + .withMessage("DigestLength must be less than or equal to 32"); + } + + @Test + void ofWhenValueIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> VolumeName.of(null)) + .withMessage("Value must not be null"); + } + + @Test + void ofGeneratesValue() { + VolumeName name = VolumeName.of("test"); + assertThat(name.toString()).isEqualTo("test"); + } + + @Test + void equalsAndHashCode() { + VolumeName n1 = VolumeName.of("test1"); + VolumeName n2 = VolumeName.of("test1"); + VolumeName n3 = VolumeName.of("test2"); + assertThat(n1.hashCode()).isEqualTo(n2.hashCode()); + assertThat(n1).isEqualTo(n1).isEqualTo(n2).isNotEqualTo(n3); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/io/ContentTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/io/ContentTests.java new file mode 100644 index 0000000000..ca615d093e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/io/ContentTests.java @@ -0,0 +1,82 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.io; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link Content}. + * + * @author Phillip Webb + */ +class ContentTests { + + @Test + void ofWhenStreamIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> Content.of(1, (IOSupplier) null)) + .withMessage("Supplier must not be null"); + } + + @Test + void ofWhenStreamReturnsWritable() throws Exception { + byte[] bytes = { 1, 2, 3, 4 }; + ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes); + Content writable = Content.of(4, () -> inputStream); + assertThat(writeToAndGetBytes(writable)).isEqualTo(bytes); + } + + @Test + void ofWhenStringIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> Content.of((String) null)) + .withMessage("String must not be null"); + } + + @Test + void ofWhenStringReturnsWritable() throws Exception { + Content writable = Content.of("spring"); + assertThat(writeToAndGetBytes(writable)).isEqualTo("spring".getBytes(StandardCharsets.UTF_8)); + } + + @Test + void ofWhenBytesIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> Content.of((byte[]) null)) + .withMessage("Bytes must not be null"); + } + + @Test + void ofWhenBytesReturnsWritable() throws Exception { + byte[] bytes = { 1, 2, 3, 4 }; + Content writable = Content.of(bytes); + assertThat(writeToAndGetBytes(writable)).isEqualTo(bytes); + } + + private byte[] writeToAndGetBytes(Content writable) throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + writable.writeTo(outputStream); + return outputStream.toByteArray(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/io/DefaultOwnerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/io/DefaultOwnerTests.java new file mode 100644 index 0000000000..61df85eff0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/io/DefaultOwnerTests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.io; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DefaultOwner}. + * + * @author Phillip Webb + */ +class DefaultOwnerTests { + + @Test + void getUidReturnsUid() { + DefaultOwner owner = new DefaultOwner(123, 456); + assertThat(owner.getUid()).isEqualTo(123); + } + + @Test + void getGidReturnsGid() { + DefaultOwner owner = new DefaultOwner(123, 456); + assertThat(owner.getGid()).isEqualTo(456); + } + + @Test + void toStringReturnsString() { + DefaultOwner owner = new DefaultOwner(123, 456); + assertThat(owner).hasToString("123/456"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/io/InspectedContentTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/io/InspectedContentTests.java new file mode 100644 index 0000000000..489770691b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/io/InspectedContentTests.java @@ -0,0 +1,99 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.io; + +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.MessageDigest; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link InspectedContent}. + * + * @author Phillip Webb + */ +class InspectedContentTests { + + @Test + void ofWhenInputStreamThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> InspectedContent.of((InputStream) null)) + .withMessage("InputStream must not be null"); + } + + @Test + void ofWhenContentIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> InspectedContent.of((Content) null)) + .withMessage("Content must not be null"); + } + + @Test + void ofWhenConsumerIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> InspectedContent.of((IOConsumer) null)) + .withMessage("Writer must not be null"); + } + + @Test + void ofFromContent() throws Exception { + InspectedContent content = InspectedContent.of(Content.of("test")); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + content.writeTo(outputStream); + assertThat(outputStream.toByteArray()).containsExactly("test".getBytes(StandardCharsets.UTF_8)); + } + + @Test + void ofSmallContent() throws Exception { + InputStream inputStream = new ByteArrayInputStream(new byte[] { 0, 1, 2 }); + InspectedContent content = InspectedContent.of(inputStream); + assertThat(content.size()).isEqualTo(3); + assertThat(readBytes(content)).containsExactly(0, 1, 2); + } + + @Test + void ofLargeContent() throws Exception { + byte[] bytes = new byte[InspectedContent.MEMORY_LIMIT + 3]; + System.arraycopy(new byte[] { 0, 1, 2 }, 0, bytes, 0, 3); + InputStream inputStream = new ByteArrayInputStream(bytes); + InspectedContent content = InspectedContent.of(inputStream); + assertThat(content.size()).isEqualTo(bytes.length); + assertThat(readBytes(content)).isEqualTo(bytes); + } + + @Test + void ofWithInspector() throws Exception { + InputStream inputStream = new ByteArrayInputStream("test".getBytes(StandardCharsets.UTF_8)); + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + InspectedContent.of(inputStream, digest::update); + assertThat(digest.digest()).inHexadecimal().contains(0x9f, 0x86, 0xd0, 0x81, 0x88, 0x4c, 0x7d, 0x65, 0x9a, 0x2f, + 0xea, 0xa0, 0xc5, 0x5a, 0xd0, 0x15, 0xa3, 0xbf, 0x4f, 0x1b, 0x2b, 0x0b, 0x82, 0x2c, 0xd1, 0x5d, 0x6c, + 0x15, 0xb0, 0xf0, 0x0a, 0x08); + } + + private byte[] readBytes(InspectedContent content) throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + content.writeTo(outputStream); + return outputStream.toByteArray(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/io/OwnerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/io/OwnerTests.java new file mode 100644 index 0000000000..ddccba4a4c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/io/OwnerTests.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.io; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link Owner}. + * + * @author Phillip Webb + */ +class OwnerTests { + + @Test + void ofReturnsNewOwner() { + Owner owner = Owner.of(123, 456); + assertThat(owner.getUid()).isEqualTo(123); + assertThat(owner.getGid()).isEqualTo(456); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/io/TarArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/io/TarArchiveTests.java new file mode 100644 index 0000000000..e27ce68389 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/io/TarArchiveTests.java @@ -0,0 +1,93 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.io; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link TarArchive}. + * + * @author Phillip Webb + */ +class TarArchiveTests { + + @TempDir + File tempDir; + + @Test + void ofWritesTarContent() throws Exception { + Owner owner = Owner.of(123, 456); + TarArchive tarArchive = TarArchive.of((content) -> { + content.folder("/workspace", owner); + content.folder("/layers", owner); + content.folder("/cnb", Owner.ROOT); + content.folder("/cnb/buildpacks", Owner.ROOT); + content.folder("/platform", Owner.ROOT); + content.folder("/platform/env", Owner.ROOT); + }); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + tarArchive.writeTo(outputStream); + try (TarArchiveInputStream tarStream = new TarArchiveInputStream( + new ByteArrayInputStream(outputStream.toByteArray()))) { + List entries = new ArrayList<>(); + TarArchiveEntry entry = tarStream.getNextTarEntry(); + while (entry != null) { + entries.add(entry); + entry = tarStream.getNextTarEntry(); + } + assertThat(entries).hasSize(6); + assertThat(entries.get(0).getName()).isEqualTo("/workspace/"); + assertThat(entries.get(0).getLongUserId()).isEqualTo(123); + assertThat(entries.get(0).getLongGroupId()).isEqualTo(456); + assertThat(entries.get(2).getName()).isEqualTo("/cnb/"); + assertThat(entries.get(2).getLongUserId()).isEqualTo(0); + assertThat(entries.get(2).getLongGroupId()).isEqualTo(0); + } + } + + @Test + void fromZipFileReturnsZipFileAdapter() throws Exception { + Owner owner = Owner.of(123, 456); + File file = new File(this.tempDir, "test.zip"); + writeTestZip(file); + TarArchive tarArchive = TarArchive.fromZip(file, owner); + assertThat(tarArchive).isInstanceOf(ZipFileTarArchive.class); + } + + private void writeTestZip(File file) throws IOException { + try (ZipArchiveOutputStream zip = new ZipArchiveOutputStream(file)) { + ZipArchiveEntry dirEntry = new ZipArchiveEntry("spring/"); + zip.putArchiveEntry(dirEntry); + zip.closeArchiveEntry(); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/io/TarLayoutWriterTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/io/TarLayoutWriterTests.java new file mode 100644 index 0000000000..ed3f5adadf --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/io/TarLayoutWriterTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.io; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Date; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link TarLayoutWriter}. + * + * @author Phillip Webb + */ +class TarLayoutWriterTests { + + @Test + void writesTarArchive() throws Exception { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (TarLayoutWriter writer = new TarLayoutWriter(outputStream)) { + writer.folder("/foo", Owner.ROOT); + writer.file("/foo/bar.txt", Owner.of(1, 1), Content.of("test")); + } + try (TarArchiveInputStream tarInputStream = new TarArchiveInputStream( + new ByteArrayInputStream(outputStream.toByteArray()))) { + TarArchiveEntry folderEntry = tarInputStream.getNextTarEntry(); + TarArchiveEntry fileEntry = tarInputStream.getNextTarEntry(); + byte[] fileContent = new byte[(int) fileEntry.getSize()]; + tarInputStream.read(fileContent); + assertThat(tarInputStream.getNextEntry()).isNull(); + assertThat(folderEntry.getName()).isEqualTo("/foo/"); + assertThat(folderEntry.getMode()).isEqualTo(0755); + assertThat(folderEntry.getLongUserId()).isEqualTo(0); + assertThat(folderEntry.getLongGroupId()).isEqualTo(0); + assertThat(folderEntry.getModTime()).isEqualTo(new Date(TarLayoutWriter.NORMALIZED_MOD_TIME)); + assertThat(fileEntry.getName()).isEqualTo("/foo/bar.txt"); + assertThat(fileEntry.getMode()).isEqualTo(0644); + assertThat(fileEntry.getLongUserId()).isEqualTo(1); + assertThat(fileEntry.getLongGroupId()).isEqualTo(1); + assertThat(fileEntry.getModTime()).isEqualTo(new Date(TarLayoutWriter.NORMALIZED_MOD_TIME)); + assertThat(fileContent).isEqualTo("test".getBytes(StandardCharsets.UTF_8)); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/io/ZipFileTarArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/io/ZipFileTarArchiveTests.java new file mode 100644 index 0000000000..6b83e5378d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/io/ZipFileTarArchiveTests.java @@ -0,0 +1,97 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.io; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.util.StreamUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link ZipFileTarArchive}. + * + * @author Phillip Webb + */ +class ZipFileTarArchiveTests { + + @TempDir + File tempDir; + + @Test + void createWhenZipIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new ZipFileTarArchive(null, Owner.ROOT)) + .withMessage("Zip must not be null"); + } + + @Test + void createWhenOwnerIsNullThrowsException() throws Exception { + File file = new File(this.tempDir, "test.zip"); + writeTestZip(file); + assertThatIllegalArgumentException().isThrownBy(() -> new ZipFileTarArchive(file, null)) + .withMessage("Owner must not be null"); + } + + @Test + void writeToAdaptsContent() throws Exception { + Owner owner = Owner.of(123, 456); + File file = new File(this.tempDir, "test.zip"); + writeTestZip(file); + TarArchive tarArchive = TarArchive.fromZip(file, owner); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + tarArchive.writeTo(outputStream); + try (TarArchiveInputStream tarStream = new TarArchiveInputStream( + new ByteArrayInputStream(outputStream.toByteArray()))) { + TarArchiveEntry dirEntry = tarStream.getNextTarEntry(); + assertThat(dirEntry.getName()).isEqualTo("spring/"); + assertThat(dirEntry.getLongUserId()).isEqualTo(123); + assertThat(dirEntry.getLongGroupId()).isEqualTo(456); + TarArchiveEntry fileEntry = tarStream.getNextTarEntry(); + assertThat(fileEntry.getName()).isEqualTo("spring/boot"); + assertThat(fileEntry.getLongUserId()).isEqualTo(123); + assertThat(fileEntry.getLongGroupId()).isEqualTo(456); + assertThat(fileEntry.getSize()).isEqualTo(4); + String fileContent = StreamUtils.copyToString(tarStream, StandardCharsets.UTF_8); + assertThat(fileContent).isEqualTo("test"); + } + } + + private void writeTestZip(File file) throws IOException { + try (ZipArchiveOutputStream zip = new ZipArchiveOutputStream(file)) { + ZipArchiveEntry dirEntry = new ZipArchiveEntry("spring/"); + zip.putArchiveEntry(dirEntry); + zip.closeArchiveEntry(); + ZipArchiveEntry fileEntry = new ZipArchiveEntry("spring/boot"); + zip.putArchiveEntry(fileEntry); + zip.write("test".getBytes(StandardCharsets.UTF_8)); + zip.closeArchiveEntry(); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/json/AbstractJsonTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/json/AbstractJsonTests.java new file mode 100644 index 0000000000..82258dde8f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/json/AbstractJsonTests.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.json; + +import java.io.InputStream; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Abstract base class for JSON based tests. + * + * @author Phillip Webb + */ +public abstract class AbstractJsonTests { + + protected final ObjectMapper getObjectMapper() { + return SharedObjectMapper.get(); + } + + protected final InputStream getContent(String name) { + InputStream result = getClass().getResourceAsStream(name); + assertThat(result).as("JSON source " + name).isNotNull(); + return result; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/json/JsonStreamTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/json/JsonStreamTests.java new file mode 100644 index 0000000000..3c7bea820b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/json/JsonStreamTests.java @@ -0,0 +1,83 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.json; + +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JsonStream}. + * + * @author Phillip Webb + */ +class JsonStreamTests extends AbstractJsonTests { + + private JsonStream jsonStream; + + JsonStreamTests() { + this.jsonStream = new JsonStream(getObjectMapper()); + } + + @Test + void getWhenReadingObjectNodeReturnsNodes() throws Exception { + List result = new ArrayList<>(); + this.jsonStream.get(getContent("stream.json"), result::add); + assertThat(result).hasSize(595); + assertThat(result.get(594).toString()).contains("Status: Downloaded newer image for cloudfoundry/cnb:bionic"); + } + + @Test + void getWhenReadTypesReturnsTypes() throws Exception { + List result = new ArrayList<>(); + this.jsonStream.get(getContent("stream.json"), TestEvent.class, result::add); + assertThat(result).hasSize(595); + assertThat(result.get(1).getId()).isEqualTo("5667fdb72017"); + assertThat(result.get(594).getStatus()).isEqualTo("Status: Downloaded newer image for cloudfoundry/cnb:bionic"); + } + + /** + * Event for type deserialization tests. + */ + static class TestEvent { + + private final String id; + + private final String status; + + @JsonCreator + TestEvent(String id, String status) { + this.id = id; + this.status = status; + } + + String getId() { + return this.id; + } + + String getStatus() { + return this.status; + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/json/MappedObjectTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/json/MappedObjectTests.java new file mode 100644 index 0000000000..f59fc33e13 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/json/MappedObjectTests.java @@ -0,0 +1,122 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.json; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.invoke.MethodHandles; + +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.cloudnativebuildpack.json.MappedObjectTests.TestMappedObject.Person; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MappedObject}. + * + * @author Phillip Webb + */ +class MappedObjectTests extends AbstractJsonTests { + + private final TestMappedObject mapped; + + MappedObjectTests() throws IOException { + this.mapped = TestMappedObject.of(getContent("test-mapped-object.json")); + } + + @Test + void ofReadsJson() throws Exception { + assertThat(this.mapped.getNode()).isNotNull(); + } + + @Test + void valueAtWhenStringReturnsValue() { + assertThat(this.mapped.valueAt("/string", String.class)).isEqualTo("stringvalue"); + } + + @Test + void valueAtWhenStringArrayReturnsValue() { + assertThat(this.mapped.valueAt("/stringarray", String[].class)).containsExactly("a", "b"); + } + + @Test + void valueAtWhenMissingReturnsNull() { + assertThat(this.mapped.valueAt("/missing", String.class)).isNull(); + } + + @Test + void valueAtWhenInterfaceReturnsProxy() { + Person person = this.mapped.valueAt("/person", Person.class); + assertThat(person.getName().getFirst()).isEqualTo("spring"); + assertThat(person.getName().getLast()).isEqualTo("boot"); + } + + @Test + void valueAtWhenInterfaceAndMissingReturnsProxy() { + Person person = this.mapped.valueAt("/missing", Person.class); + assertThat(person.getName().getFirst()).isNull(); + assertThat(person.getName().getLast()).isNull(); + } + + @Test + void valueAtWhenActualPropertyStartsWithUppercaseReturnsValue() { + assertThat(this.mapped.valueAt("/startsWithUppercase", String.class)).isEqualTo("value"); + } + + @Test + void valueAtWhenDefaultMethodReturnsValue() { + Person person = this.mapped.valueAt("/person", Person.class); + assertThat(person.getName().getFullName()).isEqualTo("dr spring boot"); + } + + /** + * {@link MappedObject} for testing. + */ + static class TestMappedObject extends MappedObject { + + TestMappedObject(JsonNode node) { + super(node, MethodHandles.lookup()); + } + + static TestMappedObject of(InputStream content) throws IOException { + return of(content, TestMappedObject::new); + } + + interface Person { + + Name getName(); + + interface Name { + + String getFirst(); + + String getLast(); + + default String getFullName() { + String title = valueAt(this, "/title", String.class); + return title + " " + getFirst() + " " + getLast(); + } + + } + + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/json/SharedObjectMapperTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/json/SharedObjectMapperTests.java new file mode 100644 index 0000000000..f04d8e32f9 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/json/SharedObjectMapperTests.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.json; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SharedObjectMapper}. + * + * @author Phillip Webb + */ +class SharedObjectMapperTests { + + @Test + void getReturnsConfiguredObjectMapper() { + ObjectMapper mapper = SharedObjectMapper.get(); + assertThat(mapper).isNotNull(); + assertThat(mapper.getRegisteredModuleIds()).contains(new ParameterNamesModule().getTypeId()); + assertThat(SerializationFeature.INDENT_OUTPUT + .enabledIn(mapper.getSerializationConfig().getSerializationFeatures())).isTrue(); + assertThat(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES + .enabledIn(mapper.getDeserializationConfig().getDeserializationFeatures())).isFalse(); + assertThat(mapper.getSerializationConfig().getPropertyNamingStrategy()) + .isEqualTo(PropertyNamingStrategy.LOWER_CAMEL_CASE); + assertThat(mapper.getDeserializationConfig().getPropertyNamingStrategy()) + .isEqualTo(PropertyNamingStrategy.LOWER_CAMEL_CASE); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/socket/FileDescriptorTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/socket/FileDescriptorTests.java new file mode 100644 index 0000000000..565bb7d893 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/socket/FileDescriptorTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.socket; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.cloudnativebuildpack.socket.FileDescriptor.Handle; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for {@link FileDescriptor}. + * + * @author Phillip Webb + */ +class FileDescriptorTests { + + private int sourceHandle = 123; + + private int closedHandle = 0; + + @Test + void acquireReturnsHandle() throws Exception { + FileDescriptor descriptor = new FileDescriptor(this.sourceHandle, this::close); + try (Handle handle = descriptor.acquire()) { + assertThat(handle.intValue()).isEqualTo(this.sourceHandle); + assertThat(handle.isClosed()).isFalse(); + } + } + + @Test + void acquireWhenClosedReturnsClosedHandle() throws Exception { + FileDescriptor descriptor = new FileDescriptor(this.sourceHandle, this::close); + descriptor.close(); + try (Handle handle = descriptor.acquire()) { + assertThat(handle.intValue()).isEqualTo(-1); + assertThat(handle.isClosed()).isTrue(); + } + } + + @Test + void acquireWhenPendingCloseReturnsClosedHandle() throws Exception { + FileDescriptor descriptor = new FileDescriptor(this.sourceHandle, this::close); + try (Handle handle1 = descriptor.acquire()) { + descriptor.close(); + try (Handle handle2 = descriptor.acquire()) { + assertThat(handle2.intValue()).isEqualTo(-1); + assertThat(handle2.isClosed()).isTrue(); + } + } + } + + @Test + void finalizeTriggersClose() { + FileDescriptor descriptor = new FileDescriptor(this.sourceHandle, this::close); + descriptor.close(); + assertThat(this.closedHandle).isEqualTo(this.sourceHandle); + } + + @Test + void closeWhenHandleAcquiredClosesOnRelease() throws Exception { + FileDescriptor descriptor = new FileDescriptor(this.sourceHandle, this::close); + try (Handle handle = descriptor.acquire()) { + descriptor.close(); + assertThat(this.closedHandle).isEqualTo(0); + } + assertThat(this.closedHandle).isEqualTo(this.sourceHandle); + } + + @Test + void closeWhenHandleNotAcquiredClosesImmediately() { + FileDescriptor descriptor = new FileDescriptor(this.sourceHandle, this::close); + descriptor.close(); + assertThat(this.closedHandle).isEqualTo(this.sourceHandle); + } + + private void close(int handle) { + this.closedHandle = handle; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/toml/TomlTests.java b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/toml/TomlTests.java new file mode 100644 index 0000000000..9ebdede108 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/java/org/springframework/boot/cloudnativebuildpack/toml/TomlTests.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2020 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. + */ + +package org.springframework.boot.cloudnativebuildpack.toml; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link Toml}. + * + * @author Phillip Webb + */ +class TomlTests { + + @Test + void createsTomlMarkup() { + Toml toml = new Toml(); + toml.table("run-image"); + toml.string("image", "cnb/test"); + toml.array("mirrors", "a", "b", "c"); + String expected = ""; + expected += "[run-image]\n"; + expected += "image = \"cnb/test\"\n"; + expected += "mirrors = [\"a\", \"b\", \"c\"]\n"; + assertThat(toml.toString()).isEqualTo(expected); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/builder-metadata.json b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/builder-metadata.json new file mode 100644 index 0000000000..5a236a2357 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/builder-metadata.json @@ -0,0 +1,222 @@ +{ + "description": "Ubuntu bionic base image with buildpacks for Java, NodeJS and Golang", + "buildpacks": [ + { + "id": "org.cloudfoundry.debug", + "version": "v1.0.106", + "latest": true + }, + { + "id": "org.cloudfoundry.go", + "version": "v0.0.2", + "latest": true + }, + { + "id": "org.cloudfoundry.springautoreconfiguration", + "version": "v1.0.113", + "latest": true + }, + { + "id": "org.cloudfoundry.buildsystem", + "version": "v1.0.133", + "latest": true + }, + { + "id": "org.cloudfoundry.procfile", + "version": "v1.0.41", + "latest": true + }, + { + "id": "org.cloudfoundry.nodejs", + "version": "v0.0.5", + "latest": true + }, + { + "id": "org.cloudfoundry.distzip", + "version": "v1.0.101", + "latest": true + }, + { + "id": "org.cloudfoundry.jdbc", + "version": "v1.0.107", + "latest": true + }, + { + "id": "org.cloudfoundry.azureapplicationinsights", + "version": "v1.0.105", + "latest": true + }, + { + "id": "org.cloudfoundry.springboot", + "version": "v1.0.112", + "latest": true + }, + { + "id": "org.cloudfoundry.openjdk", + "version": "v1.0.58", + "latest": true + }, + { + "id": "org.cloudfoundry.tomcat", + "version": "v1.1.24", + "latest": true + }, + { + "id": "org.cloudfoundry.googlestackdriver", + "version": "v1.0.54", + "latest": true + }, + { + "id": "org.cloudfoundry.jmx", + "version": "v1.0.107", + "latest": true + }, + { + "id": "org.cloudfoundry.archiveexpanding", + "version": "v1.0.99", + "latest": true + }, + { + "id": "org.cloudfoundry.jvmapplication", + "version": "v1.0.82", + "latest": true + }, + { + "id": "org.cloudfoundry.dep", + "version": "0.0.64", + "latest": true + }, + { + "id": "org.cloudfoundry.go-compiler", + "version": "0.0.55", + "latest": true + }, + { + "id": "org.cloudfoundry.go-mod", + "version": "0.0.58", + "latest": true + }, + { + "id": "org.cloudfoundry.node-engine", + "version": "0.0.102", + "latest": true + }, + { + "id": "org.cloudfoundry.npm", + "version": "0.0.63", + "latest": true + }, + { + "id": "org.cloudfoundry.yarn", + "version": "0.0.69", + "latest": true + } + ], + "groups": [ + { + "buildpacks": [ + { + "id": "org.cloudfoundry.archiveexpanding", + "version": "v1.0.99", + "optional": true + }, + { + "id": "org.cloudfoundry.openjdk", + "version": "v1.0.58" + }, + { + "id": "org.cloudfoundry.buildsystem", + "version": "v1.0.133", + "optional": true + }, + { + "id": "org.cloudfoundry.jvmapplication", + "version": "v1.0.82" + }, + { + "id": "org.cloudfoundry.tomcat", + "version": "v1.1.24", + "optional": true + }, + { + "id": "org.cloudfoundry.springboot", + "version": "v1.0.112", + "optional": true + }, + { + "id": "org.cloudfoundry.distzip", + "version": "v1.0.101", + "optional": true + }, + { + "id": "org.cloudfoundry.procfile", + "version": "v1.0.41", + "optional": true + }, + { + "id": "org.cloudfoundry.azureapplicationinsights", + "version": "v1.0.105", + "optional": true + }, + { + "id": "org.cloudfoundry.debug", + "version": "v1.0.106", + "optional": true + }, + { + "id": "org.cloudfoundry.googlestackdriver", + "version": "v1.0.54", + "optional": true + }, + { + "id": "org.cloudfoundry.jdbc", + "version": "v1.0.107", + "optional": true + }, + { + "id": "org.cloudfoundry.jmx", + "version": "v1.0.107", + "optional": true + }, + { + "id": "org.cloudfoundry.springautoreconfiguration", + "version": "v1.0.113", + "optional": true + } + ] + }, + { + "buildpacks": [ + { + "id": "org.cloudfoundry.nodejs", + "version": "v0.0.5" + } + ] + }, + { + "buildpacks": [ + { + "id": "org.cloudfoundry.go", + "version": "v0.0.2" + } + ] + } + ], + "stack": { + "runImage": { + "image": "cloudfoundry/run:base-cnb", + "mirrors": null + } + }, + "lifecycle": { + "version": "0.5.0", + "api": { + "buildpack": "0.2", + "platform": "0.1" + } + }, + "createdBy": { + "name": "Pack CLI", + "version": "dev-2019-11-19-22:34:59" + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/image.json b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/image.json new file mode 100644 index 0000000000..4d9667f335 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/image.json @@ -0,0 +1,142 @@ +{ + "Id": "sha256:9b450bffdb05bcf660d464d0bfdf344ee6ca38e9b8de4f408c8080b0c9319349", + "RepoTags": [ + "cloudfoundry/cnb:latest" + ], + "RepoDigests": [ + "cloudfoundry/cnb@sha256:915802bb193b66e3fc1a5a8f5584c6a1b6db05425e573887673bddcf426f1b90" + ], + "Parent": "", + "Comment": "", + "Created": "2019-10-30T19:34:56.296666503Z", + "Container": "84597380a7968131ab47dd1b8183a96dcfe9e1e4acff1efe5824dcd762184a67", + "ContainerConfig": { + "Hostname": "84597380a796", + "Domainname": "", + "User": "vcap", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "CNB_USER_ID=2000", + "CNB_GROUP_ID=2000", + "CNB_STACK_ID=org.cloudfoundry.stacks.cflinuxfs3" + ], + "Cmd": [ + "/bin/sh", + "-c", + "#(nop) ", + "LABEL io.buildpacks.stack.id=org.cloudfoundry.stacks.cflinuxfs3" + ], + "Image": "sha256:523c8ade6e06f814469b2cf04c8045a74becee17088955f2657958476d3fba1f", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": null, + "Labels": { + "io.buildpacks.stack.id": "org.cloudfoundry.stacks.cflinuxfs3" + } + }, + "DockerVersion": "18.09.6", + "Author": "", + "Config": { + "Hostname": "", + "Domainname": "", + "User": "vcap", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "CNB_USER_ID=2000", + "CNB_GROUP_ID=2000", + "CNB_STACK_ID=org.cloudfoundry.stacks.cflinuxfs3" + ], + "Cmd": null, + "Image": "sha256:523c8ade6e06f814469b2cf04c8045a74becee17088955f2657958476d3fba1f", + "Volumes": null, + "WorkingDir": "/layers", + "Entrypoint": null, + "OnBuild": null, + "Labels": { + "io.buildpacks.builder.metadata": "{\"description\":\"cflinuxfs3 base image with buildpacks for Java, .NET, NodeJS, Python, Golang, PHP, HTTPD and NGINX\",\"buildpacks\":[{\"id\":\"org.cloudfoundry.googlestackdriver\",\"version\":\"v1.0.40\",\"latest\":true},{\"id\":\"org.cloudfoundry.buildsystem\",\"version\":\"v1.0.114\",\"latest\":true},{\"id\":\"org.cloudfoundry.jdbc\",\"version\":\"v1.0.94\",\"latest\":true},{\"id\":\"org.cloudfoundry.springautoreconfiguration\",\"version\":\"v1.0.100\",\"latest\":true},{\"id\":\"org.cloudfoundry.archiveexpanding\",\"version\":\"v1.0.87\",\"latest\":true},{\"id\":\"org.cloudfoundry.jvmapplication\",\"version\":\"v1.0.72\",\"latest\":true},{\"id\":\"org.cloudfoundry.debug\",\"version\":\"v1.0.92\",\"latest\":true},{\"id\":\"org.cloudfoundry.go\",\"version\":\"v0.0.1\",\"latest\":true},{\"id\":\"org.cloudfoundry.openjdk\",\"version\":\"v1.0.53\",\"latest\":true},{\"id\":\"org.cloudfoundry.procfile\",\"version\":\"v1.0.37\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core\",\"version\":\"v0.0.2\",\"latest\":true},{\"id\":\"org.cloudfoundry.azureapplicationinsights\",\"version\":\"v1.0.94\",\"latest\":true},{\"id\":\"org.cloudfoundry.php\",\"version\":\"v0.0.0-RC1\",\"latest\":true},{\"id\":\"org.cloudfoundry.tomcat\",\"version\":\"v1.1.9\",\"latest\":true},{\"id\":\"org.cloudfoundry.nodejs\",\"version\":\"v0.0.3\",\"latest\":true},{\"id\":\"org.cloudfoundry.jmx\",\"version\":\"v1.0.94\",\"latest\":true},{\"id\":\"org.cloudfoundry.springboot\",\"version\":\"v1.0.97\",\"latest\":true},{\"id\":\"org.cloudfoundry.distzip\",\"version\":\"v1.0.89\",\"latest\":true},{\"id\":\"org.cloudfoundry.python\",\"version\":\"v0.0.1\",\"latest\":true},{\"id\":\"org.cloudfoundry.dep\",\"version\":\"0.0.51\",\"latest\":true},{\"id\":\"org.cloudfoundry.go-compiler\",\"version\":\"0.0.48\",\"latest\":true},{\"id\":\"org.cloudfoundry.go-mod\",\"version\":\"0.0.44\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core-aspnet\",\"version\":\"0.0.53\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core-build\",\"version\":\"0.0.18\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core-conf\",\"version\":\"0.0.57\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core-runtime\",\"version\":\"0.0.66\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core-sdk\",\"version\":\"0.0.55\",\"latest\":true},{\"id\":\"org.cloudfoundry.httpd\",\"version\":\"0.0.21\",\"latest\":true},{\"id\":\"org.cloudfoundry.nginx\",\"version\":\"0.0.25\",\"latest\":true},{\"id\":\"org.cloudfoundry.php-composer\",\"version\":\"0.0.16\",\"latest\":true},{\"id\":\"org.cloudfoundry.php-dist\",\"version\":\"0.0.30\",\"latest\":true},{\"id\":\"org.cloudfoundry.php-web\",\"version\":\"0.0.24\",\"latest\":true},{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.85\",\"latest\":true},{\"id\":\"org.cloudfoundry.npm\",\"version\":\"0.0.53\",\"latest\":true},{\"id\":\"org.cloudfoundry.yarn\",\"version\":\"0.0.58\",\"latest\":true},{\"id\":\"org.cloudfoundry.conda\",\"version\":\"0.0.37\",\"latest\":true},{\"id\":\"org.cloudfoundry.pip\",\"version\":\"0.0.53\",\"latest\":true},{\"id\":\"org.cloudfoundry.pipenv\",\"version\":\"0.0.38\",\"latest\":true},{\"id\":\"org.cloudfoundry.python-runtime\",\"version\":\"0.0.57\",\"latest\":true}],\"groups\":[{\"buildpacks\":[{\"id\":\"org.cloudfoundry.archiveexpanding\",\"version\":\"v1.0.87\",\"optional\":true},{\"id\":\"org.cloudfoundry.openjdk\",\"version\":\"v1.0.53\"},{\"id\":\"org.cloudfoundry.buildsystem\",\"version\":\"v1.0.114\",\"optional\":true},{\"id\":\"org.cloudfoundry.jvmapplication\",\"version\":\"v1.0.72\"},{\"id\":\"org.cloudfoundry.tomcat\",\"version\":\"v1.1.9\",\"optional\":true},{\"id\":\"org.cloudfoundry.springboot\",\"version\":\"v1.0.97\",\"optional\":true},{\"id\":\"org.cloudfoundry.distzip\",\"version\":\"v1.0.89\",\"optional\":true},{\"id\":\"org.cloudfoundry.procfile\",\"version\":\"v1.0.37\",\"optional\":true},{\"id\":\"org.cloudfoundry.azureapplicationinsights\",\"version\":\"v1.0.94\",\"optional\":true},{\"id\":\"org.cloudfoundry.debug\",\"version\":\"v1.0.92\",\"optional\":true},{\"id\":\"org.cloudfoundry.googlestackdriver\",\"version\":\"v1.0.40\",\"optional\":true},{\"id\":\"org.cloudfoundry.jdbc\",\"version\":\"v1.0.94\",\"optional\":true},{\"id\":\"org.cloudfoundry.jmx\",\"version\":\"v1.0.94\",\"optional\":true},{\"id\":\"org.cloudfoundry.springautoreconfiguration\",\"version\":\"v1.0.100\",\"optional\":true}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.nodejs\",\"version\":\"v0.0.3\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.python\",\"version\":\"v0.0.1\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.go\",\"version\":\"v0.0.1\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.dotnet-core\",\"version\":\"v0.0.2\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.php\",\"version\":\"v0.0.0-RC1\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.httpd\",\"version\":\"0.0.21\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.nginx\",\"version\":\"0.0.25\"}]}],\"stack\":{\"runImage\":{\"image\":\"cloudfoundry/run:full-cnb\",\"mirrors\":null}},\"lifecycle\":{\"version\":\"0.5.0\",\"api\":{\"buildpack\":\"0.2\",\"platform\":\"0.1\"}},\"createdBy\":{\"name\":\"Pack CLI\",\"version\":\"v0.5.0 (git sha: c9cfac75b49609524e1ea33f809c12071406547c)\"}}", + "io.buildpacks.buildpack.layers": "{\"org.cloudfoundry.archiveexpanding\":{\"v1.0.87\":{\"layerDiffID\":\"sha256:391d950d763a33d8ae0373f218aa59907599f51e42cd864129591887e1291034\"}},\"org.cloudfoundry.azureapplicationinsights\":{\"v1.0.94\":{\"layerDiffID\":\"sha256:3544ba1fa82d1e89619ed04c2485fab3445b1603959d224792d1183dd658033d\"}},\"org.cloudfoundry.buildsystem\":{\"v1.0.114\":{\"layerDiffID\":\"sha256:0c6ddab305e5452850f3c09fe15310dff8dc7221702d736dc7705882c1df9658\"}},\"org.cloudfoundry.conda\":{\"0.0.37\":{\"layerDiffID\":\"sha256:0943c634f5c24311ebdeca6fef5682a4a374c89a831700d188bff7f987470004\"}},\"org.cloudfoundry.debug\":{\"v1.0.92\":{\"layerDiffID\":\"sha256:ef935546e2c99da3e8962f2eb3cd6813e9e9a8b19bc8d15b56d1cac37f0342d5\"}},\"org.cloudfoundry.dep\":{\"0.0.51\":{\"layerDiffID\":\"sha256:996aee90e29ed78d80a5a0c0e50d60a732a18fddae06f87b68bef183beddd2c4\"}},\"org.cloudfoundry.distzip\":{\"v1.0.89\":{\"layerDiffID\":\"sha256:e5df92d3db931488225ca9f7290de0334225d4bd7c48fc2dcd380d0921bb6680\"}},\"org.cloudfoundry.dotnet-core\":{\"v0.0.2\":{\"layerDiffID\":\"sha256:96c7f369c29bbf11b971e4dbb6473e8991b666f9de046a414317634eb0a25d2a\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.dotnet-core-runtime\",\"version\":\"0.0.66\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-aspnet\",\"version\":\"0.0.53\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-sdk\",\"version\":\"0.0.55\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-build\",\"version\":\"0.0.18\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-conf\",\"version\":\"0.0.57\"}]}]}},\"org.cloudfoundry.dotnet-core-aspnet\":{\"0.0.53\":{\"layerDiffID\":\"sha256:ab9aaff2160873663388faea6d987cd8f2b5935137b81c64fde145bf2a330d54\"}},\"org.cloudfoundry.dotnet-core-build\":{\"0.0.18\":{\"layerDiffID\":\"sha256:e1f3ab860045b96235cbc1b89a3e73add955a303eb42905b570b6012b73b9184\"}},\"org.cloudfoundry.dotnet-core-conf\":{\"0.0.57\":{\"layerDiffID\":\"sha256:0b260d90d097379d4351132b45110d013b98f4a335795baeb95788fcebcb7f3c\"}},\"org.cloudfoundry.dotnet-core-runtime\":{\"0.0.66\":{\"layerDiffID\":\"sha256:f0f5ecd72b4e0a38d3ad73b5756d8f209955932e9615715502a61dffe56f401a\"}},\"org.cloudfoundry.dotnet-core-sdk\":{\"0.0.55\":{\"layerDiffID\":\"sha256:b4cd790490e41c808e8d65f9ac8f2e58c79bc1a9919a713c4519e77b26dc2053\"}},\"org.cloudfoundry.go\":{\"v0.0.1\":{\"layerDiffID\":\"sha256:6d644992d62bd09a2bbf490b7fe3aa1e35e6d0d2479583c2decec7092f193310\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.go-compiler\",\"version\":\"0.0.48\"},{\"id\":\"org.cloudfoundry.go-mod\",\"version\":\"0.0.44\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.go-compiler\",\"version\":\"0.0.48\"},{\"id\":\"org.cloudfoundry.dep\",\"version\":\"0.0.51\"}]}]}},\"org.cloudfoundry.go-compiler\":{\"0.0.48\":{\"layerDiffID\":\"sha256:30f6a316d4da01d694d8c17aa84b37f468cccc7184248e255486eb3095ebb87c\"}},\"org.cloudfoundry.go-mod\":{\"0.0.44\":{\"layerDiffID\":\"sha256:c694476a7241ba4e4a0663606d4d6eec7ed8624252c010fbef2713968e8f9436\"}},\"org.cloudfoundry.googlestackdriver\":{\"v1.0.40\":{\"layerDiffID\":\"sha256:8debe4b6b4290dbbfecea9edea61c22fb455e69e3cbc7d63b17f8e1ab8ea669b\"}},\"org.cloudfoundry.httpd\":{\"0.0.21\":{\"layerDiffID\":\"sha256:16b88c0e7f950c32c7496117d1efad90a8557a2badcb267d99a19676b1f0b76a\"}},\"org.cloudfoundry.jdbc\":{\"v1.0.94\":{\"layerDiffID\":\"sha256:a9527973bb5d7ccdf88b5be8eb81e024094be1709df659af3127865463c1c188\"}},\"org.cloudfoundry.jmx\":{\"v1.0.94\":{\"layerDiffID\":\"sha256:52845fb94361dad36cc4136e49b92c79ca59c16c579e2f51df0c58ba355c4367\"}},\"org.cloudfoundry.jvmapplication\":{\"v1.0.72\":{\"layerDiffID\":\"sha256:5b3ec0a6ed9e3de93bb082151f56b1cde5d7e31f2809039a1b5b55a5052fe873\"}},\"org.cloudfoundry.nginx\":{\"0.0.25\":{\"layerDiffID\":\"sha256:49d36ba00b17fb605f374ca7877ae129678de925d10fd1955f07c2b6f74dd1c9\"}},\"org.cloudfoundry.node-engine\":{\"0.0.85\":{\"layerDiffID\":\"sha256:3d12e651068a0ff19afdd568b5d14ee5292f849542b31d6c9b099a09344e1f4d\"}},\"org.cloudfoundry.nodejs\":{\"v0.0.3\":{\"layerDiffID\":\"sha256:27ad0fc48c381eb77f69b4e80edccb4d8a2399f5cebd5a8c5a3e1c32313343a6\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.85\"},{\"id\":\"org.cloudfoundry.yarn\",\"version\":\"0.0.58\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.85\"},{\"id\":\"org.cloudfoundry.npm\",\"version\":\"0.0.53\"}]}]}},\"org.cloudfoundry.npm\":{\"0.0.53\":{\"layerDiffID\":\"sha256:f01e41975a9335f5983021b081bc700e46b85efb262670223c4db61eea0a3ebd\"}},\"org.cloudfoundry.openjdk\":{\"v1.0.53\":{\"layerDiffID\":\"sha256:59d817c36a25078c8ec1f6de0d8336aec598037f89708ed13dbf661557a25084\"}},\"org.cloudfoundry.php\":{\"v0.0.0-RC1\":{\"layerDiffID\":\"sha256:12d99406b52b526af152628cd72ba6eacf5d18484dc79cfdacd4b38a21620a2b\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.php-dist\",\"version\":\"0.0.30\"},{\"id\":\"org.cloudfoundry.php-composer\",\"version\":\"0.0.16\",\"optional\":true},{\"id\":\"org.cloudfoundry.httpd\",\"version\":\"0.0.21\",\"optional\":true},{\"id\":\"org.cloudfoundry.php-web\",\"version\":\"0.0.24\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.php-dist\",\"version\":\"0.0.30\"},{\"id\":\"org.cloudfoundry.php-composer\",\"version\":\"0.0.16\",\"optional\":true},{\"id\":\"org.cloudfoundry.nginx\",\"version\":\"0.0.25\",\"optional\":true},{\"id\":\"org.cloudfoundry.php-web\",\"version\":\"0.0.24\"}]}]}},\"org.cloudfoundry.php-composer\":{\"0.0.16\":{\"layerDiffID\":\"sha256:b31d189a88ca43fee6077c25bcb623582d569193ed6ac11b4e5623558911e3de\"}},\"org.cloudfoundry.php-dist\":{\"0.0.30\":{\"layerDiffID\":\"sha256:3ecfd2822cf64c609c9c8489e2accfbc0b1de0f2a3637ff1b5d30768fb34b40c\"}},\"org.cloudfoundry.php-web\":{\"0.0.24\":{\"layerDiffID\":\"sha256:a7f09c3e09b29c5503962a068f29e8726cb91d1dbce2fab688aee0a98189b2be\"}},\"org.cloudfoundry.pip\":{\"0.0.53\":{\"layerDiffID\":\"sha256:9a183e56c86d376b408bdf922746d0a657f62b0e18c7c8f82a496b87710c576f\"}},\"org.cloudfoundry.pipenv\":{\"0.0.38\":{\"layerDiffID\":\"sha256:d919f3c2f534ddbb0b6057f82bca36051ce80a2a9cd3016c320ae276884311f5\"}},\"org.cloudfoundry.procfile\":{\"v1.0.37\":{\"layerDiffID\":\"sha256:6636ce01d12372e56a89ec77ea8d9ed510f8c701df1220750add4613764c05a4\"}},\"org.cloudfoundry.python\":{\"v0.0.1\":{\"layerDiffID\":\"sha256:290ac64fbae3288821551371c8dda38fcf5dfa063a54cb270dcc395a090f5173\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.python-runtime\",\"version\":\"0.0.57\"},{\"id\":\"org.cloudfoundry.pipenv\",\"version\":\"0.0.38\",\"optional\":true},{\"id\":\"org.cloudfoundry.pip\",\"version\":\"0.0.53\",\"optional\":true}]},{\"group\":[{\"id\":\"org.cloudfoundry.conda\",\"version\":\"0.0.37\"}]}]}},\"org.cloudfoundry.python-runtime\":{\"0.0.57\":{\"layerDiffID\":\"sha256:108a3eb288f8094aab6ffd822c593902e48e85c8a37b7da2bd21b15f785d92c5\"}},\"org.cloudfoundry.springautoreconfiguration\":{\"v1.0.100\":{\"layerDiffID\":\"sha256:480cd420e43c6895240c87c88969b87417549c02393cde1b6f71a3a3d5a2a620\"}},\"org.cloudfoundry.springboot\":{\"v1.0.97\":{\"layerDiffID\":\"sha256:7bf3a57229276fb913155b077d00a18ec6cba92c7f062728ca1c3bc3503c0b55\"}},\"org.cloudfoundry.tomcat\":{\"v1.1.9\":{\"layerDiffID\":\"sha256:3b3cda9eceb0fca56c274e3be93daf53f59501e6b3628fabbaea8ea416eb757a\"}},\"org.cloudfoundry.yarn\":{\"0.0.58\":{\"layerDiffID\":\"sha256:2b1b655bb8752f631e786c4c55670315d8569acccfe26402942977c216f2803a\"}}}", + "io.buildpacks.buildpack.order": "[{\"group\":[{\"id\":\"org.cloudfoundry.archiveexpanding\",\"optional\":true},{\"id\":\"org.cloudfoundry.openjdk\"},{\"id\":\"org.cloudfoundry.buildsystem\",\"optional\":true},{\"id\":\"org.cloudfoundry.jvmapplication\"},{\"id\":\"org.cloudfoundry.tomcat\",\"optional\":true},{\"id\":\"org.cloudfoundry.springboot\",\"optional\":true},{\"id\":\"org.cloudfoundry.distzip\",\"optional\":true},{\"id\":\"org.cloudfoundry.procfile\",\"optional\":true},{\"id\":\"org.cloudfoundry.azureapplicationinsights\",\"optional\":true},{\"id\":\"org.cloudfoundry.debug\",\"optional\":true},{\"id\":\"org.cloudfoundry.googlestackdriver\",\"optional\":true},{\"id\":\"org.cloudfoundry.jdbc\",\"optional\":true},{\"id\":\"org.cloudfoundry.jmx\",\"optional\":true},{\"id\":\"org.cloudfoundry.springautoreconfiguration\",\"optional\":true}]},{\"group\":[{\"id\":\"org.cloudfoundry.nodejs\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.python\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.go\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.dotnet-core\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.php\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.httpd\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.nginx\"}]}]", + "io.buildpacks.stack.id": "org.cloudfoundry.stacks.cflinuxfs3" + } + }, + "Architecture": "amd64", + "Os": "linux", + "Size": 1559461360, + "VirtualSize": 1559461360, + "GraphDriver": { + "Data": { + "LowerDir": "/var/lib/docker/overlay2/58e30cd9f3a4da4e0d30f20c3b50de7655e261fb3d32f04818f1bd960c1e8b6c/diff:/var/lib/docker/overlay2/ad95d738069aa405ff17a9ebb1fdc32f8490b0dd885c3ba3a28e2c3b25d64641/diff:/var/lib/docker/overlay2/74d2896cfe9efc6945ff18870a7213583b987ecf4306e189ff6b793f77af5dcd/diff:/var/lib/docker/overlay2/1052615e5c240724e10928048f735cc9e7a7676a9af5f173b895df57c6921a40/diff:/var/lib/docker/overlay2/b5a62216c4282e7568e84427073f096551977c8c6f80d3a04ebb04c25730edde/diff:/var/lib/docker/overlay2/016a36bf7d7d7258eca08da62c01e47bf8e531531f914dde7cae33e191ab2218/diff:/var/lib/docker/overlay2/a585012bf1cf9da0472b2bbe86c4919355593e1a02cf399a9b012928eb816bcd/diff:/var/lib/docker/overlay2/b4aa8b70bd59d7b7dc6d6fb2e655c2334dc8360c764232f83d036d1f241e3298/diff:/var/lib/docker/overlay2/5f4cab16092522163e2dba6587b48d53ee3b09c8778b0736999bc120dd3753b1/diff:/var/lib/docker/overlay2/90e60622603d230f238976f4d9f65797fc9f070df62b1d2ccad0cefe4e205b43/diff:/var/lib/docker/overlay2/c43877934a580e47cc477ed46e71246468d7b6d7151abc5f1a97bb1e8c8104cf/diff:/var/lib/docker/overlay2/8734b165cabb3ff234a08d488f622135aeae9b7347cf41273445ff7d07aa4565/diff:/var/lib/docker/overlay2/2743cd9d4b7da84925b1b530732dad97108fe77e75865de580255579ba2cdb92/diff:/var/lib/docker/overlay2/68308d057b24bbcde7a4880f5db0e653743debdcc0ff3e736d1776296c4168a1/diff:/var/lib/docker/overlay2/7a4411dc4ac1ed7a1da9aabf088985b8b131e0db047e513f9890eb9c001c1895/diff:/var/lib/docker/overlay2/7f7c262fea8dea5ec86507188848ea391354a76468b09ec93523920e18a400ea/diff:/var/lib/docker/overlay2/8b3bfa567fb956204ad866e49489dacd2fdf5fbfa4f9b05ed3668e1106a5383b/diff:/var/lib/docker/overlay2/31bbc4f1616a35b7ce157266e44513963502e30d836a8fd7b7ee18436a8c46cf/diff:/var/lib/docker/overlay2/149b8e9f1142cdf6dcdfe17ea286ec17197f1a329cf23d5c82958a2032facf54/diff:/var/lib/docker/overlay2/92fb1e680083eb8314c5310bf10ced63ec2b0a98afbf84cc5175a98b3d44507a/diff:/var/lib/docker/overlay2/175a35b6f7af6eb91ca500dbd3d7e798f6d174cf8549881ffe5eed8e92a70b9f/diff:/var/lib/docker/overlay2/48ca54bbd27f7df19acf2b6cc719d05dd3b63f8133038a55d216a4498d4dc913/diff:/var/lib/docker/overlay2/ffe3cc3b93c9030f9dcb0e64c258d1e554f1f0cf27a0f8d4e98bb7ece5ffe882/diff:/var/lib/docker/overlay2/1fb2d962bb27e95c40a9a2c1aa910ca847d186d04e3d7dcdf93967101cc30dde/diff:/var/lib/docker/overlay2/10b34138f9e9e8d70c684d0a564452b1309363441b9d7e048f75e0e1179411dc/diff:/var/lib/docker/overlay2/1d888c7e9c62c22ccda6478f03f3df4b43d43fa3b32a2c2fdc9345fdc7193cd9/diff:/var/lib/docker/overlay2/649fc275c002d7336b277365636e1c8e5651bb3ed1557806d26dd6dfa1d9119a/diff:/var/lib/docker/overlay2/4484c2c0ee4a20aa17017c8cd54c842c876fea32afb297e88614d759ec5410dc/diff:/var/lib/docker/overlay2/bd5f374e0ea6749c90535d778f2689c076b7290ad9d3f050af0a40c9626fdea4/diff:/var/lib/docker/overlay2/c6ba97531b15be65bccaf7ebc866d8bc0b88ce838b224aceb196a55824b289a5/diff:/var/lib/docker/overlay2/6c65fab249fe652cd20a6391b2e0786379b6d2c7d4fde02914dfb4fac84035bd/diff:/var/lib/docker/overlay2/f391b54493024e0183331b8ec7835107bc1b84b8a6e77d852f5357724eb940ff/diff:/var/lib/docker/overlay2/8044f9e3ceb529c80531fa2fe52ad550286f788e69843f235e7d756b90c213b8/diff:/var/lib/docker/overlay2/7d3b5539c46c9f0e7c4f6f733f435d1bf6428a8ca81ba71f4da1031cef58aa6c/diff:/var/lib/docker/overlay2/b8080b36b0ddec4e4d738571ddf9d89815f6a95a555d282cfebb73519b4835a0/diff:/var/lib/docker/overlay2/8a737007d5862aa43119254122eb7050c8bd110a3b653c8d6afca23e76fc4042/diff:/var/lib/docker/overlay2/3bb8f3670831e2031be2173381caf02874ad72e664716a990a330bcc3454f4a2/diff:/var/lib/docker/overlay2/cbd675efde19ccac72d3566404e5df8b152a9063c1668d8154711c7db398f852/diff:/var/lib/docker/overlay2/84fb9095136cb645f7f15aeeeba1db6fae3999cb48a559daf8dd46bf3befbeba/diff:/var/lib/docker/overlay2/cbc51912822c4a3fb8624e0cf678e5dedeb76dc2fa0e5bc56f3cbfbfefb26d68/diff:/var/lib/docker/overlay2/d08d5bdcf39aaf46bdf1e0f4576bb64931af646213ff350065b4d306e00f7e28/diff:/var/lib/docker/overlay2/cf180c218fe181bdf836065c5e85103816ea9e8dbb8ab54fb311209c33455eb2/diff:/var/lib/docker/overlay2/b0aef801fd38973eaf116001e05e7c3f8e2eb58ccc7ed37a4bd8d4fcc2ad172b/diff:/var/lib/docker/overlay2/f73c585ae34bd962e1fee2c3e2d95d47b9daf68b23cf469fb13bc3282cf77238/diff:/var/lib/docker/overlay2/c071c8471b26e55a90b6573a21c581dec43b6c7683a3fe87cb33a0734c83342a/diff", + "MergedDir": "/var/lib/docker/overlay2/41ced64ea40f3382f7a475030a5bc89b9c86e2a03d43031c5eba3c5c72616c2b/merged", + "UpperDir": "/var/lib/docker/overlay2/41ced64ea40f3382f7a475030a5bc89b9c86e2a03d43031c5eba3c5c72616c2b/diff", + "WorkDir": "/var/lib/docker/overlay2/41ced64ea40f3382f7a475030a5bc89b9c86e2a03d43031c5eba3c5c72616c2b/work" + }, + "Name": "overlay2" + }, + "RootFS": { + "Type": "layers", + "Layers": [ + "sha256:733a8e5ce32984099ef675fce04730f6e2a6dcfdf5bd292fea01a8f936265342", + "sha256:7755b972f0b4f49de73ef5114fb3ba9c69d80f217e80da99f56f0d0a5dcb3d70", + "sha256:8f0b2d09ab4b38530a1630403967d11a601e56e02e79d3f56370d34fd071fe38", + "sha256:8debe4b6b4290dbbfecea9edea61c22fb455e69e3cbc7d63b17f8e1ab8ea669b", + "sha256:0c6ddab305e5452850f3c09fe15310dff8dc7221702d736dc7705882c1df9658", + "sha256:a9527973bb5d7ccdf88b5be8eb81e024094be1709df659af3127865463c1c188", + "sha256:480cd420e43c6895240c87c88969b87417549c02393cde1b6f71a3a3d5a2a620", + "sha256:391d950d763a33d8ae0373f218aa59907599f51e42cd864129591887e1291034", + "sha256:5b3ec0a6ed9e3de93bb082151f56b1cde5d7e31f2809039a1b5b55a5052fe873", + "sha256:ef935546e2c99da3e8962f2eb3cd6813e9e9a8b19bc8d15b56d1cac37f0342d5", + "sha256:6d644992d62bd09a2bbf490b7fe3aa1e35e6d0d2479583c2decec7092f193310", + "sha256:59d817c36a25078c8ec1f6de0d8336aec598037f89708ed13dbf661557a25084", + "sha256:6636ce01d12372e56a89ec77ea8d9ed510f8c701df1220750add4613764c05a4", + "sha256:96c7f369c29bbf11b971e4dbb6473e8991b666f9de046a414317634eb0a25d2a", + "sha256:3544ba1fa82d1e89619ed04c2485fab3445b1603959d224792d1183dd658033d", + "sha256:12d99406b52b526af152628cd72ba6eacf5d18484dc79cfdacd4b38a21620a2b", + "sha256:3b3cda9eceb0fca56c274e3be93daf53f59501e6b3628fabbaea8ea416eb757a", + "sha256:27ad0fc48c381eb77f69b4e80edccb4d8a2399f5cebd5a8c5a3e1c32313343a6", + "sha256:52845fb94361dad36cc4136e49b92c79ca59c16c579e2f51df0c58ba355c4367", + "sha256:7bf3a57229276fb913155b077d00a18ec6cba92c7f062728ca1c3bc3503c0b55", + "sha256:e5df92d3db931488225ca9f7290de0334225d4bd7c48fc2dcd380d0921bb6680", + "sha256:290ac64fbae3288821551371c8dda38fcf5dfa063a54cb270dcc395a090f5173", + "sha256:996aee90e29ed78d80a5a0c0e50d60a732a18fddae06f87b68bef183beddd2c4", + "sha256:30f6a316d4da01d694d8c17aa84b37f468cccc7184248e255486eb3095ebb87c", + "sha256:c694476a7241ba4e4a0663606d4d6eec7ed8624252c010fbef2713968e8f9436", + "sha256:ab9aaff2160873663388faea6d987cd8f2b5935137b81c64fde145bf2a330d54", + "sha256:e1f3ab860045b96235cbc1b89a3e73add955a303eb42905b570b6012b73b9184", + "sha256:0b260d90d097379d4351132b45110d013b98f4a335795baeb95788fcebcb7f3c", + "sha256:f0f5ecd72b4e0a38d3ad73b5756d8f209955932e9615715502a61dffe56f401a", + "sha256:b4cd790490e41c808e8d65f9ac8f2e58c79bc1a9919a713c4519e77b26dc2053", + "sha256:16b88c0e7f950c32c7496117d1efad90a8557a2badcb267d99a19676b1f0b76a", + "sha256:49d36ba00b17fb605f374ca7877ae129678de925d10fd1955f07c2b6f74dd1c9", + "sha256:b31d189a88ca43fee6077c25bcb623582d569193ed6ac11b4e5623558911e3de", + "sha256:3ecfd2822cf64c609c9c8489e2accfbc0b1de0f2a3637ff1b5d30768fb34b40c", + "sha256:a7f09c3e09b29c5503962a068f29e8726cb91d1dbce2fab688aee0a98189b2be", + "sha256:3d12e651068a0ff19afdd568b5d14ee5292f849542b31d6c9b099a09344e1f4d", + "sha256:f01e41975a9335f5983021b081bc700e46b85efb262670223c4db61eea0a3ebd", + "sha256:2b1b655bb8752f631e786c4c55670315d8569acccfe26402942977c216f2803a", + "sha256:0943c634f5c24311ebdeca6fef5682a4a374c89a831700d188bff7f987470004", + "sha256:9a183e56c86d376b408bdf922746d0a657f62b0e18c7c8f82a496b87710c576f", + "sha256:d919f3c2f534ddbb0b6057f82bca36051ce80a2a9cd3016c320ae276884311f5", + "sha256:108a3eb288f8094aab6ffd822c593902e48e85c8a37b7da2bd21b15f785d92c5", + "sha256:f8b5dcfa1d082af23bb2b2c08526131921329d48d1614d9f2f163a997176087a", + "sha256:ee13e75c33e0af49fbf6c3aaa5bbd102fc468c2d554c4f94763d35a33964dfe4", + "sha256:2571abab1776d4c2e427fba10d61531afff2ab0789f89ef46ce925b6a5d98e0f", + "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" + ] + }, + "Metadata": { + "LastTagTime": "0001-01-01T00:00:00Z" + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/lifecycle-analyzer.json b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/lifecycle-analyzer.json new file mode 100644 index 0000000000..64d1e643fe --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/lifecycle-analyzer.json @@ -0,0 +1,11 @@ +{ + "User" : "root", + "Image" : "pack.local/ephemeral-builder", + "Cmd" : [ "/lifecycle/analyzer", "-daemon", "-layers", "/layers", "docker.io/library/my-application:latest" ], + "Labels" : { + "author" : "spring-boot" + }, + "HostConfig" : { + "Binds" : [ "/var/run/docker.sock:/var/run/docker.sock", "pack-layers-aaaaaaaaaa:/layers", "pack-app-aaaaaaaaaa:/workspace" ] + } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/lifecycle-builder.json b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/lifecycle-builder.json new file mode 100644 index 0000000000..54153a9155 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/lifecycle-builder.json @@ -0,0 +1,10 @@ +{ + "Image" : "pack.local/ephemeral-builder", + "Cmd" : [ "/lifecycle/builder", "-layers", "/layers", "-app", "/workspace", "-platform", "/platform" ], + "Labels" : { + "author" : "spring-boot" + }, + "HostConfig" : { + "Binds" : [ "pack-layers-aaaaaaaaaa:/layers", "pack-app-aaaaaaaaaa:/workspace" ] + } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/lifecycle-cacher.json b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/lifecycle-cacher.json new file mode 100644 index 0000000000..b8cf97817b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/lifecycle-cacher.json @@ -0,0 +1,11 @@ +{ + "User" : "root", + "Image" : "pack.local/ephemeral-builder", + "Cmd" : [ "/lifecycle/cacher", "-path", "/cache", "-layers", "/layers" ], + "Labels" : { + "author" : "spring-boot" + }, + "HostConfig" : { + "Binds" : [ "/var/run/docker.sock:/var/run/docker.sock", "pack-layers-aaaaaaaaaa:/layers", "pack-app-aaaaaaaaaa:/workspace", "pack-cache-b35197ac41ea.build:/cache" ] + } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/lifecycle-detector.json b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/lifecycle-detector.json new file mode 100644 index 0000000000..fc545e72c9 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/lifecycle-detector.json @@ -0,0 +1,10 @@ +{ + "Image" : "pack.local/ephemeral-builder", + "Cmd" : [ "/lifecycle/detector", "-app", "/workspace", "-platform", "/platform" ], + "Labels" : { + "author" : "spring-boot" + }, + "HostConfig" : { + "Binds" : [ "pack-layers-aaaaaaaaaa:/layers", "pack-app-aaaaaaaaaa:/workspace" ] + } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/lifecycle-exporter.json b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/lifecycle-exporter.json new file mode 100644 index 0000000000..359b5e96db --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/lifecycle-exporter.json @@ -0,0 +1,11 @@ +{ + "User" : "root", + "Image" : "pack.local/ephemeral-builder", + "Cmd" : [ "/lifecycle/exporter", "-image", "docker.io/cloudfoundry/run", "-layers", "/layers", "-app", "/workspace", "-daemon", "-launch-cache", "/launch-cache", "docker.io/library/my-application:latest" ], + "Labels" : { + "author" : "spring-boot" + }, + "HostConfig" : { + "Binds" : [ "/var/run/docker.sock:/var/run/docker.sock", "pack-layers-aaaaaaaaaa:/layers", "pack-app-aaaaaaaaaa:/workspace", "pack-cache-b35197ac41ea.launch:/launch-cache" ] + } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/lifecycle-restorer.json b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/lifecycle-restorer.json new file mode 100644 index 0000000000..21f720b876 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/lifecycle-restorer.json @@ -0,0 +1,11 @@ +{ + "User" : "root", + "Image" : "pack.local/ephemeral-builder", + "Cmd" : [ "/lifecycle/restorer", "-path", "/cache", "-layers", "/layers" ], + "Labels" : { + "author" : "spring-boot" + }, + "HostConfig" : { + "Binds" : [ "/var/run/docker.sock:/var/run/docker.sock", "pack-layers-aaaaaaaaaa:/layers", "pack-app-aaaaaaaaaa:/workspace", "pack-cache-b35197ac41ea.build:/cache" ] + } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/print-stream-build-log.txt b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/print-stream-build-log.txt new file mode 100644 index 0000000000..83cfdffd0a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/print-stream-build-log.txt @@ -0,0 +1,19 @@ +Building image 'docker.io/library/my-app:latest' + + > Pulling builder image 'docker.io/cnb/builder' .................................................. + > Pulled builder image '00000001' + > Pulling run image 'docker.io/cnb/runner' .................................................. + > Pulled run image '00000002' + > Executing lifecycle version v0.5.0 + > Using build cache volume 'pack-abc.cache' + + > Running alphabet + [alphabet] one + [alphabet] two + [alphabet] three + + > Running basket + [basket] spring + [basket] boot + +Successfully built image 'docker.io/library/my-app:latest' diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/run-image-with-bad-stack.json b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/run-image-with-bad-stack.json new file mode 100644 index 0000000000..66f0ed3336 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/run-image-with-bad-stack.json @@ -0,0 +1,142 @@ +{ + "Id": "sha256:9b450bffdb05bcf660d464d0bfdf344ee6ca38e9b8de4f408c8080b0c9319349", + "RepoTags": [ + "cloudfoundry/cnb:latest" + ], + "RepoDigests": [ + "cloudfoundry/run@sha256:715806bb793b66e3fc1a5a8f5584c6a1b6db05425e573887673bddcf426f1b90" + ], + "Parent": "", + "Comment": "", + "Created": "2019-10-30T19:34:56.296666503Z", + "Container": "84597380a7968131ab47dd1b8183a96dcfe9e1e4acff1efe5824dcd762184a67", + "ContainerConfig": { + "Hostname": "84597380a796", + "Domainname": "", + "User": "vcap", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "CNB_USER_ID=2000", + "CNB_GROUP_ID=2000", + "CNB_STACK_ID=org.cloudfoundry.stacks.cflinuxfs3" + ], + "Cmd": [ + "/bin/sh", + "-c", + "#(nop) ", + "LABEL io.buildpacks.stack.id=org.cloudfoundry.stacks.cflinuxfs3" + ], + "Image": "sha256:523c8ade6e06f814469b2cf04c8045a74becee17088955f2657958476d3fba1f", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": null, + "Labels": { + "io.buildpacks.stack.id": "org.cloudfoundry.stacks.cflinuxfs3" + } + }, + "DockerVersion": "18.09.6", + "Author": "", + "Config": { + "Hostname": "", + "Domainname": "", + "User": "vcap", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "CNB_USER_ID=2000", + "CNB_GROUP_ID=2000", + "CNB_STACK_ID=org.cloudfoundry.stacks.cflinuxfs3" + ], + "Cmd": null, + "Image": "sha256:523c8ade6e06f814469b2cf04c8045a74becee17088955f2657958476d3fba1f", + "Volumes": null, + "WorkingDir": "/layers", + "Entrypoint": null, + "OnBuild": null, + "Labels": { + "io.buildpacks.builder.metadata": "{\"description\":\"cflinuxfs3 base image with buildpacks for Java, .NET, NodeJS, Python, Golang, PHP, HTTPD and NGINX\",\"buildpacks\":[{\"id\":\"org.cloudfoundry.googlestackdriver\",\"version\":\"v1.0.40\",\"latest\":true},{\"id\":\"org.cloudfoundry.buildsystem\",\"version\":\"v1.0.114\",\"latest\":true},{\"id\":\"org.cloudfoundry.jdbc\",\"version\":\"v1.0.94\",\"latest\":true},{\"id\":\"org.cloudfoundry.springautoreconfiguration\",\"version\":\"v1.0.100\",\"latest\":true},{\"id\":\"org.cloudfoundry.archiveexpanding\",\"version\":\"v1.0.87\",\"latest\":true},{\"id\":\"org.cloudfoundry.jvmapplication\",\"version\":\"v1.0.72\",\"latest\":true},{\"id\":\"org.cloudfoundry.debug\",\"version\":\"v1.0.92\",\"latest\":true},{\"id\":\"org.cloudfoundry.go\",\"version\":\"v0.0.1\",\"latest\":true},{\"id\":\"org.cloudfoundry.openjdk\",\"version\":\"v1.0.53\",\"latest\":true},{\"id\":\"org.cloudfoundry.procfile\",\"version\":\"v1.0.37\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core\",\"version\":\"v0.0.2\",\"latest\":true},{\"id\":\"org.cloudfoundry.azureapplicationinsights\",\"version\":\"v1.0.94\",\"latest\":true},{\"id\":\"org.cloudfoundry.php\",\"version\":\"v0.0.0-RC1\",\"latest\":true},{\"id\":\"org.cloudfoundry.tomcat\",\"version\":\"v1.1.9\",\"latest\":true},{\"id\":\"org.cloudfoundry.nodejs\",\"version\":\"v0.0.3\",\"latest\":true},{\"id\":\"org.cloudfoundry.jmx\",\"version\":\"v1.0.94\",\"latest\":true},{\"id\":\"org.cloudfoundry.springboot\",\"version\":\"v1.0.97\",\"latest\":true},{\"id\":\"org.cloudfoundry.distzip\",\"version\":\"v1.0.89\",\"latest\":true},{\"id\":\"org.cloudfoundry.python\",\"version\":\"v0.0.1\",\"latest\":true},{\"id\":\"org.cloudfoundry.dep\",\"version\":\"0.0.51\",\"latest\":true},{\"id\":\"org.cloudfoundry.go-compiler\",\"version\":\"0.0.48\",\"latest\":true},{\"id\":\"org.cloudfoundry.go-mod\",\"version\":\"0.0.44\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core-aspnet\",\"version\":\"0.0.53\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core-build\",\"version\":\"0.0.18\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core-conf\",\"version\":\"0.0.57\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core-runtime\",\"version\":\"0.0.66\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core-sdk\",\"version\":\"0.0.55\",\"latest\":true},{\"id\":\"org.cloudfoundry.httpd\",\"version\":\"0.0.21\",\"latest\":true},{\"id\":\"org.cloudfoundry.nginx\",\"version\":\"0.0.25\",\"latest\":true},{\"id\":\"org.cloudfoundry.php-composer\",\"version\":\"0.0.16\",\"latest\":true},{\"id\":\"org.cloudfoundry.php-dist\",\"version\":\"0.0.30\",\"latest\":true},{\"id\":\"org.cloudfoundry.php-web\",\"version\":\"0.0.24\",\"latest\":true},{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.85\",\"latest\":true},{\"id\":\"org.cloudfoundry.npm\",\"version\":\"0.0.53\",\"latest\":true},{\"id\":\"org.cloudfoundry.yarn\",\"version\":\"0.0.58\",\"latest\":true},{\"id\":\"org.cloudfoundry.conda\",\"version\":\"0.0.37\",\"latest\":true},{\"id\":\"org.cloudfoundry.pip\",\"version\":\"0.0.53\",\"latest\":true},{\"id\":\"org.cloudfoundry.pipenv\",\"version\":\"0.0.38\",\"latest\":true},{\"id\":\"org.cloudfoundry.python-runtime\",\"version\":\"0.0.57\",\"latest\":true}],\"groups\":[{\"buildpacks\":[{\"id\":\"org.cloudfoundry.archiveexpanding\",\"version\":\"v1.0.87\",\"optional\":true},{\"id\":\"org.cloudfoundry.openjdk\",\"version\":\"v1.0.53\"},{\"id\":\"org.cloudfoundry.buildsystem\",\"version\":\"v1.0.114\",\"optional\":true},{\"id\":\"org.cloudfoundry.jvmapplication\",\"version\":\"v1.0.72\"},{\"id\":\"org.cloudfoundry.tomcat\",\"version\":\"v1.1.9\",\"optional\":true},{\"id\":\"org.cloudfoundry.springboot\",\"version\":\"v1.0.97\",\"optional\":true},{\"id\":\"org.cloudfoundry.distzip\",\"version\":\"v1.0.89\",\"optional\":true},{\"id\":\"org.cloudfoundry.procfile\",\"version\":\"v1.0.37\",\"optional\":true},{\"id\":\"org.cloudfoundry.azureapplicationinsights\",\"version\":\"v1.0.94\",\"optional\":true},{\"id\":\"org.cloudfoundry.debug\",\"version\":\"v1.0.92\",\"optional\":true},{\"id\":\"org.cloudfoundry.googlestackdriver\",\"version\":\"v1.0.40\",\"optional\":true},{\"id\":\"org.cloudfoundry.jdbc\",\"version\":\"v1.0.94\",\"optional\":true},{\"id\":\"org.cloudfoundry.jmx\",\"version\":\"v1.0.94\",\"optional\":true},{\"id\":\"org.cloudfoundry.springautoreconfiguration\",\"version\":\"v1.0.100\",\"optional\":true}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.nodejs\",\"version\":\"v0.0.3\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.python\",\"version\":\"v0.0.1\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.go\",\"version\":\"v0.0.1\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.dotnet-core\",\"version\":\"v0.0.2\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.php\",\"version\":\"v0.0.0-RC1\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.httpd\",\"version\":\"0.0.21\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.nginx\",\"version\":\"0.0.25\"}]}],\"stack\":{\"runImage\":{\"image\":\"cloudfoundry/run:full-cnb\",\"mirrors\":null}},\"lifecycle\":{\"version\":\"0.5.0\",\"api\":{\"buildpack\":\"0.2\",\"platform\":\"0.1\"}},\"createdBy\":{\"name\":\"Pack CLI\",\"version\":\"v0.5.0 (git sha: c9cfac75b49609524e1ea33f809c12071406547c)\"}}", + "io.buildpacks.buildpack.layers": "{\"org.cloudfoundry.archiveexpanding\":{\"v1.0.87\":{\"layerDiffID\":\"sha256:391d950d763a33d8ae0373f218aa59907599f51e42cd864129591887e1291034\"}},\"org.cloudfoundry.azureapplicationinsights\":{\"v1.0.94\":{\"layerDiffID\":\"sha256:3544ba1fa82d1e89619ed04c2485fab3445b1603959d224792d1183dd658033d\"}},\"org.cloudfoundry.buildsystem\":{\"v1.0.114\":{\"layerDiffID\":\"sha256:0c6ddab305e5452850f3c09fe15310dff8dc7221702d736dc7705882c1df9658\"}},\"org.cloudfoundry.conda\":{\"0.0.37\":{\"layerDiffID\":\"sha256:0943c634f5c24311ebdeca6fef5682a4a374c89a831700d188bff7f987470004\"}},\"org.cloudfoundry.debug\":{\"v1.0.92\":{\"layerDiffID\":\"sha256:ef935546e2c99da3e8962f2eb3cd6813e9e9a8b19bc8d15b56d1cac37f0342d5\"}},\"org.cloudfoundry.dep\":{\"0.0.51\":{\"layerDiffID\":\"sha256:996aee90e29ed78d80a5a0c0e50d60a732a18fddae06f87b68bef183beddd2c4\"}},\"org.cloudfoundry.distzip\":{\"v1.0.89\":{\"layerDiffID\":\"sha256:e5df92d3db931488225ca9f7290de0334225d4bd7c48fc2dcd380d0921bb6680\"}},\"org.cloudfoundry.dotnet-core\":{\"v0.0.2\":{\"layerDiffID\":\"sha256:96c7f369c29bbf11b971e4dbb6473e8991b666f9de046a414317634eb0a25d2a\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.dotnet-core-runtime\",\"version\":\"0.0.66\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-aspnet\",\"version\":\"0.0.53\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-sdk\",\"version\":\"0.0.55\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-build\",\"version\":\"0.0.18\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-conf\",\"version\":\"0.0.57\"}]}]}},\"org.cloudfoundry.dotnet-core-aspnet\":{\"0.0.53\":{\"layerDiffID\":\"sha256:ab9aaff2160873663388faea6d987cd8f2b5935137b81c64fde145bf2a330d54\"}},\"org.cloudfoundry.dotnet-core-build\":{\"0.0.18\":{\"layerDiffID\":\"sha256:e1f3ab860045b96235cbc1b89a3e73add955a303eb42905b570b6012b73b9184\"}},\"org.cloudfoundry.dotnet-core-conf\":{\"0.0.57\":{\"layerDiffID\":\"sha256:0b260d90d097379d4351132b45110d013b98f4a335795baeb95788fcebcb7f3c\"}},\"org.cloudfoundry.dotnet-core-runtime\":{\"0.0.66\":{\"layerDiffID\":\"sha256:f0f5ecd72b4e0a38d3ad73b5756d8f209955932e9615715502a61dffe56f401a\"}},\"org.cloudfoundry.dotnet-core-sdk\":{\"0.0.55\":{\"layerDiffID\":\"sha256:b4cd790490e41c808e8d65f9ac8f2e58c79bc1a9919a713c4519e77b26dc2053\"}},\"org.cloudfoundry.go\":{\"v0.0.1\":{\"layerDiffID\":\"sha256:6d644992d62bd09a2bbf490b7fe3aa1e35e6d0d2479583c2decec7092f193310\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.go-compiler\",\"version\":\"0.0.48\"},{\"id\":\"org.cloudfoundry.go-mod\",\"version\":\"0.0.44\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.go-compiler\",\"version\":\"0.0.48\"},{\"id\":\"org.cloudfoundry.dep\",\"version\":\"0.0.51\"}]}]}},\"org.cloudfoundry.go-compiler\":{\"0.0.48\":{\"layerDiffID\":\"sha256:30f6a316d4da01d694d8c17aa84b37f468cccc7184248e255486eb3095ebb87c\"}},\"org.cloudfoundry.go-mod\":{\"0.0.44\":{\"layerDiffID\":\"sha256:c694476a7241ba4e4a0663606d4d6eec7ed8624252c010fbef2713968e8f9436\"}},\"org.cloudfoundry.googlestackdriver\":{\"v1.0.40\":{\"layerDiffID\":\"sha256:8debe4b6b4290dbbfecea9edea61c22fb455e69e3cbc7d63b17f8e1ab8ea669b\"}},\"org.cloudfoundry.httpd\":{\"0.0.21\":{\"layerDiffID\":\"sha256:16b88c0e7f950c32c7496117d1efad90a8557a2badcb267d99a19676b1f0b76a\"}},\"org.cloudfoundry.jdbc\":{\"v1.0.94\":{\"layerDiffID\":\"sha256:a9527973bb5d7ccdf88b5be8eb81e024094be1709df659af3127865463c1c188\"}},\"org.cloudfoundry.jmx\":{\"v1.0.94\":{\"layerDiffID\":\"sha256:52845fb94361dad36cc4136e49b92c79ca59c16c579e2f51df0c58ba355c4367\"}},\"org.cloudfoundry.jvmapplication\":{\"v1.0.72\":{\"layerDiffID\":\"sha256:5b3ec0a6ed9e3de93bb082151f56b1cde5d7e31f2809039a1b5b55a5052fe873\"}},\"org.cloudfoundry.nginx\":{\"0.0.25\":{\"layerDiffID\":\"sha256:49d36ba00b17fb605f374ca7877ae129678de925d10fd1955f07c2b6f74dd1c9\"}},\"org.cloudfoundry.node-engine\":{\"0.0.85\":{\"layerDiffID\":\"sha256:3d12e651068a0ff19afdd568b5d14ee5292f849542b31d6c9b099a09344e1f4d\"}},\"org.cloudfoundry.nodejs\":{\"v0.0.3\":{\"layerDiffID\":\"sha256:27ad0fc48c381eb77f69b4e80edccb4d8a2399f5cebd5a8c5a3e1c32313343a6\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.85\"},{\"id\":\"org.cloudfoundry.yarn\",\"version\":\"0.0.58\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.85\"},{\"id\":\"org.cloudfoundry.npm\",\"version\":\"0.0.53\"}]}]}},\"org.cloudfoundry.npm\":{\"0.0.53\":{\"layerDiffID\":\"sha256:f01e41975a9335f5983021b081bc700e46b85efb262670223c4db61eea0a3ebd\"}},\"org.cloudfoundry.openjdk\":{\"v1.0.53\":{\"layerDiffID\":\"sha256:59d817c36a25078c8ec1f6de0d8336aec598037f89708ed13dbf661557a25084\"}},\"org.cloudfoundry.php\":{\"v0.0.0-RC1\":{\"layerDiffID\":\"sha256:12d99406b52b526af152628cd72ba6eacf5d18484dc79cfdacd4b38a21620a2b\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.php-dist\",\"version\":\"0.0.30\"},{\"id\":\"org.cloudfoundry.php-composer\",\"version\":\"0.0.16\",\"optional\":true},{\"id\":\"org.cloudfoundry.httpd\",\"version\":\"0.0.21\",\"optional\":true},{\"id\":\"org.cloudfoundry.php-web\",\"version\":\"0.0.24\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.php-dist\",\"version\":\"0.0.30\"},{\"id\":\"org.cloudfoundry.php-composer\",\"version\":\"0.0.16\",\"optional\":true},{\"id\":\"org.cloudfoundry.nginx\",\"version\":\"0.0.25\",\"optional\":true},{\"id\":\"org.cloudfoundry.php-web\",\"version\":\"0.0.24\"}]}]}},\"org.cloudfoundry.php-composer\":{\"0.0.16\":{\"layerDiffID\":\"sha256:b31d189a88ca43fee6077c25bcb623582d569193ed6ac11b4e5623558911e3de\"}},\"org.cloudfoundry.php-dist\":{\"0.0.30\":{\"layerDiffID\":\"sha256:3ecfd2822cf64c609c9c8489e2accfbc0b1de0f2a3637ff1b5d30768fb34b40c\"}},\"org.cloudfoundry.php-web\":{\"0.0.24\":{\"layerDiffID\":\"sha256:a7f09c3e09b29c5503962a068f29e8726cb91d1dbce2fab688aee0a98189b2be\"}},\"org.cloudfoundry.pip\":{\"0.0.53\":{\"layerDiffID\":\"sha256:9a183e56c86d376b408bdf922746d0a657f62b0e18c7c8f82a496b87710c576f\"}},\"org.cloudfoundry.pipenv\":{\"0.0.38\":{\"layerDiffID\":\"sha256:d919f3c2f534ddbb0b6057f82bca36051ce80a2a9cd3016c320ae276884311f5\"}},\"org.cloudfoundry.procfile\":{\"v1.0.37\":{\"layerDiffID\":\"sha256:6636ce01d12372e56a89ec77ea8d9ed510f8c701df1220750add4613764c05a4\"}},\"org.cloudfoundry.python\":{\"v0.0.1\":{\"layerDiffID\":\"sha256:290ac64fbae3288821551371c8dda38fcf5dfa063a54cb270dcc395a090f5173\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.python-runtime\",\"version\":\"0.0.57\"},{\"id\":\"org.cloudfoundry.pipenv\",\"version\":\"0.0.38\",\"optional\":true},{\"id\":\"org.cloudfoundry.pip\",\"version\":\"0.0.53\",\"optional\":true}]},{\"group\":[{\"id\":\"org.cloudfoundry.conda\",\"version\":\"0.0.37\"}]}]}},\"org.cloudfoundry.python-runtime\":{\"0.0.57\":{\"layerDiffID\":\"sha256:108a3eb288f8094aab6ffd822c593902e48e85c8a37b7da2bd21b15f785d92c5\"}},\"org.cloudfoundry.springautoreconfiguration\":{\"v1.0.100\":{\"layerDiffID\":\"sha256:480cd420e43c6895240c87c88969b87417549c02393cde1b6f71a3a3d5a2a620\"}},\"org.cloudfoundry.springboot\":{\"v1.0.97\":{\"layerDiffID\":\"sha256:7bf3a57229276fb913155b077d00a18ec6cba92c7f062728ca1c3bc3503c0b55\"}},\"org.cloudfoundry.tomcat\":{\"v1.1.9\":{\"layerDiffID\":\"sha256:3b3cda9eceb0fca56c274e3be93daf53f59501e6b3628fabbaea8ea416eb757a\"}},\"org.cloudfoundry.yarn\":{\"0.0.58\":{\"layerDiffID\":\"sha256:2b1b655bb8752f631e786c4c55670315d8569acccfe26402942977c216f2803a\"}}}", + "io.buildpacks.buildpack.order": "[{\"group\":[{\"id\":\"org.cloudfoundry.archiveexpanding\",\"optional\":true},{\"id\":\"org.cloudfoundry.openjdk\"},{\"id\":\"org.cloudfoundry.buildsystem\",\"optional\":true},{\"id\":\"org.cloudfoundry.jvmapplication\"},{\"id\":\"org.cloudfoundry.tomcat\",\"optional\":true},{\"id\":\"org.cloudfoundry.springboot\",\"optional\":true},{\"id\":\"org.cloudfoundry.distzip\",\"optional\":true},{\"id\":\"org.cloudfoundry.procfile\",\"optional\":true},{\"id\":\"org.cloudfoundry.azureapplicationinsights\",\"optional\":true},{\"id\":\"org.cloudfoundry.debug\",\"optional\":true},{\"id\":\"org.cloudfoundry.googlestackdriver\",\"optional\":true},{\"id\":\"org.cloudfoundry.jdbc\",\"optional\":true},{\"id\":\"org.cloudfoundry.jmx\",\"optional\":true},{\"id\":\"org.cloudfoundry.springautoreconfiguration\",\"optional\":true}]},{\"group\":[{\"id\":\"org.cloudfoundry.nodejs\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.python\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.go\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.dotnet-core\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.php\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.httpd\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.nginx\"}]}]", + "io.buildpacks.stack.id": "org.cloudfoundry.stacks.cfwindowsfs3" + } + }, + "Architecture": "amd64", + "Os": "linux", + "Size": 1559461360, + "VirtualSize": 1559461360, + "GraphDriver": { + "Data": { + "LowerDir": "/var/lib/docker/overlay2/58e30cd9f3a4da4e0d30f20c3b50de7655e261fb3d32f04818f1bd960c1e8b6c/diff:/var/lib/docker/overlay2/ad95d738069aa405ff17a9ebb1fdc32f8490b0dd885c3ba3a28e2c3b25d64641/diff:/var/lib/docker/overlay2/74d2896cfe9efc6945ff18870a7213583b987ecf4306e189ff6b793f77af5dcd/diff:/var/lib/docker/overlay2/1052615e5c240724e10928048f735cc9e7a7676a9af5f173b895df57c6921a40/diff:/var/lib/docker/overlay2/b5a62216c4282e7568e84427073f096551977c8c6f80d3a04ebb04c25730edde/diff:/var/lib/docker/overlay2/016a36bf7d7d7258eca08da62c01e47bf8e531531f914dde7cae33e191ab2218/diff:/var/lib/docker/overlay2/a585012bf1cf9da0472b2bbe86c4919355593e1a02cf399a9b012928eb816bcd/diff:/var/lib/docker/overlay2/b4aa8b70bd59d7b7dc6d6fb2e655c2334dc8360c764232f83d036d1f241e3298/diff:/var/lib/docker/overlay2/5f4cab16092522163e2dba6587b48d53ee3b09c8778b0736999bc120dd3753b1/diff:/var/lib/docker/overlay2/90e60622603d230f238976f4d9f65797fc9f070df62b1d2ccad0cefe4e205b43/diff:/var/lib/docker/overlay2/c43877934a580e47cc477ed46e71246468d7b6d7151abc5f1a97bb1e8c8104cf/diff:/var/lib/docker/overlay2/8734b165cabb3ff234a08d488f622135aeae9b7347cf41273445ff7d07aa4565/diff:/var/lib/docker/overlay2/2743cd9d4b7da84925b1b530732dad97108fe77e75865de580255579ba2cdb92/diff:/var/lib/docker/overlay2/68308d057b24bbcde7a4880f5db0e653743debdcc0ff3e736d1776296c4168a1/diff:/var/lib/docker/overlay2/7a4411dc4ac1ed7a1da9aabf088985b8b131e0db047e513f9890eb9c001c1895/diff:/var/lib/docker/overlay2/7f7c262fea8dea5ec86507188848ea391354a76468b09ec93523920e18a400ea/diff:/var/lib/docker/overlay2/8b3bfa567fb956204ad866e49489dacd2fdf5fbfa4f9b05ed3668e1106a5383b/diff:/var/lib/docker/overlay2/31bbc4f1616a35b7ce157266e44513963502e30d836a8fd7b7ee18436a8c46cf/diff:/var/lib/docker/overlay2/149b8e9f1142cdf6dcdfe17ea286ec17197f1a329cf23d5c82958a2032facf54/diff:/var/lib/docker/overlay2/92fb1e680083eb8314c5310bf10ced63ec2b0a98afbf84cc5175a98b3d44507a/diff:/var/lib/docker/overlay2/175a35b6f7af6eb91ca500dbd3d7e798f6d174cf8549881ffe5eed8e92a70b9f/diff:/var/lib/docker/overlay2/48ca54bbd27f7df19acf2b6cc719d05dd3b63f8133038a55d216a4498d4dc913/diff:/var/lib/docker/overlay2/ffe3cc3b93c9030f9dcb0e64c258d1e554f1f0cf27a0f8d4e98bb7ece5ffe882/diff:/var/lib/docker/overlay2/1fb2d962bb27e95c40a9a2c1aa910ca847d186d04e3d7dcdf93967101cc30dde/diff:/var/lib/docker/overlay2/10b34138f9e9e8d70c684d0a564452b1309363441b9d7e048f75e0e1179411dc/diff:/var/lib/docker/overlay2/1d888c7e9c62c22ccda6478f03f3df4b43d43fa3b32a2c2fdc9345fdc7193cd9/diff:/var/lib/docker/overlay2/649fc275c002d7336b277365636e1c8e5651bb3ed1557806d26dd6dfa1d9119a/diff:/var/lib/docker/overlay2/4484c2c0ee4a20aa17017c8cd54c842c876fea32afb297e88614d759ec5410dc/diff:/var/lib/docker/overlay2/bd5f374e0ea6749c90535d778f2689c076b7290ad9d3f050af0a40c9626fdea4/diff:/var/lib/docker/overlay2/c6ba97531b15be65bccaf7ebc866d8bc0b88ce838b224aceb196a55824b289a5/diff:/var/lib/docker/overlay2/6c65fab249fe652cd20a6391b2e0786379b6d2c7d4fde02914dfb4fac84035bd/diff:/var/lib/docker/overlay2/f391b54493024e0183331b8ec7835107bc1b84b8a6e77d852f5357724eb940ff/diff:/var/lib/docker/overlay2/8044f9e3ceb529c80531fa2fe52ad550286f788e69843f235e7d756b90c213b8/diff:/var/lib/docker/overlay2/7d3b5539c46c9f0e7c4f6f733f435d1bf6428a8ca81ba71f4da1031cef58aa6c/diff:/var/lib/docker/overlay2/b8080b36b0ddec4e4d738571ddf9d89815f6a95a555d282cfebb73519b4835a0/diff:/var/lib/docker/overlay2/8a737007d5862aa43119254122eb7050c8bd110a3b653c8d6afca23e76fc4042/diff:/var/lib/docker/overlay2/3bb8f3670831e2031be2173381caf02874ad72e664716a990a330bcc3454f4a2/diff:/var/lib/docker/overlay2/cbd675efde19ccac72d3566404e5df8b152a9063c1668d8154711c7db398f852/diff:/var/lib/docker/overlay2/84fb9095136cb645f7f15aeeeba1db6fae3999cb48a559daf8dd46bf3befbeba/diff:/var/lib/docker/overlay2/cbc51912822c4a3fb8624e0cf678e5dedeb76dc2fa0e5bc56f3cbfbfefb26d68/diff:/var/lib/docker/overlay2/d08d5bdcf39aaf46bdf1e0f4576bb64931af646213ff350065b4d306e00f7e28/diff:/var/lib/docker/overlay2/cf180c218fe181bdf836065c5e85103816ea9e8dbb8ab54fb311209c33455eb2/diff:/var/lib/docker/overlay2/b0aef801fd38973eaf116001e05e7c3f8e2eb58ccc7ed37a4bd8d4fcc2ad172b/diff:/var/lib/docker/overlay2/f73c585ae34bd962e1fee2c3e2d95d47b9daf68b23cf469fb13bc3282cf77238/diff:/var/lib/docker/overlay2/c071c8471b26e55a90b6573a21c581dec43b6c7683a3fe87cb33a0734c83342a/diff", + "MergedDir": "/var/lib/docker/overlay2/41ced64ea40f3382f7a475030a5bc89b9c86e2a03d43031c5eba3c5c72616c2b/merged", + "UpperDir": "/var/lib/docker/overlay2/41ced64ea40f3382f7a475030a5bc89b9c86e2a03d43031c5eba3c5c72616c2b/diff", + "WorkDir": "/var/lib/docker/overlay2/41ced64ea40f3382f7a475030a5bc89b9c86e2a03d43031c5eba3c5c72616c2b/work" + }, + "Name": "overlay2" + }, + "RootFS": { + "Type": "layers", + "Layers": [ + "sha256:733a8e5ce32984099ef675fce04730f6e2a6dcfdf5bd292fea01a8f936265342", + "sha256:7755b972f0b4f49de73ef5114fb3ba9c69d80f217e80da99f56f0d0a5dcb3d70", + "sha256:8f0b2d09ab4b38530a1630403967d11a601e56e02e79d3f56370d34fd071fe38", + "sha256:8debe4b6b4290dbbfecea9edea61c22fb455e69e3cbc7d63b17f8e1ab8ea669b", + "sha256:0c6ddab305e5452850f3c09fe15310dff8dc7221702d736dc7705882c1df9658", + "sha256:a9527973bb5d7ccdf88b5be8eb81e024094be1709df659af3127865463c1c188", + "sha256:480cd420e43c6895240c87c88969b87417549c02393cde1b6f71a3a3d5a2a620", + "sha256:391d950d763a33d8ae0373f218aa59907599f51e42cd864129591887e1291034", + "sha256:5b3ec0a6ed9e3de93bb082151f56b1cde5d7e31f2809039a1b5b55a5052fe873", + "sha256:ef935546e2c99da3e8962f2eb3cd6813e9e9a8b19bc8d15b56d1cac37f0342d5", + "sha256:6d644992d62bd09a2bbf490b7fe3aa1e35e6d0d2479583c2decec7092f193310", + "sha256:59d817c36a25078c8ec1f6de0d8336aec598037f89708ed13dbf661557a25084", + "sha256:6636ce01d12372e56a89ec77ea8d9ed510f8c701df1220750add4613764c05a4", + "sha256:96c7f369c29bbf11b971e4dbb6473e8991b666f9de046a414317634eb0a25d2a", + "sha256:3544ba1fa82d1e89619ed04c2485fab3445b1603959d224792d1183dd658033d", + "sha256:12d99406b52b526af152628cd72ba6eacf5d18484dc79cfdacd4b38a21620a2b", + "sha256:3b3cda9eceb0fca56c274e3be93daf53f59501e6b3628fabbaea8ea416eb757a", + "sha256:27ad0fc48c381eb77f69b4e80edccb4d8a2399f5cebd5a8c5a3e1c32313343a6", + "sha256:52845fb94361dad36cc4136e49b92c79ca59c16c579e2f51df0c58ba355c4367", + "sha256:7bf3a57229276fb913155b077d00a18ec6cba92c7f062728ca1c3bc3503c0b55", + "sha256:e5df92d3db931488225ca9f7290de0334225d4bd7c48fc2dcd380d0921bb6680", + "sha256:290ac64fbae3288821551371c8dda38fcf5dfa063a54cb270dcc395a090f5173", + "sha256:996aee90e29ed78d80a5a0c0e50d60a732a18fddae06f87b68bef183beddd2c4", + "sha256:30f6a316d4da01d694d8c17aa84b37f468cccc7184248e255486eb3095ebb87c", + "sha256:c694476a7241ba4e4a0663606d4d6eec7ed8624252c010fbef2713968e8f9436", + "sha256:ab9aaff2160873663388faea6d987cd8f2b5935137b81c64fde145bf2a330d54", + "sha256:e1f3ab860045b96235cbc1b89a3e73add955a303eb42905b570b6012b73b9184", + "sha256:0b260d90d097379d4351132b45110d013b98f4a335795baeb95788fcebcb7f3c", + "sha256:f0f5ecd72b4e0a38d3ad73b5756d8f209955932e9615715502a61dffe56f401a", + "sha256:b4cd790490e41c808e8d65f9ac8f2e58c79bc1a9919a713c4519e77b26dc2053", + "sha256:16b88c0e7f950c32c7496117d1efad90a8557a2badcb267d99a19676b1f0b76a", + "sha256:49d36ba00b17fb605f374ca7877ae129678de925d10fd1955f07c2b6f74dd1c9", + "sha256:b31d189a88ca43fee6077c25bcb623582d569193ed6ac11b4e5623558911e3de", + "sha256:3ecfd2822cf64c609c9c8489e2accfbc0b1de0f2a3637ff1b5d30768fb34b40c", + "sha256:a7f09c3e09b29c5503962a068f29e8726cb91d1dbce2fab688aee0a98189b2be", + "sha256:3d12e651068a0ff19afdd568b5d14ee5292f849542b31d6c9b099a09344e1f4d", + "sha256:f01e41975a9335f5983021b081bc700e46b85efb262670223c4db61eea0a3ebd", + "sha256:2b1b655bb8752f631e786c4c55670315d8569acccfe26402942977c216f2803a", + "sha256:0943c634f5c24311ebdeca6fef5682a4a374c89a831700d188bff7f987470004", + "sha256:9a183e56c86d376b408bdf922746d0a657f62b0e18c7c8f82a496b87710c576f", + "sha256:d919f3c2f534ddbb0b6057f82bca36051ce80a2a9cd3016c320ae276884311f5", + "sha256:108a3eb288f8094aab6ffd822c593902e48e85c8a37b7da2bd21b15f785d92c5", + "sha256:f8b5dcfa1d082af23bb2b2c08526131921329d48d1614d9f2f163a997176087a", + "sha256:ee13e75c33e0af49fbf6c3aaa5bbd102fc468c2d554c4f94763d35a33964dfe4", + "sha256:2571abab1776d4c2e427fba10d61531afff2ab0789f89ef46ce925b6a5d98e0f", + "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" + ] + }, + "Metadata": { + "LastTagTime": "0001-01-01T00:00:00Z" + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/run-image.json b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/run-image.json new file mode 100644 index 0000000000..35ca43b7d0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/build/run-image.json @@ -0,0 +1,142 @@ +{ + "Id": "sha256:9b450bffdb05bcf660d464d0bfdf344ee6ca38e9b8de4f408c8080b0c9319349", + "RepoTags": [ + "cloudfoundry/cnb:latest" + ], + "RepoDigests": [ + "cloudfoundry/run@sha256:715806bb793b66e3fc1a5a8f5584c6a1b6db05425e573887673bddcf426f1b90" + ], + "Parent": "", + "Comment": "", + "Created": "2019-10-30T19:34:56.296666503Z", + "Container": "84597380a7968131ab47dd1b8183a96dcfe9e1e4acff1efe5824dcd762184a67", + "ContainerConfig": { + "Hostname": "84597380a796", + "Domainname": "", + "User": "vcap", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "CNB_USER_ID=2000", + "CNB_GROUP_ID=2000", + "CNB_STACK_ID=org.cloudfoundry.stacks.cflinuxfs3" + ], + "Cmd": [ + "/bin/sh", + "-c", + "#(nop) ", + "LABEL io.buildpacks.stack.id=org.cloudfoundry.stacks.cflinuxfs3" + ], + "Image": "sha256:523c8ade6e06f814469b2cf04c8045a74becee17088955f2657958476d3fba1f", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": null, + "Labels": { + "io.buildpacks.stack.id": "org.cloudfoundry.stacks.cflinuxfs3" + } + }, + "DockerVersion": "18.09.6", + "Author": "", + "Config": { + "Hostname": "", + "Domainname": "", + "User": "vcap", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "CNB_USER_ID=2000", + "CNB_GROUP_ID=2000", + "CNB_STACK_ID=org.cloudfoundry.stacks.cflinuxfs3" + ], + "Cmd": null, + "Image": "sha256:523c8ade6e06f814469b2cf04c8045a74becee17088955f2657958476d3fba1f", + "Volumes": null, + "WorkingDir": "/layers", + "Entrypoint": null, + "OnBuild": null, + "Labels": { + "io.buildpacks.builder.metadata": "{\"description\":\"cflinuxfs3 base image with buildpacks for Java, .NET, NodeJS, Python, Golang, PHP, HTTPD and NGINX\",\"buildpacks\":[{\"id\":\"org.cloudfoundry.googlestackdriver\",\"version\":\"v1.0.40\",\"latest\":true},{\"id\":\"org.cloudfoundry.buildsystem\",\"version\":\"v1.0.114\",\"latest\":true},{\"id\":\"org.cloudfoundry.jdbc\",\"version\":\"v1.0.94\",\"latest\":true},{\"id\":\"org.cloudfoundry.springautoreconfiguration\",\"version\":\"v1.0.100\",\"latest\":true},{\"id\":\"org.cloudfoundry.archiveexpanding\",\"version\":\"v1.0.87\",\"latest\":true},{\"id\":\"org.cloudfoundry.jvmapplication\",\"version\":\"v1.0.72\",\"latest\":true},{\"id\":\"org.cloudfoundry.debug\",\"version\":\"v1.0.92\",\"latest\":true},{\"id\":\"org.cloudfoundry.go\",\"version\":\"v0.0.1\",\"latest\":true},{\"id\":\"org.cloudfoundry.openjdk\",\"version\":\"v1.0.53\",\"latest\":true},{\"id\":\"org.cloudfoundry.procfile\",\"version\":\"v1.0.37\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core\",\"version\":\"v0.0.2\",\"latest\":true},{\"id\":\"org.cloudfoundry.azureapplicationinsights\",\"version\":\"v1.0.94\",\"latest\":true},{\"id\":\"org.cloudfoundry.php\",\"version\":\"v0.0.0-RC1\",\"latest\":true},{\"id\":\"org.cloudfoundry.tomcat\",\"version\":\"v1.1.9\",\"latest\":true},{\"id\":\"org.cloudfoundry.nodejs\",\"version\":\"v0.0.3\",\"latest\":true},{\"id\":\"org.cloudfoundry.jmx\",\"version\":\"v1.0.94\",\"latest\":true},{\"id\":\"org.cloudfoundry.springboot\",\"version\":\"v1.0.97\",\"latest\":true},{\"id\":\"org.cloudfoundry.distzip\",\"version\":\"v1.0.89\",\"latest\":true},{\"id\":\"org.cloudfoundry.python\",\"version\":\"v0.0.1\",\"latest\":true},{\"id\":\"org.cloudfoundry.dep\",\"version\":\"0.0.51\",\"latest\":true},{\"id\":\"org.cloudfoundry.go-compiler\",\"version\":\"0.0.48\",\"latest\":true},{\"id\":\"org.cloudfoundry.go-mod\",\"version\":\"0.0.44\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core-aspnet\",\"version\":\"0.0.53\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core-build\",\"version\":\"0.0.18\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core-conf\",\"version\":\"0.0.57\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core-runtime\",\"version\":\"0.0.66\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core-sdk\",\"version\":\"0.0.55\",\"latest\":true},{\"id\":\"org.cloudfoundry.httpd\",\"version\":\"0.0.21\",\"latest\":true},{\"id\":\"org.cloudfoundry.nginx\",\"version\":\"0.0.25\",\"latest\":true},{\"id\":\"org.cloudfoundry.php-composer\",\"version\":\"0.0.16\",\"latest\":true},{\"id\":\"org.cloudfoundry.php-dist\",\"version\":\"0.0.30\",\"latest\":true},{\"id\":\"org.cloudfoundry.php-web\",\"version\":\"0.0.24\",\"latest\":true},{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.85\",\"latest\":true},{\"id\":\"org.cloudfoundry.npm\",\"version\":\"0.0.53\",\"latest\":true},{\"id\":\"org.cloudfoundry.yarn\",\"version\":\"0.0.58\",\"latest\":true},{\"id\":\"org.cloudfoundry.conda\",\"version\":\"0.0.37\",\"latest\":true},{\"id\":\"org.cloudfoundry.pip\",\"version\":\"0.0.53\",\"latest\":true},{\"id\":\"org.cloudfoundry.pipenv\",\"version\":\"0.0.38\",\"latest\":true},{\"id\":\"org.cloudfoundry.python-runtime\",\"version\":\"0.0.57\",\"latest\":true}],\"groups\":[{\"buildpacks\":[{\"id\":\"org.cloudfoundry.archiveexpanding\",\"version\":\"v1.0.87\",\"optional\":true},{\"id\":\"org.cloudfoundry.openjdk\",\"version\":\"v1.0.53\"},{\"id\":\"org.cloudfoundry.buildsystem\",\"version\":\"v1.0.114\",\"optional\":true},{\"id\":\"org.cloudfoundry.jvmapplication\",\"version\":\"v1.0.72\"},{\"id\":\"org.cloudfoundry.tomcat\",\"version\":\"v1.1.9\",\"optional\":true},{\"id\":\"org.cloudfoundry.springboot\",\"version\":\"v1.0.97\",\"optional\":true},{\"id\":\"org.cloudfoundry.distzip\",\"version\":\"v1.0.89\",\"optional\":true},{\"id\":\"org.cloudfoundry.procfile\",\"version\":\"v1.0.37\",\"optional\":true},{\"id\":\"org.cloudfoundry.azureapplicationinsights\",\"version\":\"v1.0.94\",\"optional\":true},{\"id\":\"org.cloudfoundry.debug\",\"version\":\"v1.0.92\",\"optional\":true},{\"id\":\"org.cloudfoundry.googlestackdriver\",\"version\":\"v1.0.40\",\"optional\":true},{\"id\":\"org.cloudfoundry.jdbc\",\"version\":\"v1.0.94\",\"optional\":true},{\"id\":\"org.cloudfoundry.jmx\",\"version\":\"v1.0.94\",\"optional\":true},{\"id\":\"org.cloudfoundry.springautoreconfiguration\",\"version\":\"v1.0.100\",\"optional\":true}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.nodejs\",\"version\":\"v0.0.3\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.python\",\"version\":\"v0.0.1\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.go\",\"version\":\"v0.0.1\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.dotnet-core\",\"version\":\"v0.0.2\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.php\",\"version\":\"v0.0.0-RC1\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.httpd\",\"version\":\"0.0.21\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.nginx\",\"version\":\"0.0.25\"}]}],\"stack\":{\"runImage\":{\"image\":\"cloudfoundry/run:full-cnb\",\"mirrors\":null}},\"lifecycle\":{\"version\":\"0.5.0\",\"api\":{\"buildpack\":\"0.2\",\"platform\":\"0.1\"}},\"createdBy\":{\"name\":\"Pack CLI\",\"version\":\"v0.5.0 (git sha: c9cfac75b49609524e1ea33f809c12071406547c)\"}}", + "io.buildpacks.buildpack.layers": "{\"org.cloudfoundry.archiveexpanding\":{\"v1.0.87\":{\"layerDiffID\":\"sha256:391d950d763a33d8ae0373f218aa59907599f51e42cd864129591887e1291034\"}},\"org.cloudfoundry.azureapplicationinsights\":{\"v1.0.94\":{\"layerDiffID\":\"sha256:3544ba1fa82d1e89619ed04c2485fab3445b1603959d224792d1183dd658033d\"}},\"org.cloudfoundry.buildsystem\":{\"v1.0.114\":{\"layerDiffID\":\"sha256:0c6ddab305e5452850f3c09fe15310dff8dc7221702d736dc7705882c1df9658\"}},\"org.cloudfoundry.conda\":{\"0.0.37\":{\"layerDiffID\":\"sha256:0943c634f5c24311ebdeca6fef5682a4a374c89a831700d188bff7f987470004\"}},\"org.cloudfoundry.debug\":{\"v1.0.92\":{\"layerDiffID\":\"sha256:ef935546e2c99da3e8962f2eb3cd6813e9e9a8b19bc8d15b56d1cac37f0342d5\"}},\"org.cloudfoundry.dep\":{\"0.0.51\":{\"layerDiffID\":\"sha256:996aee90e29ed78d80a5a0c0e50d60a732a18fddae06f87b68bef183beddd2c4\"}},\"org.cloudfoundry.distzip\":{\"v1.0.89\":{\"layerDiffID\":\"sha256:e5df92d3db931488225ca9f7290de0334225d4bd7c48fc2dcd380d0921bb6680\"}},\"org.cloudfoundry.dotnet-core\":{\"v0.0.2\":{\"layerDiffID\":\"sha256:96c7f369c29bbf11b971e4dbb6473e8991b666f9de046a414317634eb0a25d2a\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.dotnet-core-runtime\",\"version\":\"0.0.66\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-aspnet\",\"version\":\"0.0.53\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-sdk\",\"version\":\"0.0.55\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-build\",\"version\":\"0.0.18\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-conf\",\"version\":\"0.0.57\"}]}]}},\"org.cloudfoundry.dotnet-core-aspnet\":{\"0.0.53\":{\"layerDiffID\":\"sha256:ab9aaff2160873663388faea6d987cd8f2b5935137b81c64fde145bf2a330d54\"}},\"org.cloudfoundry.dotnet-core-build\":{\"0.0.18\":{\"layerDiffID\":\"sha256:e1f3ab860045b96235cbc1b89a3e73add955a303eb42905b570b6012b73b9184\"}},\"org.cloudfoundry.dotnet-core-conf\":{\"0.0.57\":{\"layerDiffID\":\"sha256:0b260d90d097379d4351132b45110d013b98f4a335795baeb95788fcebcb7f3c\"}},\"org.cloudfoundry.dotnet-core-runtime\":{\"0.0.66\":{\"layerDiffID\":\"sha256:f0f5ecd72b4e0a38d3ad73b5756d8f209955932e9615715502a61dffe56f401a\"}},\"org.cloudfoundry.dotnet-core-sdk\":{\"0.0.55\":{\"layerDiffID\":\"sha256:b4cd790490e41c808e8d65f9ac8f2e58c79bc1a9919a713c4519e77b26dc2053\"}},\"org.cloudfoundry.go\":{\"v0.0.1\":{\"layerDiffID\":\"sha256:6d644992d62bd09a2bbf490b7fe3aa1e35e6d0d2479583c2decec7092f193310\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.go-compiler\",\"version\":\"0.0.48\"},{\"id\":\"org.cloudfoundry.go-mod\",\"version\":\"0.0.44\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.go-compiler\",\"version\":\"0.0.48\"},{\"id\":\"org.cloudfoundry.dep\",\"version\":\"0.0.51\"}]}]}},\"org.cloudfoundry.go-compiler\":{\"0.0.48\":{\"layerDiffID\":\"sha256:30f6a316d4da01d694d8c17aa84b37f468cccc7184248e255486eb3095ebb87c\"}},\"org.cloudfoundry.go-mod\":{\"0.0.44\":{\"layerDiffID\":\"sha256:c694476a7241ba4e4a0663606d4d6eec7ed8624252c010fbef2713968e8f9436\"}},\"org.cloudfoundry.googlestackdriver\":{\"v1.0.40\":{\"layerDiffID\":\"sha256:8debe4b6b4290dbbfecea9edea61c22fb455e69e3cbc7d63b17f8e1ab8ea669b\"}},\"org.cloudfoundry.httpd\":{\"0.0.21\":{\"layerDiffID\":\"sha256:16b88c0e7f950c32c7496117d1efad90a8557a2badcb267d99a19676b1f0b76a\"}},\"org.cloudfoundry.jdbc\":{\"v1.0.94\":{\"layerDiffID\":\"sha256:a9527973bb5d7ccdf88b5be8eb81e024094be1709df659af3127865463c1c188\"}},\"org.cloudfoundry.jmx\":{\"v1.0.94\":{\"layerDiffID\":\"sha256:52845fb94361dad36cc4136e49b92c79ca59c16c579e2f51df0c58ba355c4367\"}},\"org.cloudfoundry.jvmapplication\":{\"v1.0.72\":{\"layerDiffID\":\"sha256:5b3ec0a6ed9e3de93bb082151f56b1cde5d7e31f2809039a1b5b55a5052fe873\"}},\"org.cloudfoundry.nginx\":{\"0.0.25\":{\"layerDiffID\":\"sha256:49d36ba00b17fb605f374ca7877ae129678de925d10fd1955f07c2b6f74dd1c9\"}},\"org.cloudfoundry.node-engine\":{\"0.0.85\":{\"layerDiffID\":\"sha256:3d12e651068a0ff19afdd568b5d14ee5292f849542b31d6c9b099a09344e1f4d\"}},\"org.cloudfoundry.nodejs\":{\"v0.0.3\":{\"layerDiffID\":\"sha256:27ad0fc48c381eb77f69b4e80edccb4d8a2399f5cebd5a8c5a3e1c32313343a6\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.85\"},{\"id\":\"org.cloudfoundry.yarn\",\"version\":\"0.0.58\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.85\"},{\"id\":\"org.cloudfoundry.npm\",\"version\":\"0.0.53\"}]}]}},\"org.cloudfoundry.npm\":{\"0.0.53\":{\"layerDiffID\":\"sha256:f01e41975a9335f5983021b081bc700e46b85efb262670223c4db61eea0a3ebd\"}},\"org.cloudfoundry.openjdk\":{\"v1.0.53\":{\"layerDiffID\":\"sha256:59d817c36a25078c8ec1f6de0d8336aec598037f89708ed13dbf661557a25084\"}},\"org.cloudfoundry.php\":{\"v0.0.0-RC1\":{\"layerDiffID\":\"sha256:12d99406b52b526af152628cd72ba6eacf5d18484dc79cfdacd4b38a21620a2b\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.php-dist\",\"version\":\"0.0.30\"},{\"id\":\"org.cloudfoundry.php-composer\",\"version\":\"0.0.16\",\"optional\":true},{\"id\":\"org.cloudfoundry.httpd\",\"version\":\"0.0.21\",\"optional\":true},{\"id\":\"org.cloudfoundry.php-web\",\"version\":\"0.0.24\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.php-dist\",\"version\":\"0.0.30\"},{\"id\":\"org.cloudfoundry.php-composer\",\"version\":\"0.0.16\",\"optional\":true},{\"id\":\"org.cloudfoundry.nginx\",\"version\":\"0.0.25\",\"optional\":true},{\"id\":\"org.cloudfoundry.php-web\",\"version\":\"0.0.24\"}]}]}},\"org.cloudfoundry.php-composer\":{\"0.0.16\":{\"layerDiffID\":\"sha256:b31d189a88ca43fee6077c25bcb623582d569193ed6ac11b4e5623558911e3de\"}},\"org.cloudfoundry.php-dist\":{\"0.0.30\":{\"layerDiffID\":\"sha256:3ecfd2822cf64c609c9c8489e2accfbc0b1de0f2a3637ff1b5d30768fb34b40c\"}},\"org.cloudfoundry.php-web\":{\"0.0.24\":{\"layerDiffID\":\"sha256:a7f09c3e09b29c5503962a068f29e8726cb91d1dbce2fab688aee0a98189b2be\"}},\"org.cloudfoundry.pip\":{\"0.0.53\":{\"layerDiffID\":\"sha256:9a183e56c86d376b408bdf922746d0a657f62b0e18c7c8f82a496b87710c576f\"}},\"org.cloudfoundry.pipenv\":{\"0.0.38\":{\"layerDiffID\":\"sha256:d919f3c2f534ddbb0b6057f82bca36051ce80a2a9cd3016c320ae276884311f5\"}},\"org.cloudfoundry.procfile\":{\"v1.0.37\":{\"layerDiffID\":\"sha256:6636ce01d12372e56a89ec77ea8d9ed510f8c701df1220750add4613764c05a4\"}},\"org.cloudfoundry.python\":{\"v0.0.1\":{\"layerDiffID\":\"sha256:290ac64fbae3288821551371c8dda38fcf5dfa063a54cb270dcc395a090f5173\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.python-runtime\",\"version\":\"0.0.57\"},{\"id\":\"org.cloudfoundry.pipenv\",\"version\":\"0.0.38\",\"optional\":true},{\"id\":\"org.cloudfoundry.pip\",\"version\":\"0.0.53\",\"optional\":true}]},{\"group\":[{\"id\":\"org.cloudfoundry.conda\",\"version\":\"0.0.37\"}]}]}},\"org.cloudfoundry.python-runtime\":{\"0.0.57\":{\"layerDiffID\":\"sha256:108a3eb288f8094aab6ffd822c593902e48e85c8a37b7da2bd21b15f785d92c5\"}},\"org.cloudfoundry.springautoreconfiguration\":{\"v1.0.100\":{\"layerDiffID\":\"sha256:480cd420e43c6895240c87c88969b87417549c02393cde1b6f71a3a3d5a2a620\"}},\"org.cloudfoundry.springboot\":{\"v1.0.97\":{\"layerDiffID\":\"sha256:7bf3a57229276fb913155b077d00a18ec6cba92c7f062728ca1c3bc3503c0b55\"}},\"org.cloudfoundry.tomcat\":{\"v1.1.9\":{\"layerDiffID\":\"sha256:3b3cda9eceb0fca56c274e3be93daf53f59501e6b3628fabbaea8ea416eb757a\"}},\"org.cloudfoundry.yarn\":{\"0.0.58\":{\"layerDiffID\":\"sha256:2b1b655bb8752f631e786c4c55670315d8569acccfe26402942977c216f2803a\"}}}", + "io.buildpacks.buildpack.order": "[{\"group\":[{\"id\":\"org.cloudfoundry.archiveexpanding\",\"optional\":true},{\"id\":\"org.cloudfoundry.openjdk\"},{\"id\":\"org.cloudfoundry.buildsystem\",\"optional\":true},{\"id\":\"org.cloudfoundry.jvmapplication\"},{\"id\":\"org.cloudfoundry.tomcat\",\"optional\":true},{\"id\":\"org.cloudfoundry.springboot\",\"optional\":true},{\"id\":\"org.cloudfoundry.distzip\",\"optional\":true},{\"id\":\"org.cloudfoundry.procfile\",\"optional\":true},{\"id\":\"org.cloudfoundry.azureapplicationinsights\",\"optional\":true},{\"id\":\"org.cloudfoundry.debug\",\"optional\":true},{\"id\":\"org.cloudfoundry.googlestackdriver\",\"optional\":true},{\"id\":\"org.cloudfoundry.jdbc\",\"optional\":true},{\"id\":\"org.cloudfoundry.jmx\",\"optional\":true},{\"id\":\"org.cloudfoundry.springautoreconfiguration\",\"optional\":true}]},{\"group\":[{\"id\":\"org.cloudfoundry.nodejs\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.python\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.go\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.dotnet-core\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.php\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.httpd\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.nginx\"}]}]", + "io.buildpacks.stack.id": "org.cloudfoundry.stacks.cflinuxfs3" + } + }, + "Architecture": "amd64", + "Os": "linux", + "Size": 1559461360, + "VirtualSize": 1559461360, + "GraphDriver": { + "Data": { + "LowerDir": "/var/lib/docker/overlay2/58e30cd9f3a4da4e0d30f20c3b50de7655e261fb3d32f04818f1bd960c1e8b6c/diff:/var/lib/docker/overlay2/ad95d738069aa405ff17a9ebb1fdc32f8490b0dd885c3ba3a28e2c3b25d64641/diff:/var/lib/docker/overlay2/74d2896cfe9efc6945ff18870a7213583b987ecf4306e189ff6b793f77af5dcd/diff:/var/lib/docker/overlay2/1052615e5c240724e10928048f735cc9e7a7676a9af5f173b895df57c6921a40/diff:/var/lib/docker/overlay2/b5a62216c4282e7568e84427073f096551977c8c6f80d3a04ebb04c25730edde/diff:/var/lib/docker/overlay2/016a36bf7d7d7258eca08da62c01e47bf8e531531f914dde7cae33e191ab2218/diff:/var/lib/docker/overlay2/a585012bf1cf9da0472b2bbe86c4919355593e1a02cf399a9b012928eb816bcd/diff:/var/lib/docker/overlay2/b4aa8b70bd59d7b7dc6d6fb2e655c2334dc8360c764232f83d036d1f241e3298/diff:/var/lib/docker/overlay2/5f4cab16092522163e2dba6587b48d53ee3b09c8778b0736999bc120dd3753b1/diff:/var/lib/docker/overlay2/90e60622603d230f238976f4d9f65797fc9f070df62b1d2ccad0cefe4e205b43/diff:/var/lib/docker/overlay2/c43877934a580e47cc477ed46e71246468d7b6d7151abc5f1a97bb1e8c8104cf/diff:/var/lib/docker/overlay2/8734b165cabb3ff234a08d488f622135aeae9b7347cf41273445ff7d07aa4565/diff:/var/lib/docker/overlay2/2743cd9d4b7da84925b1b530732dad97108fe77e75865de580255579ba2cdb92/diff:/var/lib/docker/overlay2/68308d057b24bbcde7a4880f5db0e653743debdcc0ff3e736d1776296c4168a1/diff:/var/lib/docker/overlay2/7a4411dc4ac1ed7a1da9aabf088985b8b131e0db047e513f9890eb9c001c1895/diff:/var/lib/docker/overlay2/7f7c262fea8dea5ec86507188848ea391354a76468b09ec93523920e18a400ea/diff:/var/lib/docker/overlay2/8b3bfa567fb956204ad866e49489dacd2fdf5fbfa4f9b05ed3668e1106a5383b/diff:/var/lib/docker/overlay2/31bbc4f1616a35b7ce157266e44513963502e30d836a8fd7b7ee18436a8c46cf/diff:/var/lib/docker/overlay2/149b8e9f1142cdf6dcdfe17ea286ec17197f1a329cf23d5c82958a2032facf54/diff:/var/lib/docker/overlay2/92fb1e680083eb8314c5310bf10ced63ec2b0a98afbf84cc5175a98b3d44507a/diff:/var/lib/docker/overlay2/175a35b6f7af6eb91ca500dbd3d7e798f6d174cf8549881ffe5eed8e92a70b9f/diff:/var/lib/docker/overlay2/48ca54bbd27f7df19acf2b6cc719d05dd3b63f8133038a55d216a4498d4dc913/diff:/var/lib/docker/overlay2/ffe3cc3b93c9030f9dcb0e64c258d1e554f1f0cf27a0f8d4e98bb7ece5ffe882/diff:/var/lib/docker/overlay2/1fb2d962bb27e95c40a9a2c1aa910ca847d186d04e3d7dcdf93967101cc30dde/diff:/var/lib/docker/overlay2/10b34138f9e9e8d70c684d0a564452b1309363441b9d7e048f75e0e1179411dc/diff:/var/lib/docker/overlay2/1d888c7e9c62c22ccda6478f03f3df4b43d43fa3b32a2c2fdc9345fdc7193cd9/diff:/var/lib/docker/overlay2/649fc275c002d7336b277365636e1c8e5651bb3ed1557806d26dd6dfa1d9119a/diff:/var/lib/docker/overlay2/4484c2c0ee4a20aa17017c8cd54c842c876fea32afb297e88614d759ec5410dc/diff:/var/lib/docker/overlay2/bd5f374e0ea6749c90535d778f2689c076b7290ad9d3f050af0a40c9626fdea4/diff:/var/lib/docker/overlay2/c6ba97531b15be65bccaf7ebc866d8bc0b88ce838b224aceb196a55824b289a5/diff:/var/lib/docker/overlay2/6c65fab249fe652cd20a6391b2e0786379b6d2c7d4fde02914dfb4fac84035bd/diff:/var/lib/docker/overlay2/f391b54493024e0183331b8ec7835107bc1b84b8a6e77d852f5357724eb940ff/diff:/var/lib/docker/overlay2/8044f9e3ceb529c80531fa2fe52ad550286f788e69843f235e7d756b90c213b8/diff:/var/lib/docker/overlay2/7d3b5539c46c9f0e7c4f6f733f435d1bf6428a8ca81ba71f4da1031cef58aa6c/diff:/var/lib/docker/overlay2/b8080b36b0ddec4e4d738571ddf9d89815f6a95a555d282cfebb73519b4835a0/diff:/var/lib/docker/overlay2/8a737007d5862aa43119254122eb7050c8bd110a3b653c8d6afca23e76fc4042/diff:/var/lib/docker/overlay2/3bb8f3670831e2031be2173381caf02874ad72e664716a990a330bcc3454f4a2/diff:/var/lib/docker/overlay2/cbd675efde19ccac72d3566404e5df8b152a9063c1668d8154711c7db398f852/diff:/var/lib/docker/overlay2/84fb9095136cb645f7f15aeeeba1db6fae3999cb48a559daf8dd46bf3befbeba/diff:/var/lib/docker/overlay2/cbc51912822c4a3fb8624e0cf678e5dedeb76dc2fa0e5bc56f3cbfbfefb26d68/diff:/var/lib/docker/overlay2/d08d5bdcf39aaf46bdf1e0f4576bb64931af646213ff350065b4d306e00f7e28/diff:/var/lib/docker/overlay2/cf180c218fe181bdf836065c5e85103816ea9e8dbb8ab54fb311209c33455eb2/diff:/var/lib/docker/overlay2/b0aef801fd38973eaf116001e05e7c3f8e2eb58ccc7ed37a4bd8d4fcc2ad172b/diff:/var/lib/docker/overlay2/f73c585ae34bd962e1fee2c3e2d95d47b9daf68b23cf469fb13bc3282cf77238/diff:/var/lib/docker/overlay2/c071c8471b26e55a90b6573a21c581dec43b6c7683a3fe87cb33a0734c83342a/diff", + "MergedDir": "/var/lib/docker/overlay2/41ced64ea40f3382f7a475030a5bc89b9c86e2a03d43031c5eba3c5c72616c2b/merged", + "UpperDir": "/var/lib/docker/overlay2/41ced64ea40f3382f7a475030a5bc89b9c86e2a03d43031c5eba3c5c72616c2b/diff", + "WorkDir": "/var/lib/docker/overlay2/41ced64ea40f3382f7a475030a5bc89b9c86e2a03d43031c5eba3c5c72616c2b/work" + }, + "Name": "overlay2" + }, + "RootFS": { + "Type": "layers", + "Layers": [ + "sha256:733a8e5ce32984099ef675fce04730f6e2a6dcfdf5bd292fea01a8f936265342", + "sha256:7755b972f0b4f49de73ef5114fb3ba9c69d80f217e80da99f56f0d0a5dcb3d70", + "sha256:8f0b2d09ab4b38530a1630403967d11a601e56e02e79d3f56370d34fd071fe38", + "sha256:8debe4b6b4290dbbfecea9edea61c22fb455e69e3cbc7d63b17f8e1ab8ea669b", + "sha256:0c6ddab305e5452850f3c09fe15310dff8dc7221702d736dc7705882c1df9658", + "sha256:a9527973bb5d7ccdf88b5be8eb81e024094be1709df659af3127865463c1c188", + "sha256:480cd420e43c6895240c87c88969b87417549c02393cde1b6f71a3a3d5a2a620", + "sha256:391d950d763a33d8ae0373f218aa59907599f51e42cd864129591887e1291034", + "sha256:5b3ec0a6ed9e3de93bb082151f56b1cde5d7e31f2809039a1b5b55a5052fe873", + "sha256:ef935546e2c99da3e8962f2eb3cd6813e9e9a8b19bc8d15b56d1cac37f0342d5", + "sha256:6d644992d62bd09a2bbf490b7fe3aa1e35e6d0d2479583c2decec7092f193310", + "sha256:59d817c36a25078c8ec1f6de0d8336aec598037f89708ed13dbf661557a25084", + "sha256:6636ce01d12372e56a89ec77ea8d9ed510f8c701df1220750add4613764c05a4", + "sha256:96c7f369c29bbf11b971e4dbb6473e8991b666f9de046a414317634eb0a25d2a", + "sha256:3544ba1fa82d1e89619ed04c2485fab3445b1603959d224792d1183dd658033d", + "sha256:12d99406b52b526af152628cd72ba6eacf5d18484dc79cfdacd4b38a21620a2b", + "sha256:3b3cda9eceb0fca56c274e3be93daf53f59501e6b3628fabbaea8ea416eb757a", + "sha256:27ad0fc48c381eb77f69b4e80edccb4d8a2399f5cebd5a8c5a3e1c32313343a6", + "sha256:52845fb94361dad36cc4136e49b92c79ca59c16c579e2f51df0c58ba355c4367", + "sha256:7bf3a57229276fb913155b077d00a18ec6cba92c7f062728ca1c3bc3503c0b55", + "sha256:e5df92d3db931488225ca9f7290de0334225d4bd7c48fc2dcd380d0921bb6680", + "sha256:290ac64fbae3288821551371c8dda38fcf5dfa063a54cb270dcc395a090f5173", + "sha256:996aee90e29ed78d80a5a0c0e50d60a732a18fddae06f87b68bef183beddd2c4", + "sha256:30f6a316d4da01d694d8c17aa84b37f468cccc7184248e255486eb3095ebb87c", + "sha256:c694476a7241ba4e4a0663606d4d6eec7ed8624252c010fbef2713968e8f9436", + "sha256:ab9aaff2160873663388faea6d987cd8f2b5935137b81c64fde145bf2a330d54", + "sha256:e1f3ab860045b96235cbc1b89a3e73add955a303eb42905b570b6012b73b9184", + "sha256:0b260d90d097379d4351132b45110d013b98f4a335795baeb95788fcebcb7f3c", + "sha256:f0f5ecd72b4e0a38d3ad73b5756d8f209955932e9615715502a61dffe56f401a", + "sha256:b4cd790490e41c808e8d65f9ac8f2e58c79bc1a9919a713c4519e77b26dc2053", + "sha256:16b88c0e7f950c32c7496117d1efad90a8557a2badcb267d99a19676b1f0b76a", + "sha256:49d36ba00b17fb605f374ca7877ae129678de925d10fd1955f07c2b6f74dd1c9", + "sha256:b31d189a88ca43fee6077c25bcb623582d569193ed6ac11b4e5623558911e3de", + "sha256:3ecfd2822cf64c609c9c8489e2accfbc0b1de0f2a3637ff1b5d30768fb34b40c", + "sha256:a7f09c3e09b29c5503962a068f29e8726cb91d1dbce2fab688aee0a98189b2be", + "sha256:3d12e651068a0ff19afdd568b5d14ee5292f849542b31d6c9b099a09344e1f4d", + "sha256:f01e41975a9335f5983021b081bc700e46b85efb262670223c4db61eea0a3ebd", + "sha256:2b1b655bb8752f631e786c4c55670315d8569acccfe26402942977c216f2803a", + "sha256:0943c634f5c24311ebdeca6fef5682a4a374c89a831700d188bff7f987470004", + "sha256:9a183e56c86d376b408bdf922746d0a657f62b0e18c7c8f82a496b87710c576f", + "sha256:d919f3c2f534ddbb0b6057f82bca36051ce80a2a9cd3016c320ae276884311f5", + "sha256:108a3eb288f8094aab6ffd822c593902e48e85c8a37b7da2bd21b15f785d92c5", + "sha256:f8b5dcfa1d082af23bb2b2c08526131921329d48d1614d9f2f163a997176087a", + "sha256:ee13e75c33e0af49fbf6c3aaa5bbd102fc468c2d554c4f94763d35a33964dfe4", + "sha256:2571abab1776d4c2e427fba10d61531afff2ab0789f89ef46ce925b6a5d98e0f", + "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" + ] + }, + "Metadata": { + "LastTagTime": "0001-01-01T00:00:00Z" + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/docker/create-container-response.json b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/docker/create-container-response.json new file mode 100644 index 0000000000..2726ac000f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/docker/create-container-response.json @@ -0,0 +1,4 @@ + { + "Id": "e90e34656806", + "Warnings": [] +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/docker/errors.json b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/docker/errors.json new file mode 100644 index 0000000000..f8b04fefcc --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/docker/errors.json @@ -0,0 +1,14 @@ +{ + "errors": [ + { + "code": "TEST1", + "message": "Test One", + "detail": 123 + }, + { + "code": "TEST2", + "message": "Test Two", + "detail": "fail" + } + ] +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/docker/load-stream.json b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/docker/load-stream.json new file mode 100644 index 0000000000..282e9ef7c7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/docker/load-stream.json @@ -0,0 +1 @@ +{"stream":"Loaded image: pack.local/builder/auqfjjbaod:latest\n"} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/docker/log-update-event-ansi.stream b/spring-boot-project/spring-boot-tools/spring-boot-cloudnativebuildpack/src/test/resources/org/springframework/boot/cloudnativebuildpack/docker/log-update-event-ansi.stream new file mode 100644 index 0000000000000000000000000000000000000000..baec6ab8e9312187b6df5a0580483ea7ab866188 GIT binary patch literal 1367 zcmcIk%W{G+6s_Ctx^{CHZpjE*XSD4iK020SwPM}4=mdz00$~F9`1|q@O2&mo1Icol97$-`rDQmH|S0mb#>>&+2)0xFUI??CU0E=uXPeC$8?SqQsaSyQT zTBc5XlQ~C~)ysKh^_{ZLhU$S&G>d|ahksCY2Qc7Fr=AD?)}>w}l{<%Ug-UBS&9H?9 z;R*JFtOy9Y3#FEVRds+zT`0+Tj6eH>kqV5@kmvGn98*qgx@LTdG{$^nF=9}TWWp5^ zIN^}8@qF|7#Eb1{L<)B6ntW@jB_EZNh4u1$nf>^n{c1cdXof>NYP~2`^4=>u@AF3` zBqFhlnjbGpTTtKdK<)NNmyJW>s}R|FRu)W#wDd3Y5Zw^lwJ7qEqQ)hc`~CRs#o-rk w{>!%)hpX^qad!BkX_+3NB^HUK#6)n+kN^rHlBU3fgW6^}q>AzaDip{|iq@-{zIk-oPw+qK z`tyxDM_=T_egYTai0w{7-SYtB*lPr@t*PmiFnDNsiaT%%d3#4MKjl0X?{DhWIYIZM rJ8soj>+89Vr+!IzeP+U6IQ748nN + +