Repository: spring-cloud/spring-cloud-deployer Branch: main Commit: 63e5946b4adb Files: 332 Total size: 136.8 MB Directory structure: gitextract_rlk86ief/ ├── .editorconfig ├── .github/ │ ├── actions/ │ │ └── trivy-scan/ │ │ └── action.yml │ ├── dco.yml │ ├── settings.xml │ └── workflows/ │ ├── build-snapshot-worker.yml │ ├── ci-it.yml │ ├── ci-pr.yml │ ├── ci.yml │ ├── common-ci.yml │ ├── issue-handler.yml │ ├── k8s-versions.json │ ├── milestone-worker.yml │ ├── next-dev-version-worker.yml │ └── release-worker.yml ├── .gitignore ├── .mvn/ │ ├── jvm.config │ ├── maven.config │ └── wrapper/ │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── .settings.xml ├── .trivyignore ├── CONTRIBUTING.adoc ├── LICENSE ├── NOTICE ├── README.adoc ├── SECURITY.md ├── mvnw ├── mvnw.cmd ├── pom.xml ├── spring-cloud-deployer-autoconfigure/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── springframework/ │ │ │ └── cloud/ │ │ │ └── deployer/ │ │ │ └── autoconfigure/ │ │ │ ├── DelegatingResourceLoaderBuilder.java │ │ │ ├── DelegatingResourceLoaderBuilderCustomizer.java │ │ │ ├── MavenConfigurationProperties.java │ │ │ └── ResourceLoadingAutoConfiguration.java │ │ └── resources/ │ │ └── META-INF/ │ │ ├── spring/ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ └── spring.factories │ └── test/ │ └── java/ │ └── org/ │ └── springframework/ │ └── cloud/ │ └── deployer/ │ └── autoconfigure/ │ └── ResourceLoadingAutoConfigurationTests.java ├── spring-cloud-deployer-cloudfoundry/ │ ├── README.adoc │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── springframework/ │ │ │ └── cloud/ │ │ │ └── deployer/ │ │ │ └── spi/ │ │ │ ├── cloudfoundry/ │ │ │ │ ├── AbstractCloudFoundryDeployer.java │ │ │ │ ├── AbstractCloudFoundryTaskLauncher.java │ │ │ │ ├── AppNameGenerator.java │ │ │ │ ├── ApplicationLogAccessor.java │ │ │ │ ├── CfEnvAwareAppDeploymentRequest.java │ │ │ │ ├── CfEnvAwareResource.java │ │ │ │ ├── CfEnvConfigurer.java │ │ │ │ ├── CloudFoundryActuatorTemplate.java │ │ │ │ ├── CloudFoundryAppDeployer.java │ │ │ │ ├── CloudFoundryAppInstanceStatus.java │ │ │ │ ├── CloudFoundryAppNameGenerator.java │ │ │ │ ├── CloudFoundryConnectionProperties.java │ │ │ │ ├── CloudFoundryDeployerAutoConfiguration.java │ │ │ │ ├── CloudFoundryDeploymentProperties.java │ │ │ │ ├── CloudFoundryPlatformSpecificInfo.java │ │ │ │ ├── CloudFoundryTaskLauncher.java │ │ │ │ ├── DurationConverter.java │ │ │ │ ├── ServiceParser.java │ │ │ │ └── UnsupportedVersionTaskLauncher.java │ │ │ └── scheduler/ │ │ │ └── cloudfoundry/ │ │ │ ├── CloudFoundryAppScheduler.java │ │ │ ├── CloudFoundryScheduleSSLException.java │ │ │ ├── CloudFoundrySchedulerProperties.java │ │ │ └── expression/ │ │ │ └── QuartzCronExpression.java │ │ └── resources/ │ │ └── META-INF/ │ │ ├── additional-spring-configuration-metadata.json │ │ ├── spring/ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ └── spring.factories │ └── test/ │ ├── java/ │ │ └── org/ │ │ └── springframework/ │ │ └── cloud/ │ │ └── deployer/ │ │ └── spi/ │ │ ├── cloudfoundry/ │ │ │ ├── AbstractAppDeployerTestSupport.java │ │ │ ├── ApplicationLogAccessorTests.java │ │ │ ├── CfEnvAwareResourceTests.java │ │ │ ├── CfEnvConfigurerTests.java │ │ │ ├── CloudFoundryActuatorTemplateTests.java │ │ │ ├── CloudFoundryAppDeployerIntegrationIT.java │ │ │ ├── CloudFoundryAppDeployerTests.java │ │ │ ├── CloudFoundryAppNameGeneratorTest.java │ │ │ ├── CloudFoundryConnectionPropertiesTests.java │ │ │ ├── CloudFoundryDeployerTests.java │ │ │ ├── CloudFoundryTaskLauncherCachingTests.java │ │ │ ├── CloudFoundryTaskLauncherIntegrationIT.java │ │ │ ├── CloudFoundryTaskLauncherTests.java │ │ │ └── ServiceParserTests.java │ │ └── scheduler/ │ │ └── cloudfoundry/ │ │ ├── CloudFoundryAppSchedulerTests.java │ │ ├── CloudFoundryScheduleSSLExceptionTests.java │ │ ├── CloudFoundrySchedulerPropertiesTest.java │ │ ├── SpringCloudSchedulerIntegrationIT.java │ │ └── expression/ │ │ └── QuartzCronExpressionTests.java │ └── resources/ │ ├── batch-job-1.0.0.BUILD-SNAPSHOT.jar │ ├── demo-0.0.1-SNAPSHOT.jar │ ├── http-source-rabbit-2.1.5.RELEASE.jar │ ├── log-sink-rabbit-3.0.0.BUILD-SNAPSHOT.jar │ ├── logback-test.xml │ ├── long-running-task-1.0.0.BUILD-SNAPSHOT.jar │ ├── timestamp-task-1.0.0.BUILD-SNAPSHOT-exec-2.jar │ ├── timestamp-task-1.0.0.BUILD-SNAPSHOT-exec-3.jar │ ├── timestamp-task-1.0.0.BUILD-SNAPSHOT-exec.jar │ └── timestamp-task-3.1.2-SNAPSHOT.jar ├── spring-cloud-deployer-dependencies/ │ └── pom.xml ├── spring-cloud-deployer-kubernetes/ │ ├── README.md │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── springframework/ │ │ │ └── cloud/ │ │ │ └── deployer/ │ │ │ └── spi/ │ │ │ └── kubernetes/ │ │ │ ├── AbstractKubernetesDeployer.java │ │ │ ├── CommandProbeCreator.java │ │ │ ├── CompositeDeploymentStateResolver.java │ │ │ ├── ContainerConfiguration.java │ │ │ ├── ContainerFactory.java │ │ │ ├── DefaultContainerFactory.java │ │ │ ├── DefaultRunningPhaseDeploymentStateResolver.java │ │ │ ├── DeploymentPropertiesResolver.java │ │ │ ├── EntryPointStyle.java │ │ │ ├── HttpProbeCreator.java │ │ │ ├── ImagePullPolicy.java │ │ │ ├── KubernetesActuatorTemplate.java │ │ │ ├── KubernetesAppDeployer.java │ │ │ ├── KubernetesAppInstanceStatus.java │ │ │ ├── KubernetesAutoConfiguration.java │ │ │ ├── KubernetesClientFactory.java │ │ │ ├── KubernetesDeployerProperties.java │ │ │ ├── KubernetesScheduler.java │ │ │ ├── KubernetesSchedulerProperties.java │ │ │ ├── KubernetesTaskLauncher.java │ │ │ ├── KubernetesTaskLauncherProperties.java │ │ │ ├── LivenessCommandProbeCreator.java │ │ │ ├── LivenessHttpProbeCreator.java │ │ │ ├── LivenessTcpProbeCreator.java │ │ │ ├── PredicateRunningPhaseDeploymentStateResolver.java │ │ │ ├── ProbeAuthenticationType.java │ │ │ ├── ProbeCreator.java │ │ │ ├── ProbeCreatorFactory.java │ │ │ ├── ProbeType.java │ │ │ ├── ReadinessCommandProbeCreator.java │ │ │ ├── ReadinessHttpProbeCreator.java │ │ │ ├── ReadinessTcpProbeCreator.java │ │ │ ├── RestartPolicy.java │ │ │ ├── RunningPhaseDeploymentStateResolver.java │ │ │ ├── StartupCommandProbeCreator.java │ │ │ ├── StartupHttpProbeCreator.java │ │ │ ├── StartupTcpProbeCreator.java │ │ │ ├── TcpProbeCreator.java │ │ │ └── support/ │ │ │ ├── ArgumentSanitizer.java │ │ │ ├── PropertyParserUtils.java │ │ │ └── RelaxedNames.java │ │ └── resources/ │ │ └── META-INF/ │ │ ├── spring/ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ └── spring.factories │ └── test/ │ ├── java/ │ │ └── org/ │ │ └── springframework/ │ │ └── cloud/ │ │ └── deployer/ │ │ └── spi/ │ │ └── kubernetes/ │ │ ├── AbstractKubernetesTaskLauncherIntegrationTests.java │ │ ├── DefaultContainerFactoryTests.java │ │ ├── DeploymentPropertiesResolverTests.java │ │ ├── EntryPointStyleTests.java │ │ ├── ImagePullPolicyTests.java │ │ ├── KubernetesActuatorTemplateTests.java │ │ ├── KubernetesAppDeployerIntegrationIT.java │ │ ├── KubernetesAppDeployerTests.java │ │ ├── KubernetesConfigurationPropertiesTests.java │ │ ├── KubernetesDeployerPropertiesTests.java │ │ ├── KubernetesSchedulerIT.java │ │ ├── KubernetesSchedulerPropertiesTests.java │ │ ├── KubernetesTaskLauncherIntegrationIT.java │ │ ├── KubernetesTaskLauncherMaximumConcurrentTasksTests.java │ │ ├── KubernetesTaskLauncherWithJobIntegrationIT.java │ │ ├── PropertyParserUtilsTests.java │ │ └── RunAbstractKubernetesDeployerTests.java │ └── resources/ │ ├── dataflow-server-configMapKeyRef.yml │ ├── dataflow-server-containerSecurityContext.yml │ ├── dataflow-server-nodeAffinity.yml │ ├── dataflow-server-podAffinity.yml │ ├── dataflow-server-podAntiAffinity.yml │ ├── dataflow-server-podsecuritycontext.yml │ ├── dataflow-server-secretKeyRef.yml │ ├── dataflow-server-tolerations.yml │ └── dataflow-server.yml ├── spring-cloud-deployer-local/ │ ├── README.adoc │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── springframework/ │ │ │ └── cloud/ │ │ │ └── deployer/ │ │ │ └── spi/ │ │ │ └── local/ │ │ │ ├── AbstractLocalDeployerSupport.java │ │ │ ├── CommandBuilder.java │ │ │ ├── DebugAddress.java │ │ │ ├── DeployerSocketUtils.java │ │ │ ├── DockerCommandBuilder.java │ │ │ ├── HttpProbeExecutor.java │ │ │ ├── JavaCommandBuilder.java │ │ │ ├── LocalActuatorTemplate.java │ │ │ ├── LocalAppDeployer.java │ │ │ ├── LocalDeployerAutoConfiguration.java │ │ │ ├── LocalDeployerProperties.java │ │ │ ├── LocalDeployerUtils.java │ │ │ └── LocalTaskLauncher.java │ │ └── resources/ │ │ └── META-INF/ │ │ └── spring/ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ └── test/ │ ├── java/ │ │ └── org/ │ │ └── springframework/ │ │ └── cloud/ │ │ └── deployer/ │ │ └── spi/ │ │ └── local/ │ │ ├── DebugAddressTests.java │ │ ├── DeployerSocketUtilsTests.java │ │ ├── DockerCommandBuilderTests.java │ │ ├── JavaExecutionCommandBuilderTests.java │ │ ├── LocalAppDeployerEnvironmentIntegrationTests.java │ │ ├── LocalAppDeployerIntegrationTests.java │ │ ├── LocalDeployerPropertiesTests.java │ │ ├── LocalDeployerSupportTests.java │ │ ├── LocalTaskLauncherIntegrationTests.java │ │ ├── RandomPortRangeContextTests.java │ │ ├── RandomPortRangeTests.java │ │ └── RandomPortTests.java │ └── resources/ │ └── testResource.txt ├── spring-cloud-deployer-resource-docker/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── org/ │ │ └── springframework/ │ │ └── cloud/ │ │ └── deployer/ │ │ └── resource/ │ │ └── docker/ │ │ ├── DockerResource.java │ │ └── DockerResourceLoader.java │ └── test/ │ └── java/ │ └── org/ │ └── springframework/ │ └── cloud/ │ └── deployer/ │ └── resource/ │ └── docker/ │ ├── DockerResourceLoaderTests.java │ └── DockerResourceTests.java ├── spring-cloud-deployer-resource-maven/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── org/ │ │ └── springframework/ │ │ └── cloud/ │ │ └── deployer/ │ │ └── resource/ │ │ └── maven/ │ │ ├── LoggingRepositoryListener.java │ │ ├── MavenArtifactResolver.java │ │ ├── MavenProperties.java │ │ ├── MavenResource.java │ │ ├── MavenResourceLoader.java │ │ ├── StaticWagonConfigurator.java │ │ └── StaticWagonProvider.java │ └── test/ │ ├── java/ │ │ └── org/ │ │ └── springframework/ │ │ └── cloud/ │ │ └── deployer/ │ │ └── resource/ │ │ └── maven/ │ │ ├── MavenArtifactResolverTests.java │ │ ├── MavenExtension.java │ │ ├── MavenPropertiesTests.java │ │ ├── MavenResourceLoaderTests.java │ │ ├── MavenResourceTests.java │ │ └── WagonHttpTests.java │ └── resources/ │ └── application.properties ├── spring-cloud-deployer-resource-support/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── org/ │ │ └── springframework/ │ │ └── cloud/ │ │ └── deployer/ │ │ └── resource/ │ │ ├── registry/ │ │ │ ├── InMemoryUriRegistry.java │ │ │ ├── UriRegistry.java │ │ │ └── UriRegistryPopulator.java │ │ └── support/ │ │ ├── DelegatingResourceLoader.java │ │ ├── DownloadingUrlResource.java │ │ ├── DownloadingUrlResourceLoader.java │ │ ├── ResourceNotResolvedException.java │ │ └── ShaUtils.java │ └── test/ │ └── java/ │ └── org/ │ └── springframework/ │ └── cloud/ │ └── deployer/ │ └── resource/ │ ├── StubResourceLoader.java │ ├── registry/ │ │ └── UriRegistryPopulatorTests.java │ └── support/ │ ├── DelegatingResourceLoaderIntegrationTests.java │ ├── DelegatingResourceLoaderTests.java │ ├── DownloadingUrlResourceTests.java │ └── ShaUtilsTests.java ├── spring-cloud-deployer-spi/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── org/ │ │ └── springframework/ │ │ └── cloud/ │ │ └── deployer/ │ │ └── spi/ │ │ ├── app/ │ │ │ ├── AbstractActuatorTemplate.java │ │ │ ├── ActuatorOperations.java │ │ │ ├── AppAdmin.java │ │ │ ├── AppDeployer.java │ │ │ ├── AppInstanceStatus.java │ │ │ ├── AppScaleRequest.java │ │ │ ├── AppStatus.java │ │ │ ├── DeploymentState.java │ │ │ └── MultiStateAppDeployer.java │ │ ├── core/ │ │ │ ├── AppDefinition.java │ │ │ ├── AppDeploymentRequest.java │ │ │ └── RuntimeEnvironmentInfo.java │ │ ├── scheduler/ │ │ │ ├── CreateScheduleException.java │ │ │ ├── ScheduleInfo.java │ │ │ ├── ScheduleRequest.java │ │ │ ├── Scheduler.java │ │ │ ├── SchedulerException.java │ │ │ ├── SchedulerPropertyKeys.java │ │ │ └── UnScheduleException.java │ │ ├── task/ │ │ │ ├── LaunchState.java │ │ │ ├── TaskLauncher.java │ │ │ └── TaskStatus.java │ │ └── util/ │ │ ├── ByteSizeUtils.java │ │ ├── CommandLineTokenizer.java │ │ └── RuntimeVersionUtils.java │ └── test/ │ └── java/ │ └── org/ │ └── springframework/ │ └── cloud/ │ └── deployer/ │ └── spi/ │ ├── app/ │ │ ├── AppDeployerTests.java │ │ └── RuntimeEnvironmentInfoBuilderTests.java │ ├── task/ │ │ └── TaskLauncherTests.java │ └── util/ │ ├── ByteSizeUtilsTests.java │ └── CommandLineTokenizerTests.java ├── spring-cloud-deployer-spi-scheduler-test-app/ │ ├── Dockerfile │ ├── pom.xml │ └── src/ │ └── main/ │ ├── java/ │ │ └── org/ │ │ └── springframework/ │ │ └── cloud/ │ │ └── deployer/ │ │ └── spi/ │ │ └── scheduler/ │ │ └── test/ │ │ └── app/ │ │ ├── SchedulerIntegrationTest.java │ │ ├── SchedulerIntegrationTestApplication.java │ │ └── SchedulerIntegrationTestProperties.java │ └── resources/ │ └── application.properties ├── spring-cloud-deployer-spi-test/ │ ├── pom.xml │ └── src/ │ └── main/ │ ├── java/ │ │ └── org/ │ │ └── springframework/ │ │ └── cloud/ │ │ └── deployer/ │ │ └── spi/ │ │ ├── scheduler/ │ │ │ └── test/ │ │ │ └── AbstractSchedulerIntegrationJUnit5Tests.java │ │ └── test/ │ │ ├── AbstractAppDeployerIntegrationJUnit5Tests.java │ │ ├── AbstractIntegrationJUnit5Tests.java │ │ ├── AbstractTaskLauncherIntegrationJUnit5Tests.java │ │ └── Timeout.java │ └── resources/ │ └── integration-test-app.properties ├── spring-cloud-deployer-spi-test-app/ │ ├── Dockerfile │ ├── pom.xml │ └── src/ │ └── main/ │ ├── java/ │ │ └── org/ │ │ └── springframework/ │ │ └── cloud/ │ │ └── deployer/ │ │ └── spi/ │ │ └── test/ │ │ └── app/ │ │ ├── DeployerIntegrationTest.java │ │ ├── DeployerIntegrationTestApplication.java │ │ └── DeployerIntegrationTestProperties.java │ └── resources/ │ └── application.properties └── src/ ├── main/ │ ├── java/ │ │ └── org/ │ │ └── springframework/ │ │ └── cloud/ │ │ └── deployer/ │ │ └── spi/ │ │ ├── kubernetes/ │ │ │ ├── DefaultContainerFactory.java │ │ │ ├── HttpProbeCreator.java │ │ │ ├── KubernetesAppDeployer.java │ │ │ ├── KubernetesAppInstanceStatus.java │ │ │ ├── KubernetesDeployerProperties.java │ │ │ ├── LivenessCommandProbeCreator.java │ │ │ ├── LivenessHttpProbeCreator.java │ │ │ ├── LivenessTcpProbeCreator.java │ │ │ ├── ProbeCreator.java │ │ │ ├── ProbeCreatorFactory.java │ │ │ ├── ReadinessCommandProbeCreator.java │ │ │ ├── ReadinessHttpProbeCreator.java │ │ │ └── ReadinessTcpProbeCreator.java │ │ └── local/ │ │ ├── AbstractLocalDeployerSupport.java │ │ ├── CommandBuilder.java │ │ ├── DebugAddress.java │ │ ├── DeployerSocketUtils.java │ │ ├── DockerCommandBuilder.java │ │ ├── HttpProbeExecutor.java │ │ ├── JavaCommandBuilder.java │ │ ├── LocalActuatorTemplate.java │ │ ├── LocalAppDeployer.java │ │ ├── LocalDeployerAutoConfiguration.java │ │ ├── LocalDeployerProperties.java │ │ ├── LocalDeployerUtils.java │ │ └── LocalTaskLauncher.java │ └── resources/ │ └── META-INF/ │ ├── additional-spring-configuration-metadata.json │ ├── spring/ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ └── spring.factories ├── scripts/ │ ├── next-minor-parent-snapshot-version │ ├── next-minor-snapshot-version │ ├── next-parent-snapshot-version │ └── next-snapshot-version └── test/ ├── java/ │ └── org/ │ └── springframework/ │ └── cloud/ │ └── deployer/ │ └── spi/ │ ├── kubernetes/ │ │ ├── DefaultContainerFactoryTests.java │ │ ├── KubernetesAppDeployerIntegrationIT.java │ │ ├── KubernetesAppDeployerTests.java │ │ ├── KubernetesTaskLauncherIntegrationIT.java │ │ └── KubernetesTaskLauncherWithJobIntegrationIT.java │ └── local/ │ ├── DebugAddressTests.java │ ├── DeployerSocketUtilsTests.java │ ├── DockerCommandBuilderTests.java │ ├── JavaExecutionCommandBuilderTests.java │ ├── LocalAppDeployerEnvironmentIntegrationTests.java │ ├── LocalAppDeployerIntegrationTests.java │ ├── LocalDeployerPropertiesTests.java │ ├── LocalDeployerSupportTests.java │ ├── LocalTaskLauncherIntegrationTests.java │ ├── RandomPortRangeContextTests.java │ ├── RandomPortRangeTests.java │ └── RandomPortTests.java └── resources/ ├── dataflow-server-podsecuritycontext.yml └── testResource.txt ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*.java] indent_style = tab indent_size = 4 [*.adoc] indent_style = tab indent_size = 4 [*.groovy] indent_style = tab indent_size = 4 [*.xml] indent_style = tab indent_size = 4 [*.yml] indent_style = space indent_size = 2 [*.yaml] indent_style = space indent_size = 2 [*.sh] indent_style = space indent_size = 4 end_of_line = lf ================================================ FILE: .github/actions/trivy-scan/action.yml ================================================ name: 'Trivy Scan' description: 'Run Trivy Scan on repository' runs: using: "composite" steps: - uses: actions/checkout@v4 - name: Run Trivy vulnerability scanner in repo mode env: TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db,aquasec/trivy-db,ghcr.io/aquasecurity/trivy-db TRIVY_JAVA_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-java-db,aquasec/trivy-java-db,ghcr.io/aquasecurity/trivy-java-db uses: aquasecurity/trivy-action@master with: scan-type: 'fs' scanners: 'vuln' ignore-unfixed: true severity: 'CRITICAL,HIGH' exit-code: 1 trivyignores: .trivyignore ================================================ FILE: .github/dco.yml ================================================ require: members: false ================================================ FILE: .github/settings.xml ================================================ stagingmilestone maven-central Maven Central https://repo.maven.apache.org/maven2 false spring-staging Spring Staging https://repo.spring.io/libs-staging false spring-milestones Spring Milestones https://repo.spring.io/libs-milestone false maven-central Maven Central https://repo.maven.apache.org/maven2 false spring-staging Spring Staging https://repo.spring.io/libs-staging false spring-milestones Spring Milestones https://repo.spring.io/libs-milestone false stagingrelease maven-central Maven Central https://repo.maven.apache.org/maven2 false spring-staging Spring Staging https://repo.spring.io/libs-staging false maven-central Maven Central https://repo.maven.apache.org/maven2 false spring-staging Spring Staging https://repo.spring.io/libs-staging false ================================================ FILE: .github/workflows/build-snapshot-worker.yml ================================================ # Worker which is dispatched from build-snapshot-controller workflow. name: Build Snapshot Worker on: workflow_dispatch: inputs: build-zoo-handler: description: 'Build Zoo Handler Payload' required: true jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v3 with: java-version: '17' distribution: 'liberica' - uses: jfrog/setup-jfrog-cli@v3 env: JF_URL: 'https://repo.spring.io' JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} # cache maven .m2 - uses: actions/cache@v3 with: path: ~/.m2/repository key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} restore-keys: | ${{ runner.os }}-m2- # target deploy repos - name: Configure JFrog Cli run: | jfrog rt mvnc --use-wrapper \ --server-id-resolve=${{ vars.JF_SERVER_ID }} \ --server-id-deploy=${{ vars.JF_SERVER_ID }} \ --repo-resolve-releases=libs-milestone \ --repo-resolve-snapshots=libs-snapshot \ --repo-deploy-releases=libs-release-local \ --repo-deploy-snapshots=libs-snapshot-local echo JFROG_CLI_BUILD_NAME=spring-cloud-deployer-main >> $GITHUB_ENV echo JFROG_CLI_BUILD_NUMBER=$GITHUB_RUN_NUMBER >> $GITHUB_ENV # zoo extract and ensure - name: Extract Zoo Context Properties uses: jvalkeal/build-zoo-handler@v0.0.4 with: dispatch-handler-extract-context-properties: true # build and publish to configured target - name: Build and Publish run: | jfrog rt mvn -U -B clean install -T 0.5C jfrog rt build-publish echo BUILD_ZOO_HANDLER_spring_cloud_deployer_version=$(./mvnw help:evaluate -Dexpression=project.version -q -DforceStdout) >> $GITHUB_ENV echo BUILD_ZOO_HANDLER_spring_cloud_deployer_buildname=spring-cloud-deployer-main >> $GITHUB_ENV echo BUILD_ZOO_HANDLER_spring_cloud_deployer_buildnumber=$GITHUB_RUN_NUMBER >> $GITHUB_ENV - name: Test Report uses: dorny/test-reporter@v1 if: ${{ success() || failure() }} with: name: Unit Test - Report path: '**/surefire-reports/*.xml' reporter: java-junit list-tests: 'failed' # zoo success - name: Notify Build Success Zoo Handler Controller uses: jvalkeal/build-zoo-handler@v0.0.4 with: dispatch-handler-token: ${{ secrets.SCDF_ACCESS_TOKEN }} dispatch-handler-client-payload-data: > { "event": "build-succeed" } # zoo failure - name: Notify Build Failure Zoo Handler Controller if: ${{ failure() }} uses: jvalkeal/build-zoo-handler@v0.0.4 with: dispatch-handler-token: ${{ secrets.SCDF_ACCESS_TOKEN }} dispatch-handler-client-payload-data: > { "event": "build-failed", "message": "spring-cloud-deployer failed" } # clean m2 cache - name: Clean cache run: | find ~/.m2/repository -type d -name '*SNAPSHOT' | xargs rm -fr scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run Trivy vulnerability scanner in repo mode uses: aquasecurity/trivy-action@master with: scan-type: 'fs' ignore-unfixed: true format: 'sarif' output: 'trivy-results.sarif' severity: 'CRITICAL,HIGH' - name: Upload Trivy scan results to GitHub Security tab uses: github/codeql-action/upload-sarif@v2 with: sarif_file: 'trivy-results.sarif' - name: 'Scanned' shell: bash run: echo "::info ::Scanned" done: runs-on: ubuntu-latest needs: [ scan, build ] steps: - name: 'Done' shell: bash run: echo "::info ::Done" ================================================ FILE: .github/workflows/ci-it.yml ================================================ name: CI IT on: workflow_dispatch: jobs: prepare: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: 'Load Matrix' id: matrix shell: bash run: | MATRIX=$(jq -c . .github/workflows/k8s-versions.json) echo "MATRIX=$MATRIX" >> $GITHUB_OUTPUT outputs: matrix: ${{ steps.matrix.outputs.MATRIX }} k8s-it: runs-on: ubuntu-latest needs: [ prepare ] strategy: fail-fast: false matrix: include: ${{ fromJson(needs.prepare.outputs.matrix) }} steps: - uses: actions/checkout@v4 - uses: actions/cache@v3 with: path: ~/.m2/repository key: ${{ runner.os }}-m2it-${{ hashFiles('**/pom.xml') }} restore-keys: | ${{ runner.os }}-m2- - uses: actions/setup-java@v3 with: java-version: '17' distribution: 'liberica' - name: 'Install minikube ${{ matrix.patch && matrix.patch || matrix.k8s_version }}' shell: bash run: | MINIKUBE_VERSION="v1.34.0" curl -LO "https://storage.googleapis.com/minikube/releases/$MINIKUBE_VERSION/minikube-linux-amd64" sudo install minikube-linux-amd64 /usr/local/bin/minikube minikube start "--kubernetes-version=${{ matrix.patch && matrix.patch || matrix.k8s_version }}" # build - name: Build run: | ./mvnw -B -Pfailsafe verify -pl spring-cloud-deployer-kubernetes - name: Test Report uses: dorny/test-reporter@v1 if: ${{ success() || failure() }} with: name: "Integration Test - Report for ${{ matrix.k8s_version }}" path: '**/failsafe-reports/*IT.xml' reporter: java-junit list-tests: 'failed' # clean m2 cache - name: Clean cache run: | find ~/.m2/repository -type d -name '*SNAPSHOT' | xargs rm -fr ================================================ FILE: .github/workflows/ci-pr.yml ================================================ name: CI PRs on: workflow_dispatch: pull_request: jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v3 with: java-version: '17' distribution: 'liberica' - name: Build run: | ./mvnw -U -B -s .settings.xml -Pspring clean install -T 0.5C - name: Scan uses: ./.github/actions/trivy-scan if: always() ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: workflow_dispatch: push: branches: - 'main' - '2.9.x' - '2.8.x' - '2.7.x' paths-ignore: - '.github/**' jobs: build: uses: ./.github/workflows/common-ci.yml with: artifactoryServerId: ${{ vars.JF_SERVER_ID }} secrets: inherit ================================================ FILE: .github/workflows/common-ci.yml ================================================ name: common-ci on: workflow_call: inputs: artifactoryServerId: type: string required: true description: 'Artifactory Server Id (typically from vars.JF_SERVER_ID)' secrets: JF_ARTIFACTORY_SPRING: env: ARTIFACTORY_KEY: ${{ secrets.JF_ARTIFACTORY_SPRING }} ARTIFACTORY_SERVER_ID: ${{ inputs.artifactoryServerId }} jobs: build_publish_scan: name: Build / Publish / Scan if: github.repository_owner == 'spring-cloud' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: java-version: '17' distribution: 'liberica' - uses: jfrog/setup-jfrog-cli@v4 env: JF_URL: 'https://repo.spring.io' JF_ENV_SPRING: ${{ env.ARTIFACTORY_KEY }} # setup frog cli - name: Configure JFrog Cli run: | jfrog mvnc --use-wrapper \ --server-id-deploy=${{ env.ARTIFACTORY_SERVER_ID }} \ --repo-deploy-releases=libs-release-local \ --repo-deploy-snapshots=libs-snapshot-local echo JFROG_CLI_BUILD_NAME=spring-cloud-deployer-main >> $GITHUB_ENV echo JFROG_CLI_BUILD_NUMBER=$GITHUB_RUN_NUMBER >> $GITHUB_ENV - name: Build and Publish run: | jfrog mvn -U -B -s .settings.xml clean install -T 0.5C jfrog rt build-publish - name: Scan uses: ./.github/actions/trivy-scan if: always() ================================================ FILE: .github/workflows/issue-handler.yml ================================================ name: Issue Handler on: workflow_dispatch: issues: types: [opened, labeled, unlabeled] issue_comment: types: [created] jobs: labeler: runs-on: ubuntu-latest steps: - name: Handle Issues uses: jvalkeal/issue-handler@v0.0.4 with: token: ${{ secrets.GITHUB_TOKEN }} config: > { "data": { "team": [ "jvalkeal", "oodamien", "ilayaperumalg", "sabbyanandan", "tzolov", "chrisjs", "cppwfs", "mminella", "dturanski", "onobc", "claudiahub", "sobychacko", "corneil" ] }, "recipes": [ { "name": "Mark new issue to get triaged", "type": "ifThen", "if": "isAction('opened') && !dataInArray('team', actor)", "then": "labelIssue(['status/need-triage'])" }, { "name": "Switch to team if user comments", "type": "ifThen", "if": "isEvent('issue_comment') && isAction('created') && actor == context.payload.issue.user.login && labelsContainsAny('status/need-feedback')", "then": "[labelIssue('for/team-attention'), removeLabel('status/need-feedback')]" }, { "name": "Switch to user if team comments", "type": "ifThen", "if": "isEvent('issue_comment') && isAction('created') && dataInArray('team', actor) && labelsContainsAny('for/team-attention') ", "then": "[labelIssue('status/need-feedback', removeLabel('for/team-attention'))]" }, { "name": "Manage backport issues", "type": "manageBackportIssues", "whenLabeled": "labeledStartsWith(['branch/'])", "whenUnlabeled": "labeledStartsWith(['branch/'])", "whenLabels": "labelsContainsAny(['for/backport'])", "fromLabels": "labeledStartsWith(['branch/'])", "additionalLabels": "'type/backport'", "body": "'Backport #' + number" } ] } ================================================ FILE: .github/workflows/k8s-versions.json ================================================ [ { "k8s_version": "v1.23", "patch": null }, { "k8s_version": "v1.24", "patch": null }, { "k8s_version": "v1.25", "patch": null }, { "k8s_version": "v1.26", "patch": null }, { "k8s_version": "v1.27", "patch": null }, { "k8s_version": "v1.28", "patch": null }, { "k8s_version": "v1.29", "patch": null }, { "k8s_version": "v1.30", "patch": null } ] ================================================ FILE: .github/workflows/milestone-worker.yml ================================================ name: Milestone Worker on: workflow_dispatch: inputs: build-zoo-handler: description: 'Build Zoo Handler Payload' required: true jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v3 with: java-version: '17' distribution: 'liberica' - uses: jfrog/setup-jfrog-cli@v3 env: JF_URL: 'https://repo.spring.io' JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} - uses: actions/cache@v3 with: path: ~/.m2/repository key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} restore-keys: | ${{ runner.os }}-m2- # target deploy repos - name: Configure JFrog Cli run: | jfrog rt mvnc --use-wrapper \ --server-id-resolve=${{ vars.JF_SERVER_ID }} \ --server-id-deploy=${{ vars.JF_SERVER_ID }} \ --repo-resolve-releases=libs-milestone \ --repo-resolve-snapshots=libs-snapshot \ --repo-deploy-releases=libs-milestone-local \ --repo-deploy-snapshots=libs-snapshot-local echo JFROG_CLI_BUILD_NAME=spring-cloud-deployer-main-milestone >> $GITHUB_ENV echo JFROG_CLI_BUILD_NUMBER=$GITHUB_RUN_NUMBER >> $GITHUB_ENV # zoo extract and ensure - name: Extract Zoo Context Properties uses: jvalkeal/build-zoo-handler@v0.0.4 with: dispatch-handler-extract-context-properties: true ensure-env: | BUILD_ZOO_HANDLER_milestone_version # build and publish to configured target - name: Build and Publish run: | jfrog rt mvn build-helper:parse-version versions:set \ -gs .github/settings.xml \ -Pstagingmilestone \ -DprocessAllModules=true \ -DgenerateBackupPoms=false \ -Dartifactory.publish.artifacts=false \ -DnewVersion='${parsedVersion.majorVersion}.${parsedVersion.minorVersion}.${parsedVersion.incrementalVersion}-'${BUILD_ZOO_HANDLER_milestone_version} \ -B echo BUILD_ZOO_HANDLER_spring_cloud_deployer_version=$(./mvnw help:evaluate -Dexpression=project.version -q -DforceStdout) >> $GITHUB_ENV jfrog rt build-clean jfrog rt mvn clean install \ -gs .github/settings.xml \ -P-spring,stagingmilestone \ -DskipTests -U -B -T 0.5C jfrog rt build-publish echo BUILD_ZOO_HANDLER_spring_cloud_deployer_buildname=spring-cloud-deployer-main-milestone >> $GITHUB_ENV echo BUILD_ZOO_HANDLER_spring_cloud_deployer_buildnumber=$GITHUB_RUN_NUMBER >> $GITHUB_ENV # zoo tag - name: Tag Release uses: jvalkeal/build-zoo-handler@v0.0.4 with: tag-release-branch: ${{ env.BUILD_ZOO_HANDLER_spring_cloud_deployer_version }} tag-release-tag: ${{ env.BUILD_ZOO_HANDLER_spring_cloud_deployer_version }} tag-release-tag-prefix: v # zoo success - name: Notify Build Success Zoo Handler Controller uses: jvalkeal/build-zoo-handler@v0.0.4 with: dispatch-handler-token: ${{ secrets.SCDF_ACCESS_TOKEN }} dispatch-handler-client-payload-data: > { "event": "build-succeed" } # zoo failure - name: Notify Build Failure Zoo Handler Controller if: ${{ failure() }} uses: jvalkeal/build-zoo-handler@v0.0.4 with: dispatch-handler-token: ${{ secrets.SCDF_ACCESS_TOKEN }} dispatch-handler-client-payload-data: > { "event": "build-failed", "message": "spring-cloud-deployer failed" } # clean m2 cache - name: Clean cache run: | find ~/.m2/repository -type d -name '*SNAPSHOT' | xargs rm -fr scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run Trivy vulnerability scanner in repo mode uses: aquasecurity/trivy-action@master with: scan-type: 'fs' ignore-unfixed: true format: 'sarif' output: 'trivy-results.sarif' severity: 'CRITICAL,HIGH' - name: Upload Trivy scan results to GitHub Security tab uses: github/codeql-action/upload-sarif@v2 with: sarif_file: 'trivy-results.sarif' - name: 'Scanned' shell: bash run: echo "::info ::Scanned" done: runs-on: ubuntu-latest needs: [ scan, build ] steps: - name: 'Done' shell: bash run: echo "::info ::Done" ================================================ FILE: .github/workflows/next-dev-version-worker.yml ================================================ name: Next Dev Version Worker on: workflow_dispatch: inputs: build-zoo-handler: description: 'Build Zoo Handler Payload' required: true jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v3 with: java-version: '17' distribution: 'liberica' - uses: jfrog/setup-jfrog-cli@v3 env: JF_URL: 'https://repo.spring.io' JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} # cache maven .m2 - uses: actions/cache@v3 with: path: ~/.m2/repository key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} restore-keys: | ${{ runner.os }}-m2- # target deploy repos - name: Configure JFrog Cli run: | jfrog rt mvnc --use-wrapper \ --server-id-resolve=${{ vars.JF_SERVER_ID }} \ --server-id-deploy=${{ vars.JF_SERVER_ID }} \ --repo-resolve-releases=libs-milestone \ --repo-resolve-snapshots=libs-snapshot \ --repo-deploy-releases=libs-release-local \ --repo-deploy-snapshots=libs-snapshot-local echo JFROG_CLI_BUILD_NAME=spring-cloud-deployer-main-ndv >> $GITHUB_ENV echo JFROG_CLI_BUILD_NUMBER=$GITHUB_RUN_NUMBER >> $GITHUB_ENV # zoo extract and ensure - name: Extract Zoo Context Properties uses: jvalkeal/build-zoo-handler@v0.0.4 with: dispatch-handler-extract-context-properties: true # build and publish to configured target - name: Build and Publish run: | jfrog rt mvn build-helper:parse-version versions:set \ -DprocessAllModules=true \ -DgenerateBackupPoms=false \ -Dartifactory.publish.artifacts=false \ -DnewVersion='${parsedVersion.majorVersion}.${parsedVersion.minorVersion}.${parsedVersion.nextIncrementalVersion}-SNAPSHOT' \ -B echo BUILD_ZOO_HANDLER_spring_cloud_deployer_version=$(./mvnw help:evaluate -Dexpression=project.version -q -DforceStdout) >> $GITHUB_ENV jfrog rt build-clean jfrog rt mvn clean install -DskipTests -B -T 0.5C jfrog rt build-publish echo BUILD_ZOO_HANDLER_spring_cloud_deployer_buildname=spring-cloud-deployer-main-ndv >> $GITHUB_ENV echo BUILD_ZOO_HANDLER_spring_cloud_deployer_buildnumber=$GITHUB_RUN_NUMBER >> $GITHUB_ENV # zoo commit - name: Commit Next Dev Changes uses: jvalkeal/build-zoo-handler@v0.0.4 with: commit-changes-branch: main commit-changes-message: Next development version # zoo success - name: Notify Build Success Zoo Handler Controller uses: jvalkeal/build-zoo-handler@v0.0.4 with: dispatch-handler-token: ${{ secrets.SCDF_ACCESS_TOKEN }} dispatch-handler-client-payload-data: > { "event": "next-dev-version-succeed" } # zoo failure - name: Notify Build Failure Zoo Handler Controller if: ${{ failure() }} uses: jvalkeal/build-zoo-handler@v0.0.4 with: dispatch-handler-token: ${{ secrets.SCDF_ACCESS_TOKEN }} dispatch-handler-client-payload-data: > { "event": "next-dev-version-failed", "message": "spring-cloud-dataflow-build next version failed" } # clean m2 cache - name: Clean cache run: | find ~/.m2/repository -type d -name '*SNAPSHOT' | xargs rm -fr scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run Trivy vulnerability scanner in repo mode uses: aquasecurity/trivy-action@master with: scan-type: 'fs' ignore-unfixed: true format: 'sarif' output: 'trivy-results.sarif' severity: 'CRITICAL,HIGH' - name: Upload Trivy scan results to GitHub Security tab uses: github/codeql-action/upload-sarif@v2 with: sarif_file: 'trivy-results.sarif' - name: 'Scanned' shell: bash run: echo "::info ::Scanned" done: runs-on: ubuntu-latest needs: [ scan, build ] steps: - name: 'Done' shell: bash run: echo "::info ::Done" ================================================ FILE: .github/workflows/release-worker.yml ================================================ name: Release Worker on: workflow_dispatch: inputs: build-zoo-handler: description: 'Build Zoo Handler Payload' required: true jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v3 with: java-version: '17' distribution: 'liberica' - uses: jfrog/setup-jfrog-cli@v3 env: JF_URL: 'https://repo.spring.io' JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} - uses: actions/cache@v3 with: path: ~/.m2/repository key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} restore-keys: | ${{ runner.os }}-m2- # target deploy repos - name: Configure JFrog Cli run: | jfrog rt mvnc --use-wrapper \ --server-id-resolve=${{ vars.JF_SERVER_ID }} \ --server-id-deploy=${{ vars.JF_SERVER_ID }} \ --repo-resolve-releases=libs-release-staging \ --repo-resolve-snapshots=libs-snapshot \ --repo-deploy-releases=libs-staging-local \ --repo-deploy-snapshots=libs-snapshot-local echo JFROG_CLI_BUILD_NAME=spring-cloud-deployer-main-release >> $GITHUB_ENV echo JFROG_CLI_BUILD_NUMBER=$GITHUB_RUN_NUMBER >> $GITHUB_ENV # zoo extract and ensure - name: Extract Zoo Context Properties uses: jvalkeal/build-zoo-handler@v0.0.4 with: dispatch-handler-extract-context-properties: true # build and publish to configured target - name: Build and Publish run: | jfrog rt mvn build-helper:parse-version versions:set \ -gs .github/settings.xml \ -Pstagingrelease \ -DprocessAllModules=true \ -DgenerateBackupPoms=false \ -Dartifactory.publish.artifacts=false \ -DnewVersion='${parsedVersion.majorVersion}.${parsedVersion.minorVersion}.${parsedVersion.incrementalVersion}' \ -B echo BUILD_ZOO_HANDLER_spring_cloud_deployer_version=$(./mvnw help:evaluate -Dexpression=project.version -q -DforceStdout) >> $GITHUB_ENV jfrog rt build-clean jfrog rt mvn clean install \ -gs .github/settings.xml \ -P-spring,stagingrelease \ -DskipTests -U -B -T 0.5C jfrog rt build-publish echo BUILD_ZOO_HANDLER_spring_cloud_deployer_buildname=spring-cloud-deployer-main-release >> $GITHUB_ENV echo BUILD_ZOO_HANDLER_spring_cloud_deployer_buildnumber=$GITHUB_RUN_NUMBER >> $GITHUB_ENV # zoo tag - name: Tag Release uses: jvalkeal/build-zoo-handler@v0.0.4 with: tag-release-branch: ${{ env.BUILD_ZOO_HANDLER_spring_cloud_deployer_version }} tag-release-tag: ${{ env.BUILD_ZOO_HANDLER_spring_cloud_deployer_version }} tag-release-tag-prefix: v # zoo success - name: Notify Build Success Zoo Handler Controller uses: jvalkeal/build-zoo-handler@v0.0.4 with: dispatch-handler-token: ${{ secrets.SCDF_ACCESS_TOKEN }} dispatch-handler-client-payload-data: > { "event": "build-succeed" } # zoo failure - name: Notify Build Failure Zoo Handler Controller if: ${{ failure() }} uses: jvalkeal/build-zoo-handler@v0.0.4 with: dispatch-handler-token: ${{ secrets.SCDF_ACCESS_TOKEN }} dispatch-handler-client-payload-data: > { "event": "build-failed", "message": "spring-cloud-deployer failed" } # clean m2 cache - name: Clean cache run: | find ~/.m2/repository -type d -name '*SNAPSHOT' | xargs rm -fr scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run Trivy vulnerability scanner in repo mode uses: aquasecurity/trivy-action@master with: scan-type: 'fs' ignore-unfixed: true format: 'sarif' output: 'trivy-results.sarif' severity: 'CRITICAL,HIGH' - name: Upload Trivy scan results to GitHub Security tab uses: github/codeql-action/upload-sarif@v2 with: sarif_file: 'trivy-results.sarif' - name: 'Scanned' shell: bash run: echo "::info ::Scanned" done: runs-on: ubuntu-latest needs: [ scan, build ] steps: - name: 'Done' shell: bash run: echo "::info ::Done" ================================================ FILE: .gitignore ================================================ *~ .#* *# *.sw* _site/ .factorypath .gradletasknamecache .DS_Store .checkstyle /application.yml /application.properties asciidoctor.css atlassian-ide-plugin.xml bin/ build/ dump.rdb out spring-shell.log target/ test-output .jfrog/ # Eclipse artifacts, including WTP generated manifests .classpath .project .settings/ .springBeans spring-*/src/main/java/META-INF/MANIFEST.MF # IDEA artifacts and output dirs *.iml *.ipr *.iws .idea/ rebel.xml # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json ================================================ FILE: .mvn/jvm.config ================================================ -Xmx1024m -XX:CICompilerCount=1 -XX:TieredStopAtLevel=1 -Djava.security.egd=file:/dev/./urandom ================================================ FILE: .mvn/maven.config ================================================ -DaltSnapshotDeploymentRepository=repo.spring.io::default::https://repo.spring.io/libs-snapshot-local -P spring ================================================ FILE: .mvn/wrapper/maven-wrapper.properties ================================================ distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.8/apache-maven-3.8.8-bin.zip ================================================ FILE: .settings.xml ================================================ repo.spring.io ${env.CI_DEPLOY_USERNAME} ${env.CI_DEPLOY_PASSWORD} spring true maven-central Maven Central https://repo.maven.apache.org/maven2 false spring-snapshots Spring Snapshots https://repo.spring.io/libs-snapshot true spring-milestones Spring Milestones https://repo.spring.io/libs-milestone false maven-central Maven Central https://repo.maven.apache.org/maven2 false spring-snapshots Spring Snapshots https://repo.spring.io/libs-snapshot true spring-milestones Spring Milestones https://repo.spring.io/libs-milestone false ================================================ FILE: .trivyignore ================================================ CVE-2022-1471 CVE-2016-1000027 ================================================ FILE: CONTRIBUTING.adoc ================================================ = Contributing to Spring Cloud Deployer :github: https://github.com/spring-cloud/spring-cloud-deployer Spring Cloud Deployer is released under the Apache 2.0 license. If you would like to contribute something, or want to hack on the code this document should help you get started. == Using GitHub Issues We use GitHub issues to track bugs and enhancements. If you have a general usage question please ask on https://stackoverflow.com[Stack Overflow]. The Spring Cloud Deployer team and the broader community monitor the https://stackoverflow.com/tags/spring-cloud-deployer[`spring-cloud-deployer`] tag. If you are reporting a bug, please help to speed up problem diagnosis by providing as much information as possible. Ideally, that would include a small sample project that reproduces the problem. == Reporting Security Vulnerabilities If you think you have found a security vulnerability in Spring Cloud Deployer please *DO NOT* disclose it publicly until we've had a chance to fix it. Please don't report security vulnerabilities using GitHub issues, instead head over to https://spring.io/security-policy and learn how to disclose them responsibly. == Developer Certificate of Origin All commits must include a **Signed-off-by** trailer at the end of each commit message to indicate that the contributor agrees to the Developer Certificate of Origin. For additional details, please refer to the blog post https://spring.io/blog/2025/01/06/hello-dco-goodbye-cla-simplifying-contributions-to-spring[Hello DCO, Goodbye CLA: Simplifying Contributions to Spring]. === Code Conventions and Housekeeping None of the following guidelines is essential for a pull request, but they all help your fellow developers understand and work with your code. They can also be added after the original pull request but before a merge. * Use the Spring Framework code format conventions. If you use Eclipse, you can import formatter settings by using the `eclipse-code-formatter.xml` file from the https://github.com/spring-cloud/spring-cloud-build/blob/master/spring-cloud-dependencies-parent/eclipse-code-formatter.xml[Spring Cloud Build] project. If you use IntelliJ, you can use the https://plugins.jetbrains.com/plugin/6546[Eclipse Code Formatter Plugin] to import the same file. * Make sure all new `.java` files have a simple Javadoc class comment with at least an `@author` tag identifying you, and preferably at least a paragraph describing the class's purpose. * Add the ASF license header comment to all new `.java` files (to do so, copy it from existing files in the project). * Add yourself as an `@author` to the .java files that you modify substantially (more than cosmetic changes). * Add some Javadocs and, if you change the namespace, some XSD doc elements. * A few unit tests would help a lot as well. Someone has to do it, and your fellow developers appreciate the effort. * If no one else uses your branch, rebase it against the current master (or other target branch in the main project). * When writing a commit message, follow https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html[these conventions]. If you fix an existing issue, add `Fixes gh-XXXX` (where XXXX is the issue number) at the end of the commit message. ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 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. ================================================ FILE: NOTICE ================================================ spring-cloud-deployer Copyright (c) 2016-Present Pivotal Software, Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 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. ================================================ FILE: README.adoc ================================================ # Spring Cloud Data Flow is no longer maintained as an open-source project by Broadcom, Inc. ## For information about extended support or enterprise options for Spring Cloud Data Flow, please read the official blog post [here](https://spring.io/blog/2025/04/21/spring-cloud-data-flow-commercial). == Spring Cloud Deployer [frame=none, grid=none, caption=, width="75%", cols="^2,^2"] .Build Status by Branches |=== | *Main* | *2.9.x* |=== The Spring Cloud Deployer project defines a Service Provider Interface (SPI) for deploying long lived applications and short lived tasks. == Components The https://github.com/spring-cloud/spring-cloud-deployer/tree/master/spring-cloud-deployer-spi[SPI] project defines the core interfaces, including https://github.com/spring-cloud/spring-cloud-deployer/blob/master/spring-cloud-deployer-spi/src/main/java/org/springframework/cloud/deployer/spi/app/AppDeployer.java[AppDeployer] and https://github.com/spring-cloud/spring-cloud-deployer/blob/master/spring-cloud-deployer-spi/src/main/java/org/springframework/cloud/deployer/spi/task/TaskLauncher.java[TaskLauncher] as well as the core domain model. The https://github.com/spring-cloud/spring-cloud-deployer/tree/master/spring-cloud-deployer-spi-test[SPI Test] project provides the basic test framework that any SPI implementation should use to verify its functionality. The https://github.com/spring-cloud/spring-cloud-deployer/tree/master/spring-cloud-deployer-resource-maven[spring-cloud-deployer-resource-maven] project provides support for referencing Maven artifacts via Spring's `Resource` abstraction. The https://github.com/spring-cloud/spring-cloud-deployer/tree/master/spring-cloud-deployer-resource-docker[spring-cloud-deployer-resource-docker] project provides support for referencing Docker artifacts via Spring's `Resource` abstraction. The https://github.com/spring-cloud/spring-cloud-deployer/tree/master/spring-cloud-deployer-resource-support[spring-cloud-deployer-resource-support] project provides various common support classes for working with `Resources`, such as the https://github.com/spring-cloud/spring-cloud-deployer/blob/master/spring-cloud-deployer-resource-support/src/main/java/org/springframework/cloud/deployer/resource/registry/UriRegistry.java[UriRegistry] for maintaining the locations of app artifacts, and the https://github.com/spring-cloud/spring-cloud-deployer/blob/master/spring-cloud-deployer-resource-support/src/main/java/org/springframework/cloud/deployer/resource/support/DelegatingResourceLoader.java[DelegatingResourceLoader] for working with multiple `ResourceLoader` implementations in a map with URI schemes as keys. There is also an implementation of the SPI for running apps locally. That link is provided below along with other SPI implementations. == Implementations This deployer SPI has been implemented for several runtime environments. Here are the github locations: * https://github.com/spring-cloud/spring-cloud-deployer/blob/master/spring-cloud-deployer-local[Local] * https://github.com/spring-cloud/spring-cloud-deployer/blob/master/spring-cloud-deployer-cloudfoundry[Cloud Foundry] * https://github.com/spring-cloud/spring-cloud-deployer/blob/master/spring-cloud-deployer-kubernetes[Kubernetes] === Building Clone the repo and type ---- $ ./mvnw clean install ---- ## Contributing We welcome contributions! See the link:./CONTRIBUTING.adoc[CONTRIBUTING] guide for details. ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Reporting a Vulnerability If you think you have found a security vulnerability, please **DO NOT** disclose it publicly until we’ve had a chance to fix it. Please don’t report security vulnerabilities using GitHub issues, instead head over to https://spring.io/security-policy and learn how to disclose them responsibly. ================================================ FILE: mvnw ================================================ #!/bin/sh # ---------------------------------------------------------------------------- # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # 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. # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- # Maven2 Start Up Batch script # # Required ENV vars: # ------------------ # JAVA_HOME - location of a JDK home dir # # Optional ENV vars # ----------------- # M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 # MAVEN_SKIP_RC - flag to disable loading of mavenrc files # ---------------------------------------------------------------------------- if [ -z "$MAVEN_SKIP_RC" ] ; then if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi if [ -f "$HOME/.mavenrc" ] ; then . "$HOME/.mavenrc" fi fi # OS specific support. $var _must_ be set to either true or false. cygwin=false; darwin=false; mingw=false case "`uname`" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true # # Look for the Apple JDKs first to preserve the existing behaviour, and then look # for the new JDKs provided by Oracle. # if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK ] ; then # # Apple JDKs # export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home fi if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Java/JavaVirtualMachines/CurrentJDK ] ; then # # Apple JDKs # export JAVA_HOME=/System/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home fi if [ -z "$JAVA_HOME" ] && [ -L "/Library/Java/JavaVirtualMachines/CurrentJDK" ] ; then # # Oracle JDKs # export JAVA_HOME=/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home fi if [ -z "$JAVA_HOME" ] && [ -x "/usr/libexec/java_home" ]; then # # Apple JDKs # export JAVA_HOME=`/usr/libexec/java_home` fi ;; esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then JAVA_HOME=`java-config --jre-home` fi fi if [ -z "$M2_HOME" ] ; then ## resolve links - $0 may be a link to maven's home PRG="$0" # need this for relative symlinks while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG="`dirname "$PRG"`/$link" fi done saveddir=`pwd` M2_HOME=`dirname "$PRG"`/.. # make it fully qualified M2_HOME=`cd "$M2_HOME" && pwd` cd "$saveddir" # echo Using m2 at $M2_HOME fi # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --unix "$CLASSPATH"` fi # For Migwn, ensure paths are in UNIX format before anything is touched if $mingw ; then [ -n "$M2_HOME" ] && M2_HOME="`(cd "$M2_HOME"; pwd)`" [ -n "$JAVA_HOME" ] && JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" # TODO classpath? fi if [ -z "$JAVA_HOME" ]; then javaExecutable="`which javac`" if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. readLink=`which readlink` if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then if $darwin ; then javaHome="`dirname \"$javaExecutable\"`" javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" else javaExecutable="`readlink -f \"$javaExecutable\"`" fi javaHome="`dirname \"$javaExecutable\"`" javaHome=`expr "$javaHome" : '\(.*\)/bin'` JAVA_HOME="$javaHome" export JAVA_HOME fi fi fi if [ -z "$JAVACMD" ] ; then if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi else JAVACMD="`which java`" fi fi if [ ! -x "$JAVACMD" ] ; then echo "Error: JAVA_HOME is not defined correctly." >&2 echo " We cannot execute $JAVACMD" >&2 exit 1 fi if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher # For Cygwin, switch paths to Windows format before running java if $cygwin; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --windows "$CLASSPATH"` fi # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { local basedir=$(pwd) local wdir=$(pwd) while [ "$wdir" != '/' ] ; do if [ -d "$wdir"/.mvn ] ; then basedir=$wdir break fi wdir=$(cd "$wdir/.."; pwd) done echo "${basedir}" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then echo "$(tr -s '\n' ' ' < "$1")" fi } export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)} MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # Provide a "standardized" way to retrieve the CLI args that will # work with both Windows and non-Windows executions. MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" export MAVEN_CMD_LINE_ARGS WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain exec "$JAVACMD" \ $MAVEN_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} "$@" ================================================ FILE: mvnw.cmd ================================================ @REM ---------------------------------------------------------------------------- @REM Licensed to the Apache Software Foundation (ASF) under one @REM or more contributor license agreements. See the NOTICE file @REM distributed with this work for additional information @REM regarding copyright ownership. The ASF licenses this file @REM to you under the Apache License, Version 2.0 (the @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM @REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @REM KIND, either express or implied. See the License for the @REM specific language governing permissions and limitations @REM under the License. @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- @REM Maven2 Start Up Batch script @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars @REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files @REM ---------------------------------------------------------------------------- @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" :skipRcPre @setlocal set ERROR_CODE=0 @REM To isolate internal variables from possible post scripts, we use another setlocal @setlocal @REM ==== START VALIDATION ==== if not "%JAVA_HOME%" == "" goto OkJHome echo. echo Error: JAVA_HOME not found in your environment. >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error :OkJHome if exist "%JAVA_HOME%\bin\java.exe" goto init echo. echo Error: JAVA_HOME is set to an invalid directory. >&2 echo JAVA_HOME = "%JAVA_HOME%" >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error @REM ==== END VALIDATION ==== :init set MAVEN_CMD_LINE_ARGS=%* @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". @REM Fallback to current working directory if not found. set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir set EXEC_DIR=%CD% set WDIR=%EXEC_DIR% :findBaseDir IF EXIST "%WDIR%"\.mvn goto baseDirFound cd .. IF "%WDIR%"=="%CD%" goto baseDirNotFound set WDIR=%CD% goto findBaseDir :baseDirFound set MAVEN_PROJECTBASEDIR=%WDIR% cd "%EXEC_DIR%" goto endDetectBaseDir :baseDirNotFound set MAVEN_PROJECTBASEDIR=%EXEC_DIR% cd "%EXEC_DIR%" :endDetectBaseDir IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig @setlocal EnableExtensions EnableDelayedExpansion for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" set WRAPPER_JAR="".\.mvn\wrapper\maven-wrapper.jar"" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CMD_LINE_ARGS% if ERRORLEVEL 1 goto error goto end :error set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' if "%MAVEN_BATCH_PAUSE%" == "on" pause if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% exit /B %ERROR_CODE% ================================================ FILE: pom.xml ================================================ 4.0.0 spring-cloud-deployer-parent 3.0.0-SNAPSHOT org.springframework.cloud pom Spring Cloud Deployer Spring Cloud Deployer Pivotal Software, Inc. https://www.spring.io https://github.com/spring-cloud/spring-cloud-deployer Apache License, Version 2.0 https://www.apache.org/licenses/LICENSE-2.0 Copyright 2014-2021 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. https://github.com/spring-cloud/spring-cloud-deployer scm:git:git://github.com/spring-cloud/spring-cloud-deployer.git scm:git:ssh://git@github.com/spring-cloud/spring-cloud-deployer.git HEAD scdf-team Data Flow Team https://github.com/spring-cloud/spring-cloud-deployer/graphs/contributors UTF-8 17 3.4.5 1.26.2 2.16.1 6.13.5 1.0.1 5.12.1.RELEASE 2.2.0.RELEASE 3.9.6 1.9.18 3.5.3 spring-cloud-deployer-spi spring-cloud-deployer-resource-docker spring-cloud-deployer-resource-maven spring-cloud-deployer-spi-test spring-cloud-deployer-resource-support spring-cloud-deployer-spi-test-app spring-cloud-deployer-spi-scheduler-test-app spring-cloud-deployer-autoconfigure spring-cloud-deployer-dependencies spring-cloud-deployer-local spring-cloud-deployer-kubernetes spring-cloud-deployer-cloudfoundry io.fabric8 kubernetes-client-bom ${kubernetes-fabric8-client.version} pom import org.apache.commons commons-compress ${commons-compress.version} commons-io commons-io ${commons-io.version} org.springframework.boot spring-boot-dependencies ${spring-boot.version} pom import org.springframework.cloud spring-cloud-deployer-dependencies ${project.version} pom import org.apache.maven.wagon wagon-http ${maven-wagon.version} org.cloudfoundry cloudfoundry-client-reactor ${cloudfoundry-java-lib.version} org.cloudfoundry cloudfoundry-operations ${cloudfoundry-java-lib.version} io.pivotal pivotal-cloudfoundry-client-reactor ${pivotal-cf-client-reactor.version} spring true maven-central Maven Central https://repo.maven.apache.org/maven2 false spring-snapshots Spring Snapshots https://repo.spring.io/libs-snapshot true spring-milestones Spring Milestones https://repo.spring.io/libs-milestone false maven-central Maven Central https://repo.maven.apache.org/maven2 false spring-snapshots Spring Snapshots https://repo.spring.io/libs-snapshot true spring-milestones Spring Milestones https://repo.spring.io/libs-milestone false coverage env.TRAVIS true org.jacoco jacoco-maven-plugin 0.7.9 agent prepare-agent report test report org.apache.maven.plugins maven-checkstyle-plugin 3.2.1 org.apache.maven.plugins maven-compiler-plugin 3.8.0 ${java.version} ${java.version} org.apache.maven.plugins maven-surefire-plugin 3.1.2 org.apache.maven.plugins maven-javadoc-plugin 3.4.1 javadoc jar package org.apache.maven.plugins maven-source-plugin 3.3.0 source jar package ================================================ FILE: spring-cloud-deployer-autoconfigure/pom.xml ================================================ 4.0.0 spring-cloud-deployer-autoconfigure jar Spring Cloud Deployer Autoconfigure org.springframework.cloud spring-cloud-deployer-parent 3.0.0-SNAPSHOT .. org.springframework.boot spring-boot-autoconfigure org.springframework.boot spring-boot-configuration-processor true org.springframework.boot spring-boot-starter-test test org.springframework.cloud spring-cloud-deployer-resource-maven true org.springframework.cloud spring-cloud-deployer-resource-support true src/main/resources true META-INF/spring.factories META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ FILE: spring-cloud-deployer-autoconfigure/src/main/java/org/springframework/cloud/deployer/autoconfigure/DelegatingResourceLoaderBuilder.java ================================================ /* * Copyright 2018 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.cloud.deployer.autoconfigure; import java.util.HashMap; import java.util.Map; import org.springframework.cloud.deployer.resource.support.DelegatingResourceLoader; import org.springframework.core.io.ResourceLoader; /** * Builder implementation for a {@link DelegatingResourceLoader} which allows to * register resource loaders with schemes before actual instance of a * {@link DelegatingResourceLoader} is created. * * @author Janne Valkealahti * */ public class DelegatingResourceLoaderBuilder { private final Map loaders = new HashMap<>(); /** * Register a map of resource loaders. * * @param loaders the resource loaders to register * @return builder instance for chaining */ public DelegatingResourceLoaderBuilder loaders(Map loaders) { this.loaders.putAll(loaders); return this; } /** * register a loader with a scheme. * * @param scheme the scheme * @param loader the resource loader * @return builder instance for chaining */ public DelegatingResourceLoaderBuilder loader(String scheme, ResourceLoader loader) { this.loaders.put(scheme, loader); return this; } /** * Builds a {@link DelegatingResourceLoader}. * * @return the built delegating resource loader */ public DelegatingResourceLoader build() { return new DelegatingResourceLoader(this.loaders); } } ================================================ FILE: spring-cloud-deployer-autoconfigure/src/main/java/org/springframework/cloud/deployer/autoconfigure/DelegatingResourceLoaderBuilderCustomizer.java ================================================ /* * Copyright 2018 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.cloud.deployer.autoconfigure; import org.springframework.cloud.deployer.resource.support.DelegatingResourceLoader; /** * Callback interface that can be used to customize resource loaders for a * {@link DelegatingResourceLoader}. * * @author Janne Valkealahti * */ @FunctionalInterface public interface DelegatingResourceLoaderBuilderCustomizer { /** * Callback to customize a {@link DelegatingResourceLoaderBuilder} instance. * * @param builder the loader builder */ void customize(DelegatingResourceLoaderBuilder builder); } ================================================ FILE: spring-cloud-deployer-autoconfigure/src/main/java/org/springframework/cloud/deployer/autoconfigure/MavenConfigurationProperties.java ================================================ /* * Copyright 2018 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.cloud.deployer.autoconfigure; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.cloud.deployer.resource.maven.MavenProperties; /** * Configuration properties for a {@link MavenProperties}. * * @author Janne Valkealahti * */ @ConfigurationProperties(prefix = "maven") public class MavenConfigurationProperties extends MavenProperties { } ================================================ FILE: spring-cloud-deployer-autoconfigure/src/main/java/org/springframework/cloud/deployer/autoconfigure/ResourceLoadingAutoConfiguration.java ================================================ /* * Copyright 2018 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.cloud.deployer.autoconfigure; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.deployer.resource.maven.MavenProperties; import org.springframework.cloud.deployer.resource.maven.MavenResourceLoader; import org.springframework.cloud.deployer.resource.support.DelegatingResourceLoader; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.core.io.ResourceLoader; /** * Autoconfiguration of a file or Maven based {@link ResourceLoader}. * * @author Michael Minella * @author Janne Valkealahti */ @Configuration public class ResourceLoadingAutoConfiguration { @Configuration @ConditionalOnClass(MavenResourceLoader.class) @EnableConfigurationProperties(MavenConfigurationProperties.class) public static class MavenResourceLoaderConfig { @Bean @Order(0) public DelegatingResourceLoaderBuilderCustomizer mavenDelegatingResourceLoaderBuilderCustomizer(MavenProperties mavenProperties) { return customizer -> customizer.loader("maven", new MavenResourceLoader(mavenProperties)); } } @Configuration @ConditionalOnMissingBean(DelegatingResourceLoader.class) public static class DelegatingResourceLoaderConfig { private final ObjectProvider loaderBuilderCustomizers; public DelegatingResourceLoaderConfig( ObjectProvider loaderBuilderCustomizers) { this.loaderBuilderCustomizers = loaderBuilderCustomizers; } @Bean public DelegatingResourceLoader delegatingResourceLoader() { DelegatingResourceLoaderBuilder builder = new DelegatingResourceLoaderBuilder(); this.loaderBuilderCustomizers.orderedStream().forEach(customizer -> customizer.customize(builder)); return builder.build(); } } } ================================================ FILE: spring-cloud-deployer-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ org.springframework.cloud.deployer.autoconfigure.ResourceLoadingAutoConfiguration ================================================ FILE: spring-cloud-deployer-autoconfigure/src/main/resources/META-INF/spring.factories ================================================ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.cloud.deployer.autoconfigure.ResourceLoadingAutoConfiguration ================================================ FILE: spring-cloud-deployer-autoconfigure/src/test/java/org/springframework/cloud/deployer/autoconfigure/ResourceLoadingAutoConfigurationTests.java ================================================ /* * Copyright 2018 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.cloud.deployer.autoconfigure; import org.assertj.core.api.Condition; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.cloud.deployer.resource.maven.MavenProperties; import org.springframework.cloud.deployer.resource.support.DelegatingResourceLoader; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import static org.assertj.core.api.Assertions.assertThat; /** * Tests for {@link ResourceLoadingAutoConfiguration}. * * @author Janne Valkealahti * */ public class ResourceLoadingAutoConfigurationTests { private final static ResourceLoader mockResourceLoader = new ResourceLoader() { @Override public Resource getResource(String location) { return null; } @Override public ClassLoader getClassLoader() { return null; } }; private final static Condition mavenCondition = new Condition<>( l -> l.getLoaders().containsKey("maven"), "maven loader"); private final static Condition mavenReplacedCondition = new Condition<>( l -> l.getLoaders().get("maven").equals(mockResourceLoader), "maven loader replaced"); private final static Condition foobarCondition = new Condition<>( l -> l.getLoaders().containsKey("foobar"), "foobar mock loader"); private final static Condition offlineCondition = new Condition<>( p -> p.isOffline(), "offline"); private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(ResourceLoadingAutoConfiguration.class)); @Test public void testAutoConfigNoProperties() { this.contextRunner .run((context) -> { assertThat(context).hasSingleBean(DelegatingResourceLoader.class); assertThat(context).getBean(DelegatingResourceLoader.class).has(mavenCondition); }); } @Test public void testMavenProperties() { this.contextRunner .withPropertyValues("maven.offline=true") .run((context) -> assertThat(context).getBean(MavenProperties.class).has(offlineCondition)); } @Test public void testBuilderRegistration() { this.contextRunner .withUserConfiguration(CustomBuilderCustomizerConfig.class) .run((context) -> assertThat(context).getBean(DelegatingResourceLoader.class).has(foobarCondition)); } @Test public void testBuilderOrderRegistration() { this.contextRunner .withUserConfiguration(MavenReplacingBuilderCustomizerConfig.class) .run((context) -> assertThat(context).getBean(DelegatingResourceLoader.class).has(mavenReplacedCondition)); } @Configuration static class CustomBuilderCustomizerConfig { @Bean public DelegatingResourceLoaderBuilderCustomizer foobarDelegatingResourceLoaderBuilderCustomizer() { return customizer -> customizer.loader("foobar", mockResourceLoader); } } @Configuration static class MavenReplacingBuilderCustomizerConfig { @Bean @Order(Ordered.LOWEST_PRECEDENCE) public DelegatingResourceLoaderBuilderCustomizer foobarDelegatingResourceLoaderBuilderCustomizer() { return customizer -> customizer.loader("maven", mockResourceLoader); } } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/README.adoc ================================================ = Spring Cloud Deployer for Cloud Foundry This project provides a common means to deploy applications to Cloud Foundry based on the Spring Cloud Deployer SPI. == Building Clone the repo and type ---- $ ./mvnw clean install ---- The project includes a set of integration tests that can be run against a Cloud Foundry installation, provided that connection information is correctly set. If credentials are not set correctly, those tests will be silently skipped. Below is a short list of common Spring Boot `@ConfigurationProperties` (in environment variable format) that you will need to set in order to deploy applications to Cloud Foundry: ---- # url of the CF API (used when using cf login -a for example) SPRING_CLOUD_DEPLOYER_CLOUDFOUNDRY_URL # name of the space into which modules will be deployed SPRING_CLOUD_DEPLOYER_CLOUDFOUNDRY_SPACE # name of the organization that owns the space above SPRING_CLOUD_DEPLOYER_CLOUDFOUNDRY_ORG # the root domain to use when mapping routes SPRING_CLOUD_DEPLOYER_CLOUDFOUNDRY_DOMAIN # Comma separated set of service instance names to bind to the deployed app SPRING_CLOUD_DEPLOYER_CLOUDFOUNDRY_SERVICES # username and password of the user to use to create apps SPRING_CLOUD_DEPLOYER_CLOUDFOUNDRY_USERNAME SPRING_CLOUD_DEPLOYER_CLOUDFOUNDRY_PASSWORD # the identity provider to be used when accessing the Cloud Foundry API (optional) # the passed string has to be a URL-Encoded JSON Object, containing the field origin with value as origin_key of an identity provider. SPRING_CLOUD_DEPLOYER_CLOUDFOUNDRY_LOGIN_HINT # whether to allow self-signed certificates during SSL validation SPRING_CLOUD_DEPLOYER_CLOUDFOUNDRY_SKIP_SSL_VALIDATION ---- Refer to `CloudFoundryDeploymentProperties.java` and `CloudFoundryConnectionProperties.java` for a complete listing. == Services, Disk and Memory Settings for Applications The deployer also supports setting the properties `spring.cloud.deployer.cloudfoundry.services`, `spring.cloud.deployer.cloudfoundry.memory`, and `spring.cloud.deployer.cloudfoundry.disk` as part of an individual deployment request. == Application Name Settings and Deployments To help avoid clashes with routes across spaces on Cloud Foundry, a naming strategy to provide a random prefix to a deployed application is available and is enabled by default. There are two configuration properties, `enableRandomAppNamePrefix` and `appNamePrefix`. The `appNamePrefix` property defaults to `spring.application.name` if present, otherwise defaults to an empty string. An application can have four possible "name" combinations. For instance, the `time` application can have name combinations as shown in the table below. The name of the deployed application is defined via deployer's high level API, in this case it is 'time'. .Application Name |=== |appNamePrefix | enableRandomAppNamePrefix | application name |server |true |server-u7r9fhm-time | |true |u7r9fhm-time |server |false |server-time | |false |time |=== == Disable Push Task Applications If an application does not exist when the `TaskLauncher` `launch` method is invoked, the TaskLauncher by default pushes a task application using the `AppDeploymentRequest` `resource` property. The default behavior is disabled by setting `spring.cloud.deployer.cloudfoundry.push-task-apps-enabled` to `false`. In this case, the application must have already been pushed to CloudFoundry via an external process and the TaskLauncher simply starts it, providing any command line arguments defined in the deployment request. This is useful in cases in which the Cloud Foundry foundation is isolated from external code repositories. == Set Additional Environment Variables By default, the deployer adds global and application configuration properties to a single `SPRING_APPLICATION_JSON` environment variable entry in the application manifest. You can configure additional top-level environment variables in the manifest by setting `spring.cloud.deployer.cloudfoundry.env.=`. This is useful for adding https://github.com/cloudfoundry/java-buildpack[Java build pack configuration properties] to the application manifest since the Java build pack does not recognize `SPRING_APPLICATION_JSON`. ================================================ FILE: spring-cloud-deployer-cloudfoundry/pom.xml ================================================ 4.0.0 spring-cloud-deployer-cloudfoundry 3.0.0-SNAPSHOT jar Spring Cloud Deployer CloudFoundry org.springframework.cloud spring-cloud-deployer-parent 3.0.0-SNAPSHOT .. org.springframework.cloud spring-cloud-deployer-spi org.springframework.boot spring-boot-loader-classic org.springframework.boot spring-boot-starter-validation org.springframework.boot spring-boot-configuration-processor true org.cloudfoundry cloudfoundry-client-reactor javax.annotation javax.annotation-api org.cloudfoundry cloudfoundry-operations io.pivotal pivotal-cloudfoundry-client-reactor org.apache.commons commons-compress com.github.ben-manes.caffeine caffeine io.projectreactor.addons reactor-extra org.yaml snakeyaml org.springframework.retry spring-retry org.springframework.boot spring-boot-starter-test test org.springframework.cloud spring-cloud-deployer-spi-test ${project.version} test org.springframework.cloud spring-cloud-deployer-resource-docker test org.springframework.cloud spring-cloud-deployer-resource-support test org.springframework.cloud spring-cloud-deployer-autoconfigure test spring true spring-snapshots Spring Snapshots https://repo.spring.io/snapshot true spring-milestones Spring Milestones https://repo.spring.io/milestone false maven-central Maven Central https://repo.maven.apache.org/maven2 false spring-snapshots Spring Snapshots https://repo.spring.io/snapshot true spring-milestones Spring Milestones https://repo.spring.io/milestone false maven-central Maven Central https://repo.maven.apache.org/maven2 false ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/main/java/org/springframework/cloud/deployer/spi/cloudfoundry/AbstractCloudFoundryDeployer.java ================================================ /* * Copyright 2016-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.cloud.deployer.spi.cloudfoundry; import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.time.Duration; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.cloudfoundry.AbstractCloudFoundryException; import org.cloudfoundry.UnknownCloudFoundryException; import org.cloudfoundry.operations.services.BindServiceInstanceRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.Exceptions; import reactor.core.publisher.Mono; import reactor.retry.Retry; import org.springframework.cloud.deployer.spi.app.AppDeployer; import org.springframework.cloud.deployer.spi.app.AppScaleRequest; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; import org.springframework.cloud.deployer.spi.util.ByteSizeUtils; import org.springframework.core.io.Resource; import org.springframework.http.HttpStatus; import org.springframework.util.FileSystemUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** * Base class dealing with configuration overrides on a per-deployment basis, as well as common code for apps and tasks. * * @author Eric Bottard * @author Ilayaperumal Gopinathan * @author David Turanski */ class AbstractCloudFoundryDeployer { protected static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); protected final RuntimeEnvironmentInfo runtimeEnvironmentInfo; final CloudFoundryDeploymentProperties deploymentProperties; private final Logger logger = LoggerFactory.getLogger(AbstractCloudFoundryDeployer.class); AbstractCloudFoundryDeployer(CloudFoundryDeploymentProperties deploymentProperties, RuntimeEnvironmentInfo runtimeEnvironmentInfo) { this.deploymentProperties = deploymentProperties; this.runtimeEnvironmentInfo = runtimeEnvironmentInfo; } int memory(AppDeploymentRequest request) { String withUnit = request.getDeploymentProperties() .getOrDefault(AppDeployer.MEMORY_PROPERTY_KEY, this.deploymentProperties.getMemory()); return (int) ByteSizeUtils.parseToMebibytes(withUnit); } int memory(AppScaleRequest request) { if (request.getProperties().isPresent() && request.getProperties().get() != null) { return (int) ByteSizeUtils.parseToMebibytes(request.getProperties().get().getOrDefault(AppDeployer.MEMORY_PROPERTY_KEY, this.deploymentProperties.getMemory())); } return (int) ByteSizeUtils.parseToMebibytes(this.deploymentProperties.getMemory()); } int diskQuota(AppScaleRequest request) { if (request.getProperties().isPresent() && request.getProperties().get() != null) { return (int) ByteSizeUtils.parseToMebibytes(request.getProperties().get().getOrDefault(AppDeployer.DISK_PROPERTY_KEY, this.deploymentProperties.getDisk())); } return (int) ByteSizeUtils.parseToMebibytes(this.deploymentProperties.getDisk()); } Set servicesToBind(AppDeploymentRequest request) { Set services = this.deploymentProperties.getServices().stream().filter(s->!ServiceParser .getServiceParameters(s).isPresent()) .collect(Collectors.toSet()); Set requestServices = ServiceParser.splitServiceProperties(request.getDeploymentProperties().get (CloudFoundryDeploymentProperties.SERVICES_PROPERTY_KEY)) .stream() .filter(s-> !ServiceParser.getServiceParameters(s).isPresent()) .collect(Collectors.toSet()); services.addAll(requestServices); return services; } boolean includesServiceParameters(AppDeploymentRequest request) { return this.deploymentProperties.getServices().stream() .anyMatch(s -> ServiceParser.getServiceParameters(s).isPresent()) || ServiceParser .splitServiceProperties(request.getDeploymentProperties() .get(CloudFoundryDeploymentProperties.SERVICES_PROPERTY_KEY)) .stream().anyMatch(s -> ServiceParser.getServiceParameters(s).isPresent()); } Stream bindParameterizedServiceInstanceRequests(AppDeploymentRequest request, String deploymentId) { return ServiceParser.splitServiceProperties(request.getDeploymentProperties().get (CloudFoundryDeploymentProperties.SERVICES_PROPERTY_KEY)).stream() .filter(s-> ServiceParser.getServiceParameters(s).isPresent()) .map(s-> BindServiceInstanceRequest.builder() .applicationName(deploymentId) .serviceInstanceName(ServiceParser.getServiceInstanceName(s)) .parameters(ServiceParser.getServiceParameters(s).get()) .build() ); } int diskQuota(AppDeploymentRequest request) { String withUnit = request.getDeploymentProperties() .getOrDefault(AppDeployer.DISK_PROPERTY_KEY, this.deploymentProperties.getDisk()); return (int) ByteSizeUtils.parseToMebibytes(withUnit); } Set buildpacks(AppDeploymentRequest request) { // TODO: When 'buildpack' setting gets removed due to deprecation, // change this logic not ot fallback to it if 'buildpacks' // is used. String buidpacksValue = request.getDeploymentProperties() .get(CloudFoundryDeploymentProperties.BUILDPACKS_PROPERTY_KEY); String buidpackValue = request.getDeploymentProperties() .get(CloudFoundryDeploymentProperties.BUILDPACK_PROPERTY_KEY); if (buidpacksValue != null) { return StringUtils.commaDelimitedListToSet(buidpacksValue); } else if (buidpackValue != null) { return new HashSet<>(Arrays.asList(buidpackValue)); } else if (!ObjectUtils.isEmpty((this.deploymentProperties.getBuildpacks()))) { return this.deploymentProperties.getBuildpacks(); } else { return new HashSet<>(Arrays.asList(this.deploymentProperties.getBuildpack())); } } String javaOpts(AppDeploymentRequest request) { return Optional .ofNullable( request.getDeploymentProperties().get(CloudFoundryDeploymentProperties.JAVA_OPTS_PROPERTY_KEY)) .orElse(this.deploymentProperties.getJavaOpts()); } Predicate isNotFoundError() { return t -> t instanceof AbstractCloudFoundryException && ((AbstractCloudFoundryException) t).getStatusCode() == HttpStatus.NOT_FOUND.value(); } /** * Return a Docker image identifier if the application Resource is for a Docker image, or {@literal null} otherwise. * * @see #getApplication(AppDeploymentRequest) */ String getDockerImage(AppDeploymentRequest request) { try { String uri = request.getResource().getURI().toString(); if (uri.startsWith("docker:")) { return uri.substring("docker:".length()); } else { return null; } } catch (IOException e) { throw Exceptions.propagate(e); } } /** * Return a Path to the application Resource or {@literal null} if the request is for a Docker image. * * @see #getDockerImage(AppDeploymentRequest) */ Path getApplication(AppDeploymentRequest request) { try { logger.info( "Preparing to push an application from {}" + ". This may take some time if the artifact must be downloaded from a remote host.", request.getResource()); if (!request.getResource().getURI().toString().startsWith("docker:")) { return request.getResource().getFile().toPath(); } else { return null; } } catch (IOException e) { throw Exceptions.propagate(e); } } /** * Return a function usable in {@literal doOnError} constructs that will unwrap unrecognized Cloud Foundry Exceptions * and log the text payload. */ protected Consumer logError(String msg) { return e -> { if (e instanceof UnknownCloudFoundryException) { logger.error(msg + "\nUnknownCloudFoundryException encountered, whose payload follows:\n" + ((UnknownCloudFoundryException)e).getPayload(), e); } else { logger.error(msg, e); } }; } /** * To be used in order to retry the status operation for an application or task. * @param id The application id or the task id * @param The type of status object being queried for, usually AppStatus or TaskStatus * @return The function that executes the retry logic around for determining App or Task Status */ Function, Mono> statusRetry(String id) { long statusTimeout = this.deploymentProperties.getStatusTimeout(); long requestTimeout = Math.round(statusTimeout * 0.40); // wait 500ms with default status timeout of 2000ms long initialRetryDelay = Math.round(statusTimeout * 0.10); // wait 200ms with status timeout of 2000ms if (requestTimeout < 500L) { logger.info("Computed statusRetry Request timeout = {} ms is below 500ms minimum value. Setting to 500ms", requestTimeout); requestTimeout = 500L; } final long requestTimeoutToUse = requestTimeout; return m -> m.timeout(Duration.ofMillis(requestTimeoutToUse)) .doOnError(e -> { // show real exception if it wasn't timeout if (e instanceof TimeoutException) { logger.warn("Error getting status for {} within {}ms, Retrying operation.", id, requestTimeoutToUse); } else if (e instanceof UnknownCloudFoundryException) { logger.warn("Received UnknownCloudFoundryException from cf with payload={}", ((UnknownCloudFoundryException) e).getPayload()); } else { logger.warn("Received error from cf", e); } }) // let all other than timeout exception to propagate back to caller .retryWhen(reactor.util.retry.Retry.withThrowable(Retry.onlyIf(c -> { logger.debug("RetryContext for id {} iteration {} backoff {}", id, c.iteration(), c.backoff()); if (c.iteration() > 5) { logger.info("Stopping retry for id {} after {} iterations", id, c.iteration()); return false; } if (c.exception().getClass().getName().contains("org.cloudfoundry.client")) { // most likely real error which is not worth to retry return false; } // might be some netty error for not connected client, etc, retry return true; }) .exponentialBackoff(Duration.ofMillis(initialRetryDelay), Duration.ofMillis(statusTimeout)) .doOnRetry(c -> logger.debug("Retrying cf call for {}", id)))) .doOnError(TimeoutException.class, e -> { logger.error("Retry operation on getStatus failed for {}. Max retry time {}ms", id, statusTimeout); }); } /** * Always delete downloaded files for static http resources. Conditionally delete maven resources. * @param appDeploymentRequest */ protected void deleteLocalApplicationResourceFile(AppDeploymentRequest appDeploymentRequest) { try { Optional fileToDelete = fileToDelete(appDeploymentRequest.getResource()); if (fileToDelete.isPresent()) { File applicationFile = fileToDelete.get(); logger.info("Free Disk Space = {} bytes, Total Disk Space = {} bytes", applicationFile.getFreeSpace(), applicationFile.getTotalSpace()); boolean deleted = deleteFileOrDirectory(applicationFile); logger.info((deleted) ? "Successfully deleted the application resource: " + applicationFile.getCanonicalPath() : "Could not delete the application resource: " + applicationFile.getCanonicalPath()); } } catch(IOException e){ logger.warn("Exception deleting the application resource after successful CF push request." + " This could cause increase in disk space usage. Exception message: " + e.getMessage()); } } /* * Always delete files downloaded from http/s url. * Delete maven resources if property is set. */ private Optional fileToDelete(Resource resource) throws IOException { String scheme = resource.getURI().getScheme().toLowerCase(Locale.ROOT); if (scheme.startsWith("http")) { return Optional.of(resource.getFile()); } if (scheme.equals("maven") && deploymentProperties.isAutoDeleteMavenArtifacts()) { return Optional.of(resource.getFile().getParentFile()); } return Optional.empty(); } private boolean deleteFileOrDirectory(File fileToDelete) { boolean deleted; if (fileToDelete.isDirectory()) deleted = FileSystemUtils.deleteRecursively(fileToDelete); else { deleted = fileToDelete.delete(); } return deleted; } /* * Merge environment variables from global DeploymentProperties, application properties, and * environment variables declared in ApplicationDeploymentRequest */ protected Map mergeEnvironmentVariables(String deploymentId, AppDeploymentRequest request) { Map envVariables = new HashMap<>(deploymentProperties.getEnv()); envVariables.putAll(getApplicationProperties(deploymentId, request)); envVariables.putAll(getDeclaredEnvironmentVariables(request)); String javaOpts = javaOpts(request); if (StringUtils.hasText(javaOpts)) { envVariables.put("JAVA_OPTS", javaOpts(request)); } if (hasCfEnv(request.getResource())) { Map env = CfEnvConfigurer.disableJavaBuildPackAutoReconfiguration(envVariables); //Only append to existing spring profiles active env.putAll(CfEnvConfigurer.activateCloudProfile(env, null)); envVariables.putAll(env); } return envVariables; } private Map getDeclaredEnvironmentVariables(AppDeploymentRequest request) { Map env = new LinkedHashMap<>(); request.getDeploymentProperties().entrySet().stream() .filter(e -> e.getKey().startsWith(CloudFoundryDeploymentProperties.ENV_KEY + ".")) .forEach(e -> env.put(e.getKey().substring(CloudFoundryDeploymentProperties.ENV_KEY.length() + 1), e.getValue())); return env; } protected boolean hasCfEnv(Resource resource) { if (resource instanceof CfEnvAwareResource) { return ((CfEnvAwareResource)resource).hasCfEnv(); } return CfEnvAwareResource.of(resource).hasCfEnv(); } private Map getApplicationProperties(String deploymentId, AppDeploymentRequest request) { Map applicationProperties = getSanitizedApplicationProperties(deploymentId, request); if (!useSpringApplicationJson(request)) { return applicationProperties; } try { return Collections.singletonMap("SPRING_APPLICATION_JSON", OBJECT_MAPPER.writeValueAsString(applicationProperties)); } catch (JsonProcessingException e) { throw new RuntimeException(e); } } private Map getSanitizedApplicationProperties(String deploymentId, AppDeploymentRequest request) { Map applicationProperties = new HashMap<>(request.getDefinition().getProperties()); // Remove server.port as CF assigns a port for us, and we don't want to override that Optional.ofNullable(applicationProperties.remove("server.port")) .ifPresent(port -> logger.warn("Ignoring 'server.port={}' for app {}, as Cloud Foundry will assign a local dynamic port. Route to the app will use port 80.", port, deploymentId)); // Update active Spring Profiles given in application properties. Create a new entry with the given key if necessary if (hasCfEnv(request.getResource())) { applicationProperties = CfEnvConfigurer .activateCloudProfile(applicationProperties, CfEnvConfigurer.SPRING_PROFILES_ACTIVE_FQN); } return applicationProperties; } private boolean useSpringApplicationJson(AppDeploymentRequest request) { return Optional .ofNullable(request.getDeploymentProperties() .get(CloudFoundryDeploymentProperties.USE_SPRING_APPLICATION_JSON_KEY)) .map(Boolean::valueOf).orElse(this.deploymentProperties.isUseSpringApplicationJson()); } public RuntimeEnvironmentInfo environmentInfo() { return runtimeEnvironmentInfo; } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/main/java/org/springframework/cloud/deployer/spi/cloudfoundry/AbstractCloudFoundryTaskLauncher.java ================================================ /* * Copyright 2016-2023 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.cloud.deployer.spi.cloudfoundry; import java.time.Duration; import java.util.HashMap; import java.util.Map; import io.jsonwebtoken.lang.Assert; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.organizations.ListOrganizationsRequest; import org.cloudfoundry.client.v2.spaces.ListSpacesRequest; import org.cloudfoundry.client.v3.tasks.CancelTaskRequest; import org.cloudfoundry.client.v3.tasks.CancelTaskResponse; import org.cloudfoundry.client.v3.tasks.GetTaskRequest; import org.cloudfoundry.client.v3.tasks.GetTaskResponse; import org.cloudfoundry.client.v3.tasks.ListTasksRequest; import org.cloudfoundry.client.v3.tasks.TaskState; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; import org.springframework.cloud.deployer.spi.task.LaunchState; import org.springframework.cloud.deployer.spi.task.TaskLauncher; import org.springframework.cloud.deployer.spi.task.TaskStatus; /** * Abstract class to provide base functionality for launching Tasks on Cloud Foundry. This * class provides the base SPI for the {@link CloudFoundryTaskLauncher}. * * Does not override the default no-op implementation for * {@link TaskLauncher#cleanup(String)} and {@link TaskLauncher#destroy(String)}. */ abstract class AbstractCloudFoundryTaskLauncher extends AbstractCloudFoundryDeployer implements TaskLauncher { private static final Logger logger = LoggerFactory.getLogger(AbstractCloudFoundryTaskLauncher.class); private final CloudFoundryClient client; private final Mono organizationId; private final Mono spaceId; AbstractCloudFoundryTaskLauncher(CloudFoundryClient client, CloudFoundryDeploymentProperties deploymentProperties, RuntimeEnvironmentInfo runtimeEnvironmentInfo) { super(deploymentProperties, runtimeEnvironmentInfo); this.client = client; organizationId = organizationId(); spaceId = spaceId(); } /** * Setup a reactor flow to cancel a running task. This implementation opts to be * asynchronous. * * @param id the task's id to be canceled as returned from the * {@link TaskLauncher#launch(AppDeploymentRequest)} */ @Override public void cancel(String id) { requestCancelTask(id) .timeout(Duration.ofSeconds(this.deploymentProperties.getApiTimeout())) .doOnSuccess(r -> logger.info("Task {} cancellation successful", id)) .doOnError(logError(String.format("Task %s cancellation failed", id))) .subscribe(); } /** * Lookup the current status based on task id. * * @param id taskId as returned from the {@link TaskLauncher#launch(AppDeploymentRequest)} * @return the current task status */ @Override public TaskStatus status(String id) { try { return getStatus(id) .doOnSuccess(v -> logger.info("Successfully computed status [{}] for id={}", v, id)) .doOnError(logError(String.format("Failed to compute status for %s", id))) .block(Duration.ofMillis(this.deploymentProperties.getStatusTimeout())); } catch (Exception timeoutDueToBlock) { logger.error("Caught exception while querying for status of id={}", id, timeoutDueToBlock); return createErrorTaskStatus(id); } } @Override public int getRunningTaskExecutionCount() { Mono> orgAndSpace = Mono.zip(organizationId, spaceId); Mono listTasksRequest = orgAndSpace.map(tuple-> ListTasksRequest.builder() .state(TaskState.RUNNING) .organizationId(tuple.getT1()) .spaceId(tuple.getT2()) .build()); return listTasksRequest.flatMap(request-> this.client.tasks().list(request)) .map(listTasksResponse -> listTasksResponse.getPagination().getTotalResults()) .doOnError(logError("Failed to list running tasks")) .doOnSuccess(count -> logger.info(String.format("There are %d running tasks", count))) .block(Duration.ofMillis(this.deploymentProperties.getStatusTimeout())); } @Override public int getMaximumConcurrentTasks() { return this.deploymentProperties.getMaximumConcurrentTasks(); } protected boolean maxConcurrentExecutionsReached() { return this.getRunningTaskExecutionCount() >= this.getMaximumConcurrentTasks(); } private Mono getStatus(String id) { return requestGetTask(id) .map(this::toTaskStatus) .onErrorResume(isNotFoundError(), t -> { logger.debug("Task for id={} does not exist", id); return Mono.just(new TaskStatus(id, LaunchState.unknown, null)); }) .transform(statusRetry(id)) .onErrorReturn(createErrorTaskStatus(id)); } private TaskStatus createErrorTaskStatus(String id) { return new TaskStatus(id, LaunchState.error, null); } protected TaskStatus toTaskStatus(GetTaskResponse response) { Map attributes = new HashMap<>(); //retrieve CF-GUID for retrieving logs Assert.notNull(response.getTaskRelationships(), "response must contain task relationships."); Assert.notNull(response.getTaskRelationships().getApp(), "app in the taskRelationships of the response must not be null"); Assert.notNull(response.getTaskRelationships().getApp().getData(), "data in the app of the task relationships within the response must not be null"); attributes.put("app-cf-guid", response.getTaskRelationships().getApp().getData().getId()); switch (response.getState()) { case SUCCEEDED: return new TaskStatus(response.getId(), LaunchState.complete, attributes); case RUNNING: return new TaskStatus(response.getId(), LaunchState.running, attributes); case PENDING: return new TaskStatus(response.getId(), LaunchState.launching, attributes); case CANCELING: return new TaskStatus(response.getId(), LaunchState.cancelled, attributes); case FAILED: return new TaskStatus(response.getId(), LaunchState.failed, attributes); default: throw new IllegalStateException(String.format("Unsupported CF task state %s", response.getState())); } } private Mono requestCancelTask(String taskId) { return this.client.tasks() .cancel(CancelTaskRequest.builder() .taskId(taskId) .build()); } private Mono requestGetTask(String taskId) { return this.client.tasks() .get(GetTaskRequest.builder() .taskId(taskId) .build()); } private Mono organizationId() { String org = this.runtimeEnvironmentInfo.getPlatformSpecificInfo().get(CloudFoundryPlatformSpecificInfo.ORG); Assert.hasText(org,"Missing runtimeEnvironmentInfo : 'org' required."); ListOrganizationsRequest listOrganizationsRequest = ListOrganizationsRequest.builder() .name(org).build(); return this.client.organizations().list(listOrganizationsRequest) .doOnError(logError("Failed to list organizations")) .map(listOrganizationsResponse -> listOrganizationsResponse.getResources().get(0).getMetadata().getId()) .cache(aValue -> Duration.ofMillis(Long.MAX_VALUE), aValue -> Duration.ZERO, () -> Duration.ZERO); } private Mono spaceId() { String space = this.runtimeEnvironmentInfo.getPlatformSpecificInfo().get(CloudFoundryPlatformSpecificInfo.SPACE); Assert.hasText(space,"Missing runtimeEnvironmentInfo : 'space' required."); ListSpacesRequest listSpacesRequest = ListSpacesRequest.builder() .name(space).build(); return this.client.spaces().list(listSpacesRequest) .doOnError(logError("Failed to list spaces")) .map(listSpacesResponse -> listSpacesResponse.getResources().get(0).getMetadata().getId()) .cache(aValue -> Duration.ofMillis(Long.MAX_VALUE), aValue -> Duration.ZERO, () -> Duration.ZERO); } @Override public void cleanup(String id) { } @Override public void destroy(String appName) { } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/main/java/org/springframework/cloud/deployer/spi/cloudfoundry/AppNameGenerator.java ================================================ /* * Copyright 2016 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.cloud.deployer.spi.cloudfoundry; /** * Strategy interface for generating the names of deployed applications. * * * @author Soby Chacko */ public interface AppNameGenerator { /** * Generate an application name given a base name as the starting point. * * @param appName base application name * @return the generated app name */ String generateAppName(String appName); } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/main/java/org/springframework/cloud/deployer/spi/cloudfoundry/ApplicationLogAccessor.java ================================================ /* * Copyright 2023 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.cloud.deployer.spi.cloudfoundry; import org.cloudfoundry.logcache.v1.Envelope; import org.cloudfoundry.logcache.v1.Log; import org.cloudfoundry.logcache.v1.LogCacheClient; import org.cloudfoundry.logcache.v1.ReadRequest; import org.cloudfoundry.logcache.v1.ReadResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.Assert; import reactor.core.publisher.Flux; import java.time.Duration; import java.util.List; /** * Provide api access to retrieve logs for applications. * * @author Glenn Renfro * @author Chris Bono * * @since 2.9.3 */ public class ApplicationLogAccessor { private final static int MAX_LOG_LIMIT = 1000; private final static Logger logger = LoggerFactory.getLogger(ApplicationLogAccessor.class); private final LogCacheClient logCacheClient; public ApplicationLogAccessor(LogCacheClient logCacheClient) { Assert.notNull(logCacheClient, "logCacheClient must not be null"); this.logCacheClient = logCacheClient; } /** * Retrieve logs for specified deployment id. * @param deploymentId the deployment id of the application. * @param apiTimeout specify duration of the timeout for the api. * @return String containing the log information or empty string if no entries are available. */ public String getLog(String deploymentId, Duration apiTimeout) { logger.debug("Retrieving log for deploymentId:{} with apiTimeout:{}", deploymentId, apiTimeout); Assert.hasText(deploymentId, "id must have text and not null"); Assert.notNull(apiTimeout, "apiTimeout must not be null"); StringBuilder stringBuilder = new StringBuilder(); ReadRequest request = ReadRequest.builder().sourceId(deploymentId).limit(MAX_LOG_LIMIT).descending(true).build(); List logs = this.logCacheClient .read(request) .flatMapMany(this::responseToEnvelope) .collectList() .block(apiTimeout); // if no log exists the result set is null. if(logs == null) { return ""; } logs.forEach((log) -> { stringBuilder.append(log.getPayloadAsText()); stringBuilder.append(System.lineSeparator()); }); String [] lines = stringBuilder.toString().split("\n"); StringBuilder stringBuilderReconstruct = new StringBuilder(); for(int i = lines.length -1 ; i >= 0 ; i--) { stringBuilderReconstruct.append(lines[i]); if ( i > 0 ) { stringBuilderReconstruct.append("\n"); } } return stringBuilderReconstruct.toString(); } private Flux responseToEnvelope(ReadResponse response) { return Flux.fromIterable(response.getEnvelopes().getBatch()) .filter(envelope -> envelope.getLog() != null) .map(Envelope::getLog); } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/main/java/org/springframework/cloud/deployer/spi/cloudfoundry/CfEnvAwareAppDeploymentRequest.java ================================================ /* * Copyright 2020-2021 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.cloud.deployer.spi.cloudfoundry; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; /** * Copies an {@link AppDeploymentRequest} using a {@link CfEnvAwareResource}. * * @author David Turanski * @since 2.4 */ class CfEnvAwareAppDeploymentRequest extends AppDeploymentRequest { static CfEnvAwareAppDeploymentRequest of(AppDeploymentRequest appDeploymentRequest) { return new CfEnvAwareAppDeploymentRequest(appDeploymentRequest); } private CfEnvAwareAppDeploymentRequest(AppDeploymentRequest appDeploymentRequest) { super(appDeploymentRequest.getDefinition(), CfEnvAwareResource.of(appDeploymentRequest.getResource()), appDeploymentRequest.getDeploymentProperties(), appDeploymentRequest.getCommandlineArguments()); } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/main/java/org/springframework/cloud/deployer/spi/cloudfoundry/CfEnvAwareResource.java ================================================ /* * Copyright 2020-2024 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.cloud.deployer.spi.cloudfoundry; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URI; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Optional; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.boot.loader.archive.JarFileArchive; import org.springframework.core.io.Resource; /** * A {@link Resource} implementation that delegates to a resource and keeps the state of a CfEnv dependency * as an {@link Optional} which may be empty, true, or false. * * @author David Turanski * @since 2.4 */ class CfEnvAwareResource implements Resource { private final Resource resource; private final boolean hasCfEnv; static CfEnvAwareResource of(Resource resource) { return new CfEnvAwareResource(resource); } private CfEnvAwareResource(Resource resource) { this.resource = resource; this.hasCfEnv = CfEnvResolver.hasCfEnv(this); } @Override public boolean exists() { return resource.exists(); } @Override public URL getURL() throws IOException { return resource.getURL(); } @Override public URI getURI() throws IOException { return resource.getURI(); } @Override public File getFile() throws IOException { return resource.getFile(); } @Override public long contentLength() throws IOException { return resource.contentLength(); } @Override public long lastModified() throws IOException { return resource.lastModified(); } @Override public Resource createRelative(String s) throws IOException { return resource.createRelative(s); } @Override public String getFilename() { return resource.getFilename(); } @Override public String getDescription() { return resource.getDescription(); } @Override public InputStream getInputStream() throws IOException { return resource.getInputStream(); } boolean hasCfEnv() { return this.hasCfEnv; } /** * Inspect the {@link CfEnvAwareResource} to determine if it contains a dependency on io.pivotal.cfenv.core.CfEnv. * Cache the result in the resource. */ static class CfEnvResolver { private static Log logger = LogFactory.getLog(CfEnvResolver.class); private static final String CF_ENV = "io.pivotal.cfenv.core.CfEnv"; static boolean hasCfEnv(CfEnvAwareResource app ) { try { String scheme = app.getURI().getScheme().toLowerCase(Locale.ROOT); if (scheme.equals("docker")) { return false; } } catch (IOException e) { throw new IllegalArgumentException(e.getMessage(), e); } try { JarFileArchive archive = new JarFileArchive(app.getFile()); List urls = new ArrayList<>(); archive.getNestedArchives(entry -> entry.getName().endsWith(".jar"), null).forEachRemaining(a -> { try { urls.add(a.getUrl()); } catch (MalformedURLException e) { logger.error("Unable to process nested archive " + e.getMessage()); } }); URLClassLoader classLoader = new URLClassLoader(urls.toArray(new URL[urls.size()]), null); try { logger.info("Attempting to load class CFEnv"); Class.forName(CF_ENV, false, classLoader); return true; } catch (UnsupportedClassVersionError err) { logger.debug(app.getFilename() + " contains " + CF_ENV); // class found but can't load it i.e. because it's newer class version return true; } catch (ClassNotFoundException e) { logger.debug(app.getFilename() + " doesn't contain " + CF_ENV); return false; } } catch (Exception e) { logger.warn("Unable to determine dependencies for file " + app.getFilename()); } return false; } } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/main/java/org/springframework/cloud/deployer/spi/cloudfoundry/CfEnvConfigurer.java ================================================ /* * Copyright 2020-2021 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.cloud.deployer.spi.cloudfoundry; import java.util.HashMap; import java.util.Map; import java.util.stream.Stream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.util.StringUtils; /** * Provides methods to configure environment, application properties, and command line * args if the deployed artifact uses java-cfenv. * * @author David Turanski * @since 2.4 */ class CfEnvConfigurer { private static final Log log = LogFactory.getLog(CfEnvConfigurer.class); static final String SPRING_PROFILES_ACTIVE = "SPRING_PROFILES_ACTIVE"; static final String SPRING_PROFILES_ACTIVE_FQN = "spring.profiles.active"; static final String SPRING_PROFILES_ACTIVE_HYPHENATED = "spring-profiles-active"; static final String CLOUD_PROFILE_NAME = "cloud"; static final String JBP_CONFIG_SPRING_AUTO_RECONFIGURATION = "JBP_CONFIG_SPRING_AUTO_RECONFIGURATION"; static final String ENABLED_FALSE = "{ enabled: false }"; /** * Disable Java Buildpack Spring Auto-reconfiguration. * * @param environment a map containing environment variables * @return an copy of the map setting the environment variable needed to disable * auto-reconfiguration */ static Map disableJavaBuildPackAutoReconfiguration(Map environment) { log.debug("Disabling 'JBP_CONFIG_SPRING_AUTO_RECONFIGURATION'"); Map updatedEnvironment = new HashMap<>(environment); updatedEnvironment.putIfAbsent(JBP_CONFIG_SPRING_AUTO_RECONFIGURATION, ENABLED_FALSE); return updatedEnvironment; } /** * Activate the cloud profile. Add to a key that binds to * spring.profiles.active if one exists. * @param environment a map containing environment variables or application properties * @param keyToCreate create a new entry using a preferred key if none exists. * @return the updated map */ static Map activateCloudProfile(Map environment, String keyToCreate) { log.debug("Activating cloud profile"); Map updatedEnvironment = new HashMap<>(environment); if (appendToExistingEntry(updatedEnvironment, SPRING_PROFILES_ACTIVE, CLOUD_PROFILE_NAME)) { return updatedEnvironment; } else if (appendToExistingEntry(updatedEnvironment, SPRING_PROFILES_ACTIVE_FQN, CLOUD_PROFILE_NAME)) { return updatedEnvironment; } else if (appendToExistingEntry(updatedEnvironment, SPRING_PROFILES_ACTIVE_HYPHENATED, CLOUD_PROFILE_NAME)) { return updatedEnvironment; } // If Key provided, create new spring profiles active entry. if (StringUtils.hasText(keyToCreate)) { updatedEnvironment.put(keyToCreate, CLOUD_PROFILE_NAME); } return updatedEnvironment; } /** * Process a command line argument to append the cloud profile if it binds to * spring.profiles.active. * * @param arg the current value * @return the updated value */ static String appendCloudProfileToSpringProfilesActiveArg(String arg) { if ((arg.contains(SPRING_PROFILES_ACTIVE_FQN) || arg.contains(SPRING_PROFILES_ACTIVE_HYPHENATED) || arg.contains(SPRING_PROFILES_ACTIVE)) && arg.contains("=")) { String[] tokens = arg.split("="); arg = String.join("=", tokens[0], appendToValueIfPresent(tokens[1], CLOUD_PROFILE_NAME)); } return arg; } private static boolean appendToExistingEntry(Map environment, String key, String value) { if (environment.containsKey(key)) { String current = environment.get(key); environment.put(key, appendToValueIfPresent(current, value)); return true; } return false; } private static String appendToValueIfPresent(String current, String value) { if (StringUtils.hasText(current)) { if (!Stream.of(current.split(",")).filter(s -> s.trim().equals(value)).findFirst().isPresent()) { return current.join(",", current, value); } return current; } return value; } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/main/java/org/springframework/cloud/deployer/spi/cloudfoundry/CloudFoundryActuatorTemplate.java ================================================ /* * Copyright 2022 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.cloud.deployer.spi.cloudfoundry; import java.util.Optional; import org.springframework.cloud.deployer.spi.app.AbstractActuatorTemplate; import org.springframework.cloud.deployer.spi.app.AppAdmin; import org.springframework.cloud.deployer.spi.app.AppDeployer; import org.springframework.cloud.deployer.spi.app.AppInstanceStatus; import org.springframework.http.HttpHeaders; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; /** * Access the actuator endpoint for an app instance deployed to Cloud Foundry. * * @author David Turanski */ public class CloudFoundryActuatorTemplate extends AbstractActuatorTemplate { public CloudFoundryActuatorTemplate(RestTemplate restTemplate, AppDeployer appDeployer, AppAdmin appAdmin) { super(restTemplate, appDeployer, appAdmin); } @Override protected String actuatorUrlForInstance(AppInstanceStatus appInstanceStatus) { return UriComponentsBuilder.fromHttpUrl(appInstanceStatus.getAttributes().get("url")) .path("/actuator").toUriString(); } @Override public Optional httpHeadersForInstance(AppInstanceStatus appInstanceStatus) { HttpHeaders headers = new HttpHeaders(); headers.add("X-Cf-App-Instance", String.format("%s:%d", appInstanceStatus.getAttributes() .get(CloudFoundryAppInstanceStatus.CF_GUID), Integer.valueOf(appInstanceStatus.getAttributes().get(CloudFoundryAppInstanceStatus.INDEX)))); return Optional.of(headers); } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/main/java/org/springframework/cloud/deployer/spi/cloudfoundry/CloudFoundryAppDeployer.java ================================================ /* * Copyright 2016-2023 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.cloud.deployer.spi.cloudfoundry; import java.time.Duration; import java.util.Collections; import java.util.HashSet; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import org.cloudfoundry.client.v2.ClientV2Exception; import org.cloudfoundry.operations.CloudFoundryOperations; import org.cloudfoundry.operations.applications.ApplicationDetail; import org.cloudfoundry.operations.applications.ApplicationHealthCheck; import org.cloudfoundry.operations.applications.ApplicationManifest; import org.cloudfoundry.operations.applications.ApplicationSummary; import org.cloudfoundry.operations.applications.DeleteApplicationRequest; import org.cloudfoundry.operations.applications.Docker; import org.cloudfoundry.operations.applications.GetApplicationRequest; import org.cloudfoundry.operations.applications.InstanceDetail; import org.cloudfoundry.operations.applications.PushApplicationManifestRequest; import org.cloudfoundry.operations.applications.Route; import org.cloudfoundry.operations.applications.ScaleApplicationRequest; import org.cloudfoundry.operations.applications.StartApplicationRequest; import org.cloudfoundry.operations.services.BindServiceInstanceRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.yaml.snakeyaml.Yaml; import reactor.cache.CacheMono; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.Signal; import reactor.util.retry.Retry; import org.springframework.cloud.deployer.spi.app.AppDeployer; import org.springframework.cloud.deployer.spi.app.AppScaleRequest; import org.springframework.cloud.deployer.spi.app.AppStatus; import org.springframework.cloud.deployer.spi.app.DeploymentState; import org.springframework.cloud.deployer.spi.app.MultiStateAppDeployer; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; import org.springframework.util.StringUtils; /** * A deployer that targets Cloud Foundry using the public API. * * @author Eric Bottard * @author Greg Turnquist * @author Ben Hale * @author Ilayaperumal Gopinathan * @author David Turanski */ public class CloudFoundryAppDeployer extends AbstractCloudFoundryDeployer implements MultiStateAppDeployer { private static final Logger logger = LoggerFactory.getLogger(CloudFoundryAppDeployer.class); private static final String CF_GUID_ID = "cf-guid"; private final AppNameGenerator applicationNameGenerator; private final CloudFoundryOperations operations; private final Cache cache = Caffeine.newBuilder().expireAfterWrite(5, TimeUnit.SECONDS) .build(); private final ApplicationLogAccessor applicationLogAccessor; public CloudFoundryAppDeployer( AppNameGenerator applicationNameGenerator, CloudFoundryDeploymentProperties deploymentProperties, CloudFoundryOperations operations, RuntimeEnvironmentInfo runtimeEnvironmentInfo, ApplicationLogAccessor applicationLogAccessor ) { super(deploymentProperties, runtimeEnvironmentInfo); this.operations = operations; this.applicationNameGenerator = applicationNameGenerator; this.applicationLogAccessor = applicationLogAccessor; } @Override public String deploy(AppDeploymentRequest appDeploymentRequest) { final AppDeploymentRequest request = CfEnvAwareAppDeploymentRequest.of(appDeploymentRequest); logger.trace("Entered deploy: Deploying AppDeploymentRequest: AppDefinition = {}, Resource = {}, Deployment Properties = {}", request.getDefinition(), request.getResource(), request.getDeploymentProperties()); String deploymentId = deploymentId(request); logger.trace("deploy: Getting Status for Deployment Id = {}", deploymentId); getStatus(deploymentId) .doOnNext(status -> assertApplicationDoesNotExist(deploymentId, status)) // Need to block here to be able to throw exception early .block(Duration.ofSeconds(this.deploymentProperties.getApiTimeout())); logger.trace("deploy: Pushing application"); pushApplication(deploymentId, request) .timeout(Duration.ofSeconds(this.deploymentProperties.getApiTimeout())) .doOnSuccess(item -> { logger.info("Successfully deployed {}", deploymentId); }) .doOnError(error -> { if (isNotFoundError().test(error)) { logger.warn("Unable to deploy application. It may have been destroyed before start completed: " + error.getMessage()); } else { logError(String.format("Failed to deploy %s", deploymentId)).accept(error); } }) .doOnSuccess((c) -> { deleteLocalApplicationResourceFile(request); }).doOnError((c) -> { deleteLocalApplicationResourceFile(request); }) .subscribe(); logger.trace("Exiting deploy(). Deployment Id = {}", deploymentId); return deploymentId; } @Override public Map states(String... ids) { return requestSummary() .collect(Collectors.toMap(ApplicationSummary::getName, this::mapShallowAppState)) .block(); } @Override public Mono> statesReactive(String... ids) { return requestSummary() .collect(Collectors.toMap(ApplicationSummary::getName, this::mapShallowAppState)); } private DeploymentState mapShallowAppState(ApplicationSummary applicationSummary) { if (applicationSummary.getRunningInstances().equals(applicationSummary.getInstances())) { return DeploymentState.deployed; } else if (applicationSummary.getInstances() > 0) { return DeploymentState.partial; } else { return DeploymentState.undeployed; } } @Override protected Map mergeEnvironmentVariables(String deploymentId, AppDeploymentRequest request) { Map envVariables = super.mergeEnvironmentVariables(deploymentId, request); envVariables.putAll(getCommandLineArguments(request)); String group = request.getDeploymentProperties().get(AppDeployer.GROUP_PROPERTY_KEY); if (StringUtils.hasText(group)) { envVariables.put("SPRING_CLOUD_APPLICATION_GROUP", group); } envVariables.put("SPRING_CLOUD_APPLICATION_GUID", "${vcap.application.name}:${vcap.application.instance_index}"); envVariables.put("SPRING_APPLICATION_INDEX", "${vcap.application.instance_index}"); this.deploymentProperties.getAppAdmin().addCredentialsToAppEnvironment(envVariables); return envVariables; } private Map getCommandLineArguments(AppDeploymentRequest request) { if (request.getCommandlineArguments().isEmpty()) { return Collections.emptyMap(); } String argumentsAsString = request.getCommandlineArguments().stream() .collect(Collectors.joining(" ")); String yaml = new Yaml().dump(Collections.singletonMap("arguments", argumentsAsString)); return Collections.singletonMap("JBP_CONFIG_JAVA_MAIN", yaml); } @Override public AppStatus status(String id) { try { return getStatus(id) .doOnSuccess(v -> logger.info("Successfully computed status [{}] for {}", v, id)) .doOnError(logError(String.format("Failed to compute status for %s", id))) .block(Duration.ofMillis(this.deploymentProperties.getStatusTimeout())); } catch (Exception timeoutDueToBlock) { logger.error("Caught exception while querying for status of {}", id, timeoutDueToBlock); return createErrorAppStatus(id); } } @Override public Mono statusReactive(String id) { return getStatus(id); } @Override public void undeploy(String id) { getStatus(id) .doOnNext(status -> assertApplicationExists(id, status)) // Need to block here to be able to throw exception early .block(Duration.ofSeconds(this.deploymentProperties.getApiTimeout())); requestDeleteApplication(id) .timeout(Duration.ofSeconds(this.deploymentProperties.getApiTimeout())) .doOnSuccess(v -> logger.info("Successfully undeployed app {}", id)) .doOnError(logError(String.format("Failed to undeploy app %s", id))) .subscribe(); } @Override public String getLog(String id) { AppStatus status = status(id); String cfGuid = status.getInstances().values().stream() .map((appInstanceStatus) -> appInstanceStatus.getAttributes().get(CF_GUID_ID)) .filter(Objects::nonNull) .findFirst() .orElseThrow(() -> new IllegalArgumentException("Unable to find " + CF_GUID_ID)); return applicationLogAccessor.getLog(cfGuid, Duration.ofSeconds(this.deploymentProperties.getApiTimeout())); } @Override public void scale(AppScaleRequest appScaleRequest) { logger.info("Scaling the application instance using {}", appScaleRequest); ScaleApplicationRequest scaleApplicationRequest = ScaleApplicationRequest.builder() .name(appScaleRequest.getDeploymentId()) .instances(appScaleRequest.getCount()) .memoryLimit(memory(appScaleRequest)) .diskLimit(diskQuota(appScaleRequest)) .stagingTimeout(this.deploymentProperties.getStagingTimeout()) .startupTimeout(this.deploymentProperties.getStartupTimeout()) .build(); this.operations.applications().scale(scaleApplicationRequest) .timeout(Duration.ofSeconds(this.deploymentProperties.getApiTimeout())) .doOnSuccess(v -> logger.info("Scaled the application with deploymentId = {}", appScaleRequest.getDeploymentId())) .doOnError(e -> logger.error("Error: {} scaling the app instance {}", e.getMessage(), appScaleRequest.getDeploymentId())) .subscribe(); } private void assertApplicationDoesNotExist(String deploymentId, AppStatus status) { DeploymentState state = status.getState(); if (state != DeploymentState.unknown && state != DeploymentState.error) { throw new IllegalStateException(String.format("App %s is already deployed with state %s", deploymentId, state)); } } private void assertApplicationExists(String deploymentId, AppStatus status) { DeploymentState state = status.getState(); if (state == DeploymentState.unknown) { throw new IllegalStateException(String.format("App %s is not in a deployed state", deploymentId)); } } private AppStatus createAppStatus(ApplicationDetail applicationDetail, String deploymentId) { logger.trace("Gathering instances for " + applicationDetail); logger.trace("InstanceDetails: " + applicationDetail.getInstanceDetails()); AppStatus.Builder builder = AppStatus.of(deploymentId); int i = 0; for (InstanceDetail instanceDetail : applicationDetail.getInstanceDetails()) { builder.with(new CloudFoundryAppInstanceStatus(applicationDetail, instanceDetail, i++)); } for (; i < applicationDetail.getInstances(); i++) { builder.with(new CloudFoundryAppInstanceStatus(applicationDetail, null, i)); } return builder.build(); } private AppStatus createEmptyAppStatus(String deploymentId) { return AppStatus.of(deploymentId) .build(); } private AppStatus createErrorAppStatus(String deploymentId) { return AppStatus.of(deploymentId) .generalState(DeploymentState.error) .build(); } private String deploymentId(AppDeploymentRequest request) { String prefix = Optional.ofNullable(request.getDeploymentProperties().get(GROUP_PROPERTY_KEY)) .map(group -> String.format("%s-", group)) .orElse(""); String appName = String.format("%s%s", prefix, request.getDefinition().getName()); return this.applicationNameGenerator.generateAppName(appName); } private String domain(AppDeploymentRequest request) { return Optional .ofNullable(request.getDeploymentProperties().get(CloudFoundryDeploymentProperties.DOMAIN_PROPERTY)) .orElse(this.deploymentProperties.getDomain()); } private Mono getStatus(String deploymentId) { return requestGetApplication(deploymentId) .map(applicationDetail -> createAppStatus(applicationDetail, deploymentId)) .onErrorResume(IllegalArgumentException.class, t -> { logger.debug("Application for {} does not exist.", deploymentId); return Mono.just(createEmptyAppStatus(deploymentId)); }) .transform(statusRetry(deploymentId)) .onErrorReturn(createErrorAppStatus(deploymentId)); } private ApplicationHealthCheck healthCheck(AppDeploymentRequest request) { return Optional .ofNullable(request.getDeploymentProperties() .get(CloudFoundryDeploymentProperties.HEALTHCHECK_PROPERTY_KEY)) .map(this::toApplicationHealthCheck).orElse(this.deploymentProperties.getHealthCheck()); } private String healthCheckEndpoint(AppDeploymentRequest request) { return Optional .ofNullable(request.getDeploymentProperties() .get(CloudFoundryDeploymentProperties.HEALTHCHECK_HTTP_ENDPOINT_PROPERTY_KEY)) .orElse(this.deploymentProperties.getHealthCheckHttpEndpoint()); } private Integer healthCheckTimeout(AppDeploymentRequest request) { String timeoutString = request.getDeploymentProperties().getOrDefault( CloudFoundryDeploymentProperties.HEALTHCHECK_TIMEOUT_PROPERTY_KEY, this.deploymentProperties.getHealthCheckTimeout()); return Integer.parseInt(timeoutString); } private String host(AppDeploymentRequest request) { return Optional .ofNullable(request.getDeploymentProperties().get(CloudFoundryDeploymentProperties.HOST_PROPERTY)) .orElse(this.deploymentProperties.getHost()); } private int instances(AppDeploymentRequest request) { return Optional.ofNullable(request.getDeploymentProperties().get(AppDeployer.COUNT_PROPERTY_KEY)) .map(Integer::parseInt) .orElse(this.deploymentProperties.getInstances()); } private Mono pushApplication(String deploymentId, AppDeploymentRequest request) { ApplicationManifest.Builder manifest = ApplicationManifest.builder() .path(getApplication(request)) // Only one of the two is non-null .disk(diskQuota(request)) .environmentVariables(mergeEnvironmentVariables(deploymentId, request)) .healthCheckType(healthCheck(request)) .healthCheckHttpEndpoint(healthCheckEndpoint(request)) .timeout(healthCheckTimeout(request)) .instances(instances(request)) .memory(memory(request)) .name(deploymentId) .noRoute(toggleNoRoute(request)) .services(servicesToBind(request)); Optional.ofNullable(host(request)).ifPresent(manifest::host); Optional.ofNullable(domain(request)).ifPresent(manifest::domain); Optional.ofNullable(routePath(request)).ifPresent(manifest::routePath); if (route(request) != null) { manifest.route(Route.builder().route(route(request)).build()); } if (!routes(request).isEmpty()) { Set routes = routes(request).stream() .map(r -> Route.builder().route(r).build()) .collect(Collectors.toSet()); manifest.routes(routes); } if (getDockerImage(request) != null) { logger.info("Preparing to run a container from {}. This may take some time if the image must be downloaded from a remote container registry.", request.getResource()); manifest.docker(Docker.builder().image(getDockerImage(request)).build()); } else { manifest.buildpacks(buildpacks(request)); } if (!includesServiceParameters(request)) { return pushApplicationWithNoServiceParameters(manifest.build(), deploymentId); } else { return pushApplicationWithServiceParameters(manifest.build(), request, deploymentId); } } private Mono pushApplicationWithNoServiceParameters(ApplicationManifest manifest, String deploymentId) { logger.debug("Pushing application manifest"); return requestPushApplication(PushApplicationManifestRequest.builder() .manifest(manifest) .stagingTimeout(this.deploymentProperties.getStagingTimeout()) .startupTimeout(this.deploymentProperties.getStartupTimeout()) .build()) .doOnSuccess(v -> logger.info("Done uploading bits for {}", deploymentId)) .doOnError(e -> logger.error("Error: {} creating app {}", e.getMessage(), deploymentId)); } private Mono requestBind(BindServiceInstanceRequest bindRequest) { // defer so that new reques gets created when retry does re-sub return Mono.defer(() -> this.operations.services().bind(bindRequest)) .doOnError(e -> { if (e instanceof ClientV2Exception) { ClientV2Exception ce = (ClientV2Exception) e; if (ce.getCode() == 10001) { logger.warn("Retry service bind due to concurrency error"); } else { logger.warn("Received ClientV2Exception error from cf", ce); } } else { logger.warn("Received error from cf", e); } }) // check expected code indicating concurrency error, aka // CF-ConcurrencyError(10001): The service broker could not perform this operation in parallel with other running operations // and retry those and let other errors to pass and fail fast .retryWhen(Retry.withThrowable(reactor.retry.Retry.onlyIf(c -> { if (c.exception() instanceof ClientV2Exception) { ClientV2Exception e = (ClientV2Exception) c.exception(); if (e.getCode() == 10001) { return true; } } return false; }) // for now try 30 seconds and do some jitter to limit concurrency issues .timeout(Duration.ofSeconds(30)) .randomBackoff(Duration.ofSeconds(1), Duration.ofSeconds(5)) .doOnRetry(c -> logger.debug("Retrying cf call for {}", bindRequest)) )); } private Mono pushApplicationWithServiceParameters( ApplicationManifest manifest, AppDeploymentRequest request, String deploymentId ) { logger.debug("Pushing application manifest with no start"); return requestPushApplication(PushApplicationManifestRequest.builder() .manifest(manifest) .noStart(true) .build()) .doOnSuccess(v -> logger.info("Done uploading bits for {}", deploymentId)) .doOnError(e -> logger.error(String.format("Error creating app %s. Exception Message %s", deploymentId, e.getMessage()))) .thenMany(Flux.fromStream(bindParameterizedServiceInstanceRequests(request, deploymentId))) .flatMap(bindRequest -> this.requestBind(bindRequest) .doOnSuccess(bv -> logger.info("Done binding service {} for {}", bindRequest.getServiceInstanceName(), deploymentId)) .doOnError(e -> logger.error("Error: {} binding service {}", e.getMessage(), bindRequest.getServiceInstanceName()))) .then(this.operations.applications() .start(StartApplicationRequest.builder() .name(deploymentId) .stagingTimeout(this.deploymentProperties.getStagingTimeout()) .startupTimeout(this.deploymentProperties.getStartupTimeout()) .build()) .doOnSuccess(sv -> logger.info("Started app for {} ", deploymentId)) .doOnError(e -> logger.error("Error: {} starting app for {}.", e.getMessage(), deploymentId))) .doOnError(e -> logger.error(String.format("Error: %s creating app %s", e.getMessage(), deploymentId), e)); } private Mono requestDeleteApplication(String id) { return this.operations.applications() .delete(DeleteApplicationRequest.builder() .deleteRoutes(deploymentProperties.isDeleteRoutes()) .name(id) .build()); } private Mono requestGetApplication(String id) { return CacheMono .lookup(k -> Mono.defer(() -> { ApplicationDetail ifPresent = cache.getIfPresent(id); logger.debug("Cache get {}", ifPresent); return Mono.justOrEmpty(ifPresent).map(Signal::next); }), id) .onCacheMissResume(Mono.defer(() -> { logger.debug("Cache miss {}", id); return getApplicationDetail(id); })) .andWriteWith((k, sig) -> Mono.fromRunnable(() -> { ApplicationDetail ap = sig.get(); if (ap != null) { logger.debug("Cache put {} {}", k, ap); cache.put(k, ap); } })); } private Mono getApplicationDetail(String id) { return this.operations.applications() .get(GetApplicationRequest.builder() .name(id) .build()); } private Mono requestPushApplication(PushApplicationManifestRequest request) { return this.operations.applications() .pushManifest(request); } private Flux requestSummary() { return this.operations.applications().list(); } private String routePath(AppDeploymentRequest request) { String routePath = request.getDeploymentProperties().get(CloudFoundryDeploymentProperties.ROUTE_PATH_PROPERTY); if (StringUtils.hasText(routePath) && !routePath.startsWith("/")) { throw new IllegalArgumentException( "Cloud Foundry routes must start with \"/\". Route passed = [" + routePath + "]."); } return routePath; } private String route(AppDeploymentRequest request) { return request.getDeploymentProperties().get(CloudFoundryDeploymentProperties.ROUTE_PROPERTY); } private Set routes(AppDeploymentRequest request) { Set routes = new HashSet<>(); routes.addAll(this.deploymentProperties.getRoutes()); routes.addAll(StringUtils.commaDelimitedListToSet( request.getDeploymentProperties().get(CloudFoundryDeploymentProperties.ROUTES_PROPERTY))); return routes; } private ApplicationHealthCheck toApplicationHealthCheck(String raw) { try { return ApplicationHealthCheck.from(raw); } catch (IllegalArgumentException e) { throw new IllegalArgumentException(String.format("Unsupported health-check value '%s'. Available values are %s", raw, StringUtils.arrayToCommaDelimitedString(ApplicationHealthCheck.values())), e); } } private Boolean toggleNoRoute(AppDeploymentRequest request) { return Optional .ofNullable(request.getDeploymentProperties().get(CloudFoundryDeploymentProperties.NO_ROUTE_PROPERTY)) .map(Boolean::valueOf).orElse(null); } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/main/java/org/springframework/cloud/deployer/spi/cloudfoundry/CloudFoundryAppInstanceStatus.java ================================================ /* * Copyright 2016-2022 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.cloud.deployer.spi.cloudfoundry; import java.util.List; import java.util.Map; import java.util.TreeMap; import org.cloudfoundry.operations.applications.ApplicationDetail; import org.cloudfoundry.operations.applications.InstanceDetail; import org.springframework.cloud.deployer.spi.app.AppInstanceStatus; import org.springframework.cloud.deployer.spi.app.DeploymentState; /** * Maps status returned by the Cloud Foundry API to {@link AppInstanceStatus}. * * @author Eric Bottard * @author David Turanski */ public class CloudFoundryAppInstanceStatus implements AppInstanceStatus { private final InstanceDetail instanceDetail; private final ApplicationDetail applicationDetail; private final int index; private final Map attributes = new TreeMap<>(); /** * The deployer assigned unique id for each app instance. */ static final String GUID = "guid"; /** * The platform assigned guid - common among app instances with replicas */ static final String CF_GUID = "cf-guid"; /** * The app index. */ static final String INDEX = "index"; public CloudFoundryAppInstanceStatus(ApplicationDetail applicationDetail, InstanceDetail instanceDetail, int index) { this.applicationDetail = applicationDetail; this.instanceDetail = instanceDetail; this.index = index; } @Override public String getId() { return applicationDetail.getName() + "-" + index; } @Override public DeploymentState getState() { if (instanceDetail == null) { return DeploymentState.failed; } switch (instanceDetail.getState()) { case "STARTING": case "DOWN": return DeploymentState.deploying; case "CRASHED": return DeploymentState.failed; // Seems the client incorrectly reports apps as FLAPPING when they are // obviously fine. Mapping as RUNNING for now case "FLAPPING": case "RUNNING": return DeploymentState.deployed; case "UNKNOWN": return DeploymentState.unknown; default: throw new IllegalStateException("Unsupported CF state: " + instanceDetail.getState()); } } @Override public Map getAttributes() { if (instanceDetail != null) { if (instanceDetail.getCpu() != null) { attributes.put("metrics.machine.cpu", String.format("%.1f%%", instanceDetail.getCpu() * 100d)); } if (instanceDetail.getDiskQuota() != null && instanceDetail.getDiskUsage() != null) { attributes.put("metrics.machine.disk", String.format("%.1f%%", 100d * instanceDetail.getDiskUsage() / instanceDetail.getDiskQuota())); } if (instanceDetail.getMemoryQuota() != null && instanceDetail.getMemoryUsage() != null) { attributes.put("metrics.machine.memory", String.format("%.1f%%", 100d * instanceDetail.getMemoryUsage() / instanceDetail.getMemoryQuota())); } } List urls = applicationDetail.getUrls(); if (!urls.isEmpty()) { attributes.put("url", "http://" + urls.get(0)); for (int i = 0; i < urls.size() ; i++) { attributes.put("url." + i, "http://" + urls.get(i)); } } // TODO cf-java-client versions > 2.8 will have an index formally added ot InstanceDetail /* The deployer GUID must be unique for each app instance, the CloudFoundry GUID is common to all instances of the same app. */ attributes.put(GUID, applicationDetail.getName() + ":" + index); attributes.put(CF_GUID, applicationDetail.getId()); attributes.put(INDEX, String.valueOf(index)); return attributes; } @Override public String toString() { return String.format("%s[%s : %s]" , getClass().getSimpleName(), getId(), getState()); } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/main/java/org/springframework/cloud/deployer/spi/cloudfoundry/CloudFoundryAppNameGenerator.java ================================================ /* * Copyright 2016 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.cloud.deployer.spi.cloudfoundry; import java.util.Random; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.StringUtils; /** * CloudFoundry specific implementation of {@link AppNameGenerator}. This takes into account the * configuration properties {@code enableRandomAppNamePrefix} and {@code appNamePrefix}. * * If {@code enableRandomAppNamePrefix} a random short string is prefixed to the base name. * It is true by default. If {@code appNamePrefix} is set, it is also prefixed before the random string. * By default the {@code appNamePrefix} is the value of {@code spring.application.name} if available, * otherwise the empty string. * * As an example, if {@code enableRandomAppNamePrefix=true}, {@code spring.application.name=server}, * and we are deploying an application whose base name is {@code time}, then the deployed name on Cloud Foundry * may be {@code server-ug54dh5-time} * * @author Soby Chacko * @author Mark Pollack */ public class CloudFoundryAppNameGenerator implements AppNameGenerator, InitializingBean { private static final Log logger = LogFactory.getLog(CloudFoundryAppNameGenerator.class); /* Given that appnames are by default part of a hostname, limit to 63 chars max. */ private static final int MAX_APPNAME_LENGTH = 63; private String prefixToUse = ""; private final CloudFoundryDeploymentProperties properties; public CloudFoundryAppNameGenerator(CloudFoundryDeploymentProperties cloudFoundryDeploymentProperties) { this.properties = cloudFoundryDeploymentProperties; } @Override public void afterPropertiesSet() throws Exception { if (properties.isEnableRandomAppNamePrefix()) { prefixToUse = createUniquePrefix(); if (!StringUtils.isEmpty(properties.getAppNamePrefix())) { prefixToUse = String.format("%s-%s", properties.getAppNamePrefix(), prefixToUse); } } else { if (!StringUtils.isEmpty(properties.getAppNamePrefix())) { prefixToUse = properties.getAppNamePrefix(); } } logger.info(String.format("Prefix to be used for deploying apps: %s", prefixToUse)); } @Override public String generateAppName(String appName) { if (StringUtils.isEmpty(prefixToUse)) { return appName.substring(0, Math.min(MAX_APPNAME_LENGTH, appName.length())); } else { String string = String.format("%s-%s", prefixToUse, appName); return string.substring(0, Math.min(MAX_APPNAME_LENGTH, string.length())); } } private String createUniquePrefix() { String alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; char[] result = new char[7]; Random random = new Random(); for (int i = 0 ; i < result.length ; i++) { result[i] = alphabet.charAt(random.nextInt(alphabet.length())); } return new String(result); } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/main/java/org/springframework/cloud/deployer/spi/cloudfoundry/CloudFoundryConnectionProperties.java ================================================ /* * Copyright 2016-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.cloud.deployer.spi.cloudfoundry; import java.net.URL; import jakarta.validation.constraints.NotNull; import org.springframework.validation.annotation.Validated; /** * Holds configuration properties for connecting to a Cloud Foundry runtime. * * @author Eric Bottard * @author Greg Turnquist */ @Validated public class CloudFoundryConnectionProperties { /** * Top level prefix for Cloud Foundry related configuration properties. */ public static final String CLOUDFOUNDRY_PROPERTIES = "spring.cloud.deployer.cloudfoundry"; /** * The organization to use when registering new applications. */ @NotNull private String org; /** * The space to use when registering new applications. */ @NotNull private String space; /** * Location of the CloudFoundry REST API endpoint to use. */ @NotNull private URL url; /** * Username to use to authenticate against the Cloud Foundry API. */ @NotNull private String username; /** * Password to use to authenticate against the Cloud Foundry API. */ @NotNull private String password; /** * ClientId to use with token providers, effectively defaults to "cf" in * cloudfroundry client. */ private String clientId; /** * ClientSecret to use with token providers, effectively defaults to empty in * cloudfroundry client. */ private String clientSecret; /** * Indicates the identity provider to be used when accessing the Cloud Foundry API. * The passed string has to be a URL-Encoded JSON Object, containing the field origin with value as origin_key of an identity provider. */ private String loginHint; /** * Allow operation using self-signed certificates. */ private boolean skipSslValidation = false; public String getOrg() { return org; } public void setOrg(String org) { this.org = org; } public String getSpace() { return space; } public void setSpace(String space) { this.space = space; } public URL getUrl() { return url; } public void setUrl(URL url) { this.url = url; } public String getClientId() { return clientId; } public void setClientId(String clientId) { this.clientId = clientId; } public String getClientSecret() { return clientSecret; } public void setClientSecret(String clientSecret) { this.clientSecret = clientSecret; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public boolean isSkipSslValidation() { return skipSslValidation; } public void setSkipSslValidation(boolean skipSslValidation) { this.skipSslValidation = skipSslValidation; } public String getLoginHint() { return loginHint; } public void setLoginHint(String loginHint) { this.loginHint = loginHint; } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/main/java/org/springframework/cloud/deployer/spi/cloudfoundry/CloudFoundryDeployerAutoConfiguration.java ================================================ /* * Copyright 2016-2021 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.cloud.deployer.spi.cloudfoundry; import java.time.Duration; import com.github.zafarkhaja.semver.Version; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.info.GetInfoRequest; import org.cloudfoundry.logcache.v1.LogCacheClient; import org.cloudfoundry.operations.CloudFoundryOperations; import org.cloudfoundry.operations.DefaultCloudFoundryOperations; import org.cloudfoundry.reactor.ConnectionContext; import org.cloudfoundry.reactor.DefaultConnectionContext; import org.cloudfoundry.reactor.TokenProvider; import org.cloudfoundry.reactor.client.ReactorCloudFoundryClient; import org.cloudfoundry.reactor.logcache.v1.ReactorLogCacheClient; import org.cloudfoundry.reactor.tokenprovider.PasswordGrantTokenProvider; import org.cloudfoundry.reactor.tokenprovider.PasswordGrantTokenProvider.Builder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.AutoConfigureOrder; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationPropertiesBinding; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.deployer.spi.app.AppDeployer; import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; import org.springframework.cloud.deployer.spi.task.TaskLauncher; import org.springframework.cloud.deployer.spi.util.RuntimeVersionUtils; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.util.StringUtils; import org.springframework.web.client.RestTemplate; /** * Creates a {@link CloudFoundryAppDeployer} * * @author Eric Bottard * @author Ben Hale * @author David Turanski */ @Configuration @EnableConfigurationProperties @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) public class CloudFoundryDeployerAutoConfiguration { private static final Logger logger = LoggerFactory.getLogger(CloudFoundryDeployerAutoConfiguration.class); @Autowired private EarlyConnectionConfiguration connectionConfiguration; @Bean @ConditionalOnMissingBean public CloudFoundryOperations cloudFoundryOperations(CloudFoundryClient cloudFoundryClient, CloudFoundryConnectionProperties properties) { return DefaultCloudFoundryOperations.builder() .cloudFoundryClient(cloudFoundryClient) .organization(properties.getOrg()) .space(properties.getSpace()) .build(); } private RuntimeEnvironmentInfo runtimeEnvironmentInfo(Class spiClass, Class implementationClass, CloudFoundryConnectionProperties cloudFoundryConnectionProperties) { CloudFoundryClient client = connectionConfiguration.cloudFoundryClient( connectionConfiguration.connectionContext(cloudFoundryConnectionProperties), connectionConfiguration.tokenProvider(cloudFoundryConnectionProperties)); Version version = connectionConfiguration.version(client); return new CloudFoundryPlatformSpecificInfo(new RuntimeEnvironmentInfo.Builder()) .apiEndpoint(cloudFoundryConnectionProperties.getUrl().toString()) .org(cloudFoundryConnectionProperties.getOrg()) .space(cloudFoundryConnectionProperties.getSpace()) .builder() .implementationName(implementationClass.getSimpleName()) .spiClass(spiClass) .implementationVersion(RuntimeVersionUtils.getVersion(CloudFoundryAppDeployer.class)) .platformType("Cloud Foundry") .platformClientVersion(RuntimeVersionUtils.getVersion(client.getClass())) .platformApiVersion(version.toString()) .platformHostVersion("unknown") .build(); } @Bean @ConditionalOnMissingBean(AppDeployer.class) public AppDeployer appDeployer( CloudFoundryOperations operations, AppNameGenerator applicationNameGenerator, ApplicationLogAccessor applicationLogAccessor ) { return new CloudFoundryAppDeployer( applicationNameGenerator, connectionConfiguration.appDeploymentProperties(), operations, runtimeEnvironmentInfo(AppDeployer.class, CloudFoundryAppDeployer.class, connectionConfiguration.cloudFoundryConnectionProperties()), applicationLogAccessor); } @Bean CloudFoundryActuatorTemplate actuatorOperations(RestTemplate actuatorRestTemplate, AppDeployer appDeployer) { return new CloudFoundryActuatorTemplate(actuatorRestTemplate, appDeployer, connectionConfiguration.appDeploymentProperties().getAppAdmin()); } @Bean RestTemplate actuatorRestTemplate() { return new RestTemplate(); } @Bean @ConditionalOnMissingBean(AppNameGenerator.class) public AppNameGenerator appDeploymentCustomizer() { return new CloudFoundryAppNameGenerator(connectionConfiguration.appDeploymentProperties()); } @Bean @ConditionalOnMissingBean(TaskLauncher.class) public TaskLauncher taskLauncher( CloudFoundryClient client, CloudFoundryOperations operations, Version version, ApplicationLogAccessor applicationLogAccessor ) { if (version.greaterThanOrEqualTo(UnsupportedVersionTaskLauncher.MINIMUM_SUPPORTED_VERSION)) { RuntimeEnvironmentInfo runtimeEnvironmentInfo = runtimeEnvironmentInfo(TaskLauncher.class, CloudFoundryTaskLauncher.class, connectionConfiguration.cloudFoundryConnectionProperties()); return new CloudFoundryTaskLauncher( client, connectionConfiguration.taskDeploymentProperties(), operations, runtimeEnvironmentInfo, applicationLogAccessor); } else { RuntimeEnvironmentInfo runtimeEnvironmentInfo = runtimeEnvironmentInfo(TaskLauncher.class, UnsupportedVersionTaskLauncher.class, connectionConfiguration.cloudFoundryConnectionProperties()); return new UnsupportedVersionTaskLauncher(version, runtimeEnvironmentInfo); } } @Bean @ConditionalOnMissingBean(ApplicationLogAccessor.class) public ApplicationLogAccessor logHelper(LogCacheClient logCacheClient) { return new ApplicationLogAccessor(logCacheClient); } @Bean @ConfigurationPropertiesBinding public DurationConverter durationConverter() { return new DurationConverter(); } /** * A subset of configuration beans that can be used on its own to connect to the Cloud Controller API * and query it for its version. Automatically applied in CloudFoundryDeployerAutoConfiguration by virtue * of being a static inner class of it. * * @author Eric Bottard */ @Configuration @EnableConfigurationProperties public static class EarlyConnectionConfiguration { @Bean @ConditionalOnMissingBean(name = "appDeploymentProperties") public CloudFoundryDeploymentProperties appDeploymentProperties() { return defaultSharedDeploymentProperties(); } @Bean @ConditionalOnMissingBean(name = "taskDeploymentProperties") public CloudFoundryDeploymentProperties taskDeploymentProperties() { return defaultSharedDeploymentProperties(); } @Bean @ConfigurationProperties(prefix = CloudFoundryConnectionProperties.CLOUDFOUNDRY_PROPERTIES) public CloudFoundryDeploymentProperties defaultSharedDeploymentProperties() { return new CloudFoundryDeploymentProperties(); } @Bean @ConditionalOnMissingBean public Version version(CloudFoundryClient client) { return client.info() .get(GetInfoRequest.builder() .build()) .map(response -> Version.valueOf(response.getApiVersion())) .doOnError(e -> { throw new RuntimeException("Bad credentials connecting to Cloud Foundry.", e); }) .doOnNext(version -> logger.info("Connecting to Cloud Foundry with API Version {}", version)) .block(Duration.ofSeconds(appDeploymentProperties().getApiTimeout())); } @Bean @ConditionalOnMissingBean public CloudFoundryClient cloudFoundryClient(ConnectionContext connectionContext, TokenProvider tokenProvider) { return ReactorCloudFoundryClient.builder() .connectionContext(connectionContext) .tokenProvider(tokenProvider) .build(); } @Bean @ConditionalOnMissingBean public LogCacheClient logCacheClient(ConnectionContext connectionContext, TokenProvider tokenProvider) { return ReactorLogCacheClient.builder() .connectionContext(connectionContext) .tokenProvider(tokenProvider) .build(); } @Bean @ConditionalOnMissingBean public TokenProvider tokenProvider(CloudFoundryConnectionProperties properties) { Builder tokenProviderBuilder = PasswordGrantTokenProvider.builder() .username(properties.getUsername()) .password(properties.getPassword()) .loginHint(properties.getLoginHint()); if (StringUtils.hasText(properties.getClientId())) { tokenProviderBuilder.clientId(properties.getClientId()); } if (StringUtils.hasText(properties.getClientSecret())) { tokenProviderBuilder.clientSecret(properties.getClientSecret()); } return tokenProviderBuilder.build(); } @Bean @ConditionalOnMissingBean @ConfigurationProperties(prefix = CloudFoundryConnectionProperties.CLOUDFOUNDRY_PROPERTIES) public CloudFoundryConnectionProperties cloudFoundryConnectionProperties() { return new CloudFoundryConnectionProperties(); } @Bean @ConditionalOnMissingBean public ConnectionContext connectionContext(CloudFoundryConnectionProperties properties) { return DefaultConnectionContext.builder() .apiHost(properties.getUrl().getHost()) .skipSslValidation(properties.isSkipSslValidation()) .build(); } } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/main/java/org/springframework/cloud/deployer/spi/cloudfoundry/CloudFoundryDeploymentProperties.java ================================================ /* * Copyright 2016-2021 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.cloud.deployer.spi.cloudfoundry; import java.time.Duration; import java.util.Collections; import java.util.HashSet; import java.util.Map; import java.util.Optional; import java.util.Set; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.cloudfoundry.operations.applications.ApplicationHealthCheck; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.deployer.spi.app.AppAdmin; import org.springframework.validation.annotation.Validated; /** * Holds configuration properties for specifying what resources and services an app * deployed to a Cloud Foundry runtime will get. * * @author Eric Bottard * @author Greg Turnquist * @author Ilayaperumal Gopinathan * @author David Turanski */ @Validated public class CloudFoundryDeploymentProperties { private static final Log log = LogFactory.getLog(CloudFoundryDeploymentProperties.class); public static final String SERVICES_PROPERTY_KEY = CloudFoundryConnectionProperties.CLOUDFOUNDRY_PROPERTIES + ".services"; public static final String HEALTHCHECK_PROPERTY_KEY = CloudFoundryConnectionProperties.CLOUDFOUNDRY_PROPERTIES + ".health-check"; public static final String HEALTHCHECK_HTTP_ENDPOINT_PROPERTY_KEY = CloudFoundryConnectionProperties.CLOUDFOUNDRY_PROPERTIES + ".health-check-http-endpoint"; public static final String HEALTHCHECK_TIMEOUT_PROPERTY_KEY = CloudFoundryConnectionProperties.CLOUDFOUNDRY_PROPERTIES + ".health-check-timeout"; public static final String ROUTE_PATH_PROPERTY = CloudFoundryConnectionProperties.CLOUDFOUNDRY_PROPERTIES + ".route-path"; public static final String ROUTE_PROPERTY = CloudFoundryConnectionProperties.CLOUDFOUNDRY_PROPERTIES + ".route"; public static final String ROUTES_PROPERTY = CloudFoundryConnectionProperties.CLOUDFOUNDRY_PROPERTIES + ".routes"; public static final String NO_ROUTE_PROPERTY = CloudFoundryConnectionProperties.CLOUDFOUNDRY_PROPERTIES + ".no-route"; public static final String HOST_PROPERTY = CloudFoundryConnectionProperties.CLOUDFOUNDRY_PROPERTIES + ".host"; public static final String DOMAIN_PROPERTY = CloudFoundryConnectionProperties.CLOUDFOUNDRY_PROPERTIES + ".domain"; public static final String BUILDPACK_PROPERTY_KEY = CloudFoundryConnectionProperties.CLOUDFOUNDRY_PROPERTIES + ".buildpack"; public static final String BUILDPACKS_PROPERTY_KEY = CloudFoundryConnectionProperties.CLOUDFOUNDRY_PROPERTIES + ".buildpacks"; public static final String JAVA_OPTS_PROPERTY_KEY = CloudFoundryConnectionProperties.CLOUDFOUNDRY_PROPERTIES + ".javaOpts"; public static final String USE_SPRING_APPLICATION_JSON_KEY = CloudFoundryConnectionProperties.CLOUDFOUNDRY_PROPERTIES + ".use-spring-application-json"; public static final String ENV_KEY = CloudFoundryConnectionProperties.CLOUDFOUNDRY_PROPERTIES + ".env"; private static final String DEFAULT_BUILDPACK = "https://github.com/cloudfoundry/java-buildpack.git#v4.29.1"; /** * The names of services to bind to all applications deployed as a module. This should * typically contain a service capable of playing the role of a binding transport. */ private Set services = new HashSet<>(); /** * The host name to use as part of the route. Defaults to hostname derived by Cloud * Foundry. */ private String host = null; /** * The domain to use when mapping routes for applications. */ private String domain; /** * The routes that the application should be bound to. Mutually exclusive with host and * domain. */ private Set routes = new HashSet<>(); /** * The buildpack to use for deploying the application. */ @Deprecated private String buildpack = DEFAULT_BUILDPACK; /** * The buildpacks to use for deploying the application. */ private Set buildpacks = new HashSet<>(); /** * The amount of memory to allocate, if not overridden per-app. Default unit is mebibytes, * 'M' and 'G" suffixes supported. */ private String memory = "1024m"; /** * The amount of disk space to allocate, if not overridden per-app. Default unit is * mebibytes, 'M' and 'G" suffixes supported. */ private String disk = "1024m"; /** * The type of health check to perform on deployed application, if not overridden per-app. * Defaults to PORT */ private ApplicationHealthCheck healthCheck = ApplicationHealthCheck.PORT; /** * The path that the http health check will use, defaults to @{code /health} */ private String healthCheckHttpEndpoint = "/health"; /** * The timeout value for health checks in seconds. Defaults to 120 seconds. */ private String healthCheckTimeout = "120"; /** * The number of instances to run. */ private int instances = 1; /** * Flag to enable prefixing the app name with a random prefix. */ private boolean enableRandomAppNamePrefix = true; /** * Timeout for blocking API calls, in seconds. */ private long apiTimeout = 360L; /** * Timeout for status API operations in milliseconds */ private long statusTimeout = 30_000L; /** * Flag to indicate whether application properties are fed into SPRING_APPLICATION_JSON or * ENVIRONMENT VARIABLES. */ private boolean useSpringApplicationJson = true; /** * If set, override the timeout allocated for staging the app by the client. */ private Duration stagingTimeout = Duration.ofMinutes(15L); /** * If set, override the timeout allocated for starting the app by the client. */ private Duration startupTimeout = Duration.ofMinutes(5L); /** * String to use as prefix for name of deployed app. Defaults to spring.application.name. */ @Value("${spring.application.name:}") private String appNamePrefix; /** * Whether to also delete routes when un-deploying an application. */ private boolean deleteRoutes = true; /** * Whether to push task apps */ private boolean pushTaskAppsEnabled = true; /** * Whether to automatically delete cached Maven artifacts after deployment. */ private boolean autoDeleteMavenArtifacts = true; /** * The maximum concurrent tasks allowed. */ @Min(1) private int maximumConcurrentTasks = 20; private String javaOpts; private Optional> env = Optional.empty(); /** * Location of the PCF scheduler REST API enpoint ot use. */ private String schedulerUrl; /** * The number of retries allowed when scheduling a task if an {@link javax.net.ssl.SSLException} is thrown. */ private int scheduleSSLRetryCount = 5; /** * The number of seconds to wait for a unSchedule to complete. */ private int unScheduleTimeoutInSeconds = 30; /** * The number of seconds to wait for a schedule to complete. * This excludes the time it takes to stage the application on Cloud Foundry. */ private int scheduleTimeoutInSeconds = 30; /** * The number of seconds to wait for a list of schedules to be returned. */ private int listTimeoutInSeconds = 60; private AppAdmin appAdmin = new AppAdmin(); public Set getServices() { return services; } public void setServices(Set services) { this.services = services; } @Deprecated public String getBuildpack() { return buildpack; } @Deprecated public void setBuildpack(String buildpack) { this.buildpack = buildpack; } public Set getBuildpacks() { return buildpacks; } public void setBuildpacks(Set buildpacks) { this.buildpacks = buildpacks; } public String getMemory() { return memory; } public void setMemory(String memory) { this.memory = memory; } public String getDisk() { return disk; } public void setDisk(String disk) { this.disk = disk; } public int getInstances() { return instances; } public void setInstances(int instances) { this.instances = instances; } public boolean isEnableRandomAppNamePrefix() { return enableRandomAppNamePrefix; } public void setEnableRandomAppNamePrefix(boolean enableRandomAppNamePrefix) { this.enableRandomAppNamePrefix = enableRandomAppNamePrefix; } public String getAppNamePrefix() { return appNamePrefix; } public void setAppNamePrefix(String appNamePrefix) { this.appNamePrefix = appNamePrefix; } public long getApiTimeout() { return apiTimeout; } public void setApiTimeout(long apiTimeout) { this.apiTimeout = apiTimeout; } public boolean isUseSpringApplicationJson() { return useSpringApplicationJson; } public void setUseSpringApplicationJson(boolean useSpringApplicationJson) { this.useSpringApplicationJson = useSpringApplicationJson; } public ApplicationHealthCheck getHealthCheck() { return healthCheck; } public void setHealthCheck(ApplicationHealthCheck healthCheck) { this.healthCheck = healthCheck; } public String getHealthCheckHttpEndpoint() { return healthCheckHttpEndpoint; } public void setHealthCheckHttpEndpoint(String healthCheckHttpEndpoint) { this.healthCheckHttpEndpoint = healthCheckHttpEndpoint; } public String getHealthCheckTimeout() { return healthCheckTimeout; } public void setHealthCheckTimeout(String healthCheckTimeout) { this.healthCheckTimeout = healthCheckTimeout; } public String getDomain() { return domain; } public void setDomain(String domain) { this.domain = domain; } public String getHost() { return host; } public void setHost(String host) { this.host = host; } public Set getRoutes() { return routes; } public void setRoutes(Set routes) { this.routes = routes; } public Duration getStagingTimeout() { return stagingTimeout; } public void setStagingTimeout(Duration stagingTimeout) { this.stagingTimeout = stagingTimeout; } public Duration getStartupTimeout() { return startupTimeout; } public void setStartupTimeout(Duration startupTimeout) { this.startupTimeout = startupTimeout; } public long getStatusTimeout() { return statusTimeout; } public void setStatusTimeout(long statusTimeout) { this.statusTimeout = statusTimeout; } public boolean isDeleteRoutes() { return deleteRoutes; } public void setDeleteRoutes(boolean deleteRoutes) { this.deleteRoutes = deleteRoutes; } public String getJavaOpts() { return javaOpts; } public void setJavaOpts(String javaOpts) { this.javaOpts = javaOpts; } public int getMaximumConcurrentTasks() { return maximumConcurrentTasks; } public void setMaximumConcurrentTasks(int maximumConcurrentTasks) { this.maximumConcurrentTasks = maximumConcurrentTasks; } public boolean isPushTaskAppsEnabled() { return pushTaskAppsEnabled; } public void setPushTaskAppsEnabled(boolean pushTaskAppsEnabled) { this.pushTaskAppsEnabled = pushTaskAppsEnabled; } public boolean isAutoDeleteMavenArtifacts() { return autoDeleteMavenArtifacts; } public void setAutoDeleteMavenArtifacts(boolean autoDeleteMavenArtifacts) { this.autoDeleteMavenArtifacts = autoDeleteMavenArtifacts; } public Map getEnv() { return env.orElseGet(Collections::emptyMap); } public void setEnv(@NotNull Map env) { this.env.map(e -> { log.error("Environment is immutable. New entries have not been applied"); return this.env; } ).orElse(this.env = Optional.of(Collections.unmodifiableMap(env))); } public String getSchedulerUrl() { return schedulerUrl; } public void setSchedulerUrl(String schedulerUrl) { this.schedulerUrl = schedulerUrl; } public int getScheduleSSLRetryCount() { return scheduleSSLRetryCount; } public void setScheduleSSLRetryCount(int scheduleSSLRetryCount) { this.scheduleSSLRetryCount = scheduleSSLRetryCount; } public int getUnScheduleTimeoutInSeconds() { return unScheduleTimeoutInSeconds; } public void setUnScheduleTimeoutInSeconds(int unScheduleTimeoutInSeconds) { this.unScheduleTimeoutInSeconds = unScheduleTimeoutInSeconds; } public int getScheduleTimeoutInSeconds() { return scheduleTimeoutInSeconds; } public void setScheduleTimeoutInSeconds(int scheduleTimeoutInSeconds) { this.scheduleTimeoutInSeconds = scheduleTimeoutInSeconds; } public int getListTimeoutInSeconds() { return listTimeoutInSeconds; } public void setListTimeoutInSeconds(int listTimeoutInSeconds) { this.listTimeoutInSeconds = listTimeoutInSeconds; } public AppAdmin getAppAdmin() { return appAdmin; } public void setAppAdmin(AppAdmin appAdmin) { this.appAdmin = appAdmin; } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/main/java/org/springframework/cloud/deployer/spi/cloudfoundry/CloudFoundryPlatformSpecificInfo.java ================================================ /* * Copyright 2020-2021 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.cloud.deployer.spi.cloudfoundry; import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; import org.springframework.util.Assert; /** * A Provides required platform specific values to {@link RuntimeEnvironmentInfo}. * * @author David Turanski */ public class CloudFoundryPlatformSpecificInfo { static final String API_ENDPOINT = "API Endpoint"; static final String ORG = "Organization"; static final String SPACE = "Space"; private final RuntimeEnvironmentInfo.Builder runtimeEnvironmentInfo; private String apiEndpoint; private String org; private String space; public CloudFoundryPlatformSpecificInfo(RuntimeEnvironmentInfo.Builder runtimeEnvironmentInfo) { this.runtimeEnvironmentInfo = runtimeEnvironmentInfo; } public CloudFoundryPlatformSpecificInfo apiEndpoint(String apiEndpoint) { this.apiEndpoint = apiEndpoint; return this; } public CloudFoundryPlatformSpecificInfo org(String org) { this.org = org; return this; } public CloudFoundryPlatformSpecificInfo space(String space) { this.space = space; return this; } public RuntimeEnvironmentInfo.Builder builder() { Assert.hasText(apiEndpoint, "'apiEndpoint' must contain text"); Assert.hasText(org, "'org' must contain text"); Assert.hasText(space, "'space' must contain text"); runtimeEnvironmentInfo.addPlatformSpecificInfo(API_ENDPOINT, apiEndpoint); runtimeEnvironmentInfo.addPlatformSpecificInfo(ORG, org); runtimeEnvironmentInfo.addPlatformSpecificInfo(SPACE, space); return runtimeEnvironmentInfo; } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/main/java/org/springframework/cloud/deployer/spi/cloudfoundry/CloudFoundryTaskLauncher.java ================================================ /* * Copyright 2016-2023 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.cloud.deployer.spi.cloudfoundry; import java.time.Duration; import java.util.NoSuchElementException; import java.util.stream.Collectors; import java.util.stream.Stream; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.applications.SummaryApplicationResponse; import org.cloudfoundry.client.v3.tasks.CreateTaskRequest; import org.cloudfoundry.client.v3.tasks.CreateTaskResponse; import org.cloudfoundry.operations.CloudFoundryOperations; import org.cloudfoundry.operations.applications.AbstractApplicationSummary; import org.cloudfoundry.operations.applications.ApplicationDetail; import org.cloudfoundry.operations.applications.ApplicationHealthCheck; import org.cloudfoundry.operations.applications.ApplicationManifest; import org.cloudfoundry.operations.applications.ApplicationSummary; import org.cloudfoundry.operations.applications.DeleteApplicationRequest; import org.cloudfoundry.operations.applications.Docker; import org.cloudfoundry.operations.applications.GetApplicationRequest; import org.cloudfoundry.operations.applications.PushApplicationManifestRequest; import org.cloudfoundry.operations.applications.StopApplicationRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.deployer.spi.task.TaskStatus; import org.springframework.util.Assert; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; import org.springframework.cloud.deployer.spi.task.TaskLauncher; /** * {@link TaskLauncher} implementation for CloudFoundry. When a task is launched, if it has not previously been * deployed, the app is created, the package is uploaded, and the droplet is created before launching the actual * task. If the app has been deployed previously, the app/package/droplet is reused and a new task is created. * * @author Greg Turnquist * @author Michael Minella * @author Ben Hale * @author Ilayaperumal Gopinathan * @author Glenn Renfro * @author David Turanski * @author David Bernard */ public class CloudFoundryTaskLauncher extends AbstractCloudFoundryTaskLauncher { private static final Logger logger = LoggerFactory.getLogger(CloudFoundryTaskLauncher.class); private final CloudFoundryClient client; private final CloudFoundryDeploymentProperties deploymentProperties; private final CloudFoundryOperations operations; private final ApplicationLogAccessor applicationLogAccessor; public CloudFoundryTaskLauncher(CloudFoundryClient client, CloudFoundryDeploymentProperties deploymentProperties, CloudFoundryOperations operations, RuntimeEnvironmentInfo runtimeEnvironmentInfo, ApplicationLogAccessor applicationLogAccessor) { super(client, deploymentProperties, runtimeEnvironmentInfo); this.client = client; this.deploymentProperties = deploymentProperties; this.operations = operations; this.applicationLogAccessor = applicationLogAccessor; } /** * Set up a reactor flow to launch a task. Before launch, check if the base application exists. If not, deploy * then launch task. * * @param appDeploymentRequest description of the application to be launched * @return name of the launched task, returned without waiting for reactor pipeline to complete */ @Override public String launch(AppDeploymentRequest appDeploymentRequest) { final AppDeploymentRequest request = CfEnvAwareAppDeploymentRequest.of(appDeploymentRequest); if (this.maxConcurrentExecutionsReached()) { throw new IllegalStateException( String.format("Cannot launch task %s. The maximum concurrent task executions is at its limit [%d].", request.getDefinition().getName(), this.getMaximumConcurrentTasks()) ); } return getOrDeployApplication(request) .flatMap(application -> launchTask(application, request)) .doOnSuccess(r -> { logger.info("Task {} launch successful", request.getDefinition().getName()); }) .doOnError(logError(String.format("Task %s launch failed", request.getDefinition().getName()))) .doOnTerminate(() -> { if (pushTaskAppsEnabled()) { deleteLocalApplicationResourceFile(request); } }) .block(Duration.ofSeconds(this.deploymentProperties.getApiTimeout())); } @Override public void destroy(String appName) { if (!pushTaskAppsEnabled()) { logger.warn("The application {} will not be deleted since 'pushTaskApps' is not enabled.", appName); return; } requestDeleteApplication(appName) .timeout(Duration.ofSeconds(this.deploymentProperties.getApiTimeout())) .doOnSuccess(v -> logger.info("Successfully destroyed app {}", appName)) .doOnError(logError(String.format("Failed to destroy app %s", appName))) .block(Duration.ofSeconds(this.deploymentProperties.getApiTimeout())); } @Override public String getLog(String id) { TaskStatus taskStatus = this.status(id); String appCfGuid = taskStatus.getAttributes().get("app-cf-guid"); logger.debug("Retrieving log for app-guid-id {} for task-guid-id {}", appCfGuid, id); Assert.hasText(appCfGuid, "could not find a GUID app id for the task guid id " + id); return this.applicationLogAccessor.getLog(appCfGuid, Duration.ofSeconds(this.deploymentProperties.getApiTimeout())); } /** * Set up a reactor flow to stage a task. Before staging check if the base * application exists. If not, then stage it. * * @param request description of the application to be staged. * @return SummaryApplicationResponse containing the status of the staging. */ public SummaryApplicationResponse stage(AppDeploymentRequest request) { return getOrDeployApplication(request).doOnSuccess(r -> logger.info("Task {} staged successfully", request.getDefinition().getName())) .doOnError(logError(String.format("Task %s stage failed", request.getDefinition().getName()))) .block(Duration.ofSeconds(this.deploymentProperties.getApiTimeout())); } /** * Creates the command string required to launch a task by a service on Cloud Foundry. * @param application the {@link SummaryApplicationResponse} containing the result of the requested staging. * @param request The {@link AppDeploymentRequest} associated with the task staging. * @return the command string */ public String getCommand(SummaryApplicationResponse application, AppDeploymentRequest request) { final boolean appHasCfEnv = hasCfEnv(request.getResource()); return Stream.concat(Stream.of(application.getDetectedStartCommand()), request.getCommandlineArguments().stream()) .map( arg-> { int indexOfEquals = arg.indexOf("="); if(indexOfEquals > -1) { String key = arg.substring(0, indexOfEquals); String value = arg.substring(indexOfEquals); key = escapeChar(key, "("); key = escapeChar(key, ")"); arg = key + value; } return appHasCfEnv ? CfEnvConfigurer.appendCloudProfileToSpringProfilesActiveArg(arg) : arg; }) .collect(Collectors.joining(" ")); } private String escapeChar(String value, String character) { if(value.contains(character)) { value = value.replace(character, "\\\\\\" + character); } return value; } private boolean pushTaskAppsEnabled() { return deploymentProperties.isPushTaskAppsEnabled(); } private Mono deployApplication(AppDeploymentRequest request) { String name = request.getDefinition().getName(); return pushApplication(name, request) .then(requestStopApplication(name)) .then(requestGetApplication(name)) .cast(AbstractApplicationSummary.class); } private Mono getOptionalApplication(AppDeploymentRequest request) { String name = request.getDefinition().getName(); Flux applications = requestListApplications() .filter(application -> name.equals(application.getName())); if (!pushTaskAppsEnabled()) { return applications .single() .onErrorMap(t-> t instanceof NoSuchElementException ? new IllegalStateException(String.format("Application %s does not exist", name)) : t) .cast(AbstractApplicationSummary.class); } return applications .singleOrEmpty() .cast(AbstractApplicationSummary.class); } private Mono getOrDeployApplication(AppDeploymentRequest request) { return getOptionalApplication(request) .switchIfEmpty(deployApplication(request)) .flatMap(application -> requestGetApplicationSummary(application.getId())); } private Mono launchTask(SummaryApplicationResponse application, AppDeploymentRequest request) { return requestCreateTask(application.getId(), getCommand(application, request), memory(request), diskQuota(request), request.getDefinition().getName()) .map(CreateTaskResponse::getId); } private Mono pushApplication(String name, AppDeploymentRequest request) { if (!pushTaskAppsEnabled()) { return Mono.empty(); } return requestPushApplication(PushApplicationManifestRequest.builder() .manifest(ApplicationManifest.builder() .path(getApplication(request)) .docker(Docker.builder().image(getDockerImage(request)).build()) .buildpacks(buildpacks(request)) .command("echo '*** First run of container to allow droplet creation.***' && sleep 300") .disk(diskQuota(request)) .environmentVariables(mergeEnvironmentVariables(name, request)) .healthCheckType(ApplicationHealthCheck.NONE) .memory(memory(request)) .name(name) .noRoute(true) .services(servicesToBind(request)) .build()) .stagingTimeout(this.deploymentProperties.getStagingTimeout()) .startupTimeout(this.deploymentProperties.getStartupTimeout()) .build()); } private Mono requestCreateTask(String applicationId, String command, int memory, int disk, String name) { return this.client.tasks() .create(CreateTaskRequest.builder() .applicationId(applicationId) .command(command) .memoryInMb(memory) .diskInMb(disk) .name(name) .build()); } private Mono requestDeleteApplication(String name) { return this.operations.applications() .delete(DeleteApplicationRequest.builder() .deleteRoutes(deploymentProperties.isDeleteRoutes()) .name(name) .build()); } private Mono requestGetApplication(String name) { return this.operations.applications() .get(GetApplicationRequest.builder() .name(name) .build()); } private Mono requestGetApplicationSummary(String applicationId) { return this.client.applicationsV2() .summary(org.cloudfoundry.client.v2.applications.SummaryApplicationRequest.builder() .applicationId(applicationId) .build()); } private Flux requestListApplications() { return this.operations.applications() .list(); } private Mono requestPushApplication(PushApplicationManifestRequest request) { return this.operations.applications() .pushManifest(request); } private Mono requestStopApplication(String name) { return this.operations.applications() .stop(StopApplicationRequest.builder() .name(name) .build()); } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/main/java/org/springframework/cloud/deployer/spi/cloudfoundry/DurationConverter.java ================================================ /* * Copyright 2016 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.cloud.deployer.spi.cloudfoundry; import java.time.Duration; import java.time.format.DateTimeParseException; import org.springframework.core.convert.converter.Converter; import org.springframework.util.StringUtils; /** * Converter from String to {@link java.time.Duration}, accepting human readable forms (without an H or a T). * * @author Eric Bottard */ public class DurationConverter implements Converter { @Override public Duration convert(String source) { if (StringUtils.hasText(source)) { return null; } try { return Duration.parse(source); } catch (DateTimeParseException ignored) { return Duration.parse("PT" + source); } } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/main/java/org/springframework/cloud/deployer/spi/cloudfoundry/ServiceParser.java ================================================ /* * Copyright 2019-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.cloud.deployer.spi.cloudfoundry; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; import org.springframework.util.StringUtils; /** * Parses service instances and parses binding parameters if provided. Accepts Strings like * 'myservice foo=bar, cat=bat' or 'service foo:bar, cat:bat', White space is required between the * service name and parameters, but optional between the key-value pairs. * * @author David Turanski */ abstract class ServiceParser { private static Pattern serviceWithParameters = Pattern.compile("([^\\s]+)\\s*?(.*)?"); private static Pattern singleQuotedLiteral = Pattern.compile("'([^']*?)'"); /** * @param serviceSpec the service instance followed by optional parameters. * @return an Option of parameters. */ static Optional> getServiceParameters(String serviceSpec) { Matcher m = serviceWithParameters.matcher(serviceSpec); if (m.matches()) { return parseParameters(m.group(2), serviceSpec); } return Optional.ofNullable(null); } /** * Extract the service name. * * @param serviceSpec the service instance name followed by optional parameters * @return the service instance */ static String getServiceInstanceName(String serviceSpec) { Matcher m = serviceWithParameters.matcher(serviceSpec); if (m.matches()) { return m.group(1); } else { throw new IllegalArgumentException("invalid service specification: " + serviceSpec); } } static List splitServiceProperties(String serviceProperties) { List serviceInstances = new ArrayList<>(); if (StringUtils.hasText(serviceProperties)) { Matcher m = singleQuotedLiteral.matcher(serviceProperties); int index = 0; while (index >= 0 && m.find(index)) { String val = m.group().replaceAll("'", ""); serviceInstances.add(val); serviceProperties = serviceProperties.replaceAll(m.group(), ""); index = serviceProperties.indexOf("'"); m = singleQuotedLiteral.matcher(serviceProperties); } Stream.of(serviceProperties.split(",")) .filter(StringUtils::hasText) .map(String::trim) .forEach(s -> serviceInstances.add(s)); } return serviceInstances; } private static Optional> parseParameters( String rawParametersString, String serviceSpec) { if (StringUtils.hasText(rawParametersString)) { Map parameters = new LinkedHashMap<>(); String[] entries = rawParametersString.split(","); Stream.of(entries) .forEach( s -> { String[] pair = s.split("[:|=]"); if (pair.length != 2) { throw new IllegalArgumentException( "invalid service specification: " + serviceSpec); } parameters.put(pair[0].trim(), pair[1].trim()); }); return Optional.of(parameters); } return Optional.ofNullable(null); } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/main/java/org/springframework/cloud/deployer/spi/cloudfoundry/UnsupportedVersionTaskLauncher.java ================================================ /* * Copyright 2017 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.cloud.deployer.spi.cloudfoundry; import com.github.zafarkhaja.semver.Version; import org.cloudfoundry.operations.CloudFoundryOperations; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; import org.springframework.cloud.deployer.spi.task.TaskLauncher; import org.springframework.cloud.deployer.spi.task.TaskStatus; import org.springframework.cloud.deployer.spi.util.RuntimeVersionUtils; /** * A failing implementation of {@link org.springframework.cloud.deployer.spi.task.TaskLauncher} for versions of the * Cloud Controller API below {@link #MINIMUM_SUPPORTED_VERSION}. * * @author Eric Bottard * @see CloudFoundryDeployerAutoConfiguration */ public class UnsupportedVersionTaskLauncher implements TaskLauncher { public static final Version MINIMUM_SUPPORTED_VERSION = Version.forIntegers(2, 63, 0); private final Version actualVersion; private final RuntimeEnvironmentInfo info; public UnsupportedVersionTaskLauncher(Version actualVersion, RuntimeEnvironmentInfo info) { this.actualVersion = actualVersion; this.info = info; } @Override public String launch(AppDeploymentRequest request) { throw failure(); } @Override public void cancel(String id) { throw failure(); } @Override public TaskStatus status(String id) { throw failure(); } @Override public void cleanup(String id) { throw failure(); } @Override public void destroy(String appName) { throw failure(); } @Override public RuntimeEnvironmentInfo environmentInfo() { return info; } @Override public int getMaximumConcurrentTasks() { throw failure(); } @Override public int getRunningTaskExecutionCount() { throw failure(); } @Override public String getLog(String appName) { throw failure(); } private UnsupportedOperationException failure() { return new UnsupportedOperationException("Cloud Foundry API version " + actualVersion + " is earlier than " + MINIMUM_SUPPORTED_VERSION + " and is incompatible with cf-java-client " + RuntimeVersionUtils.getVersion(CloudFoundryOperations.class)+ ". It is thus unsupported"); } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/main/java/org/springframework/cloud/deployer/spi/scheduler/cloudfoundry/CloudFoundryAppScheduler.java ================================================ /* * Copyright 2018-2021 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.cloud.deployer.spi.scheduler.cloudfoundry; import java.text.ParseException; import java.time.Duration; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.stream.Collectors; import javax.net.ssl.SSLException; import io.jsonwebtoken.lang.Assert; import io.pivotal.scheduler.SchedulerClient; import io.pivotal.scheduler.v1.jobs.CreateJobRequest; import io.pivotal.scheduler.v1.jobs.DeleteJobRequest; import io.pivotal.scheduler.v1.jobs.Job; import io.pivotal.scheduler.v1.jobs.ListJobsRequest; import io.pivotal.scheduler.v1.jobs.ListJobsResponse; import io.pivotal.scheduler.v1.jobs.ScheduleJobRequest; import io.pivotal.scheduler.v1.jobs.ScheduleJobResponse; import io.pivotal.scheduler.v1.schedules.ExpressionType; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.cloudfoundry.client.v2.applications.SummaryApplicationResponse; import org.cloudfoundry.operations.CloudFoundryOperations; import org.cloudfoundry.operations.applications.AbstractApplicationSummary; import org.cloudfoundry.operations.applications.ApplicationSummary; import org.cloudfoundry.operations.spaces.SpaceSummary; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.cloud.deployer.spi.cloudfoundry.CloudFoundryConnectionProperties; import org.springframework.cloud.deployer.spi.cloudfoundry.CloudFoundryDeploymentProperties; import org.springframework.cloud.deployer.spi.cloudfoundry.CloudFoundryTaskLauncher; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.scheduler.CreateScheduleException; import org.springframework.cloud.deployer.spi.scheduler.ScheduleInfo; import org.springframework.cloud.deployer.spi.scheduler.ScheduleRequest; import org.springframework.cloud.deployer.spi.scheduler.Scheduler; import org.springframework.cloud.deployer.spi.scheduler.SchedulerException; import org.springframework.cloud.deployer.spi.scheduler.SchedulerPropertyKeys; import org.springframework.cloud.deployer.spi.scheduler.UnScheduleException; import org.springframework.cloud.deployer.spi.scheduler.cloudfoundry.expression.QuartzCronExpression; import org.springframework.retry.RecoveryCallback; import org.springframework.retry.RetryCallback; import org.springframework.retry.RetryContext; import org.springframework.retry.policy.SimpleRetryPolicy; import org.springframework.retry.support.RetryTemplate; /** * A Cloud Foundry implementation of the Scheduler interface. * * @author Glenn Renfro */ public class CloudFoundryAppScheduler implements Scheduler { public final static String CRON_EXPRESSION_KEY = "spring.cloud.deployer.cloudfoundry.cron.expression"; private final static int PCF_PAGE_START_NUM = 1; //First PageNum for PCFScheduler starts at 1. private final static int MAX_SCHEDULE_NAME_LENGTH = 255; private final static String SCHEDULER_SERVICE_ERROR_MESSAGE = "Scheduler Service returned a null response."; protected final static Log logger = LogFactory.getLog(CloudFoundryAppScheduler.class); private final SchedulerClient client; private final CloudFoundryOperations operations; private final CloudFoundryConnectionProperties properties; private final CloudFoundryTaskLauncher taskLauncher; private final CloudFoundrySchedulerProperties schedulerProperties; private final CloudFoundryDeploymentProperties deploymentProperties; @Deprecated public CloudFoundryAppScheduler(SchedulerClient client, CloudFoundryOperations operations, CloudFoundryConnectionProperties properties, CloudFoundryTaskLauncher taskLauncher, CloudFoundrySchedulerProperties schedulerProperties) { Assert.notNull(client, "client must not be null"); Assert.notNull(operations, "operations must not be null"); Assert.notNull(properties, "properties must not be null"); Assert.notNull(taskLauncher, "taskLauncher must not be null"); Assert.notNull(schedulerProperties, "schedulerProperties must not be null"); this.client = client; this.operations = operations; this.properties = properties; this.taskLauncher = taskLauncher; this.schedulerProperties = schedulerProperties; this.deploymentProperties = new CloudFoundryDeploymentProperties(); this.deploymentProperties.setSchedulerUrl(this.schedulerProperties.getSchedulerUrl()); this.deploymentProperties.setScheduleSSLRetryCount(this.schedulerProperties.getScheduleSSLRetryCount()); this.deploymentProperties.setScheduleTimeoutInSeconds(this.schedulerProperties.getScheduleTimeoutInSeconds()); this.deploymentProperties.setUnScheduleTimeoutInSeconds(this.schedulerProperties.getScheduleTimeoutInSeconds()); this.deploymentProperties.setListTimeoutInSeconds(this.schedulerProperties.getListTimeoutInSeconds()); } public CloudFoundryAppScheduler(SchedulerClient client, CloudFoundryOperations operations, CloudFoundryConnectionProperties properties, CloudFoundryTaskLauncher taskLauncher, CloudFoundryDeploymentProperties deploymentProperties) { Assert.notNull(client, "client must not be null"); Assert.notNull(operations, "operations must not be null"); Assert.notNull(properties, "properties must not be null"); Assert.notNull(taskLauncher, "taskLauncher must not be null"); Assert.notNull(deploymentProperties, "deployment must not be null"); this.client = client; this.operations = operations; this.properties = properties; this.taskLauncher = taskLauncher; this.deploymentProperties = deploymentProperties; this.schedulerProperties = null; } @Override public void schedule(ScheduleRequest scheduleRequest) { String appName = scheduleRequest.getDefinition().getName(); String scheduleName = scheduleRequest.getScheduleName(); logger.debug(String.format("Scheduling: %s", scheduleName)); if (scheduleName.length() > MAX_SCHEDULE_NAME_LENGTH) { throw new CreateScheduleException(String.format("Schedule can not be created because its name " + "'%s' has too many characters. Schedule name length" + " must be %s characters or less.", scheduleName, MAX_SCHEDULE_NAME_LENGTH), null); } String cronExpressionCandidate = null; if (cronExpressionCandidate == null && scheduleRequest.getSchedulerProperties() != null) { cronExpressionCandidate = scheduleRequest.getSchedulerProperties().get(SchedulerPropertyKeys.CRON_EXPRESSION); } else if (cronExpressionCandidate == null && scheduleRequest.getDeploymentProperties().get("spring.cloud.scheduler.cron.expression") != null) { cronExpressionCandidate = scheduleRequest.getDeploymentProperties().get("spring.cloud.scheduler.cron.expression"); } else if (cronExpressionCandidate == null && scheduleRequest.getDeploymentProperties().get("spring.cloud.deployer.cron.expression") != null) { cronExpressionCandidate = scheduleRequest.getDeploymentProperties().get("spring.cloud.deployer.cron.expression"); } else if (scheduleRequest.getDeploymentProperties().get(CRON_EXPRESSION_KEY) != null) { cronExpressionCandidate = scheduleRequest.getDeploymentProperties().get(CRON_EXPRESSION_KEY); } Assert.hasText(cronExpressionCandidate, String.format( "request's scheduleProperties must have a %s or %s that is not null nor empty", SchedulerPropertyKeys.CRON_EXPRESSION, CRON_EXPRESSION_KEY)); String cronExpression = cronExpressionCandidate; try { new QuartzCronExpression("0 " + cronExpression); } catch(ParseException pe) { throw new CreateScheduleException("Cron Expression is invalid: " + pe.getMessage(), pe); } String command = stageTask(scheduleRequest); retryTemplate().execute(new RetryCallback() { @Override public Void doWithRetry(RetryContext retryContext) throws RuntimeException { scheduleTask(appName, scheduleName, cronExpression, command); return null; } }, new RecoveryCallback() { @Override public Void recover(RetryContext retryContext) throws Exception { if (retryContext.getLastThrowable() != null) { logger.error("Retry Context reported the following exception: " + retryContext.getLastThrowable().getMessage()); } logger.error("Unable to schedule application"); try { logger.debug("removing job portion of the schedule."); unschedule(scheduleName); } catch (UnScheduleException ex) { logger.debug("No job to be removed."); } throw new CreateScheduleException(scheduleName, retryContext.getLastThrowable()); } }); } @Override public void unschedule(String scheduleName) { logger.debug(String.format("Unscheduling: %s", scheduleName)); this.client.jobs().delete(DeleteJobRequest.builder() .jobId(getJob(scheduleName)) .build()) .block(Duration.ofSeconds(this.deploymentProperties.getUnScheduleTimeoutInSeconds())); } @Override public List list(String taskDefinitionName) { return list().stream().filter(scheduleInfo -> scheduleInfo.getTaskDefinitionName().equals(taskDefinitionName)) .collect(Collectors.toList()); } @Override public List list() { List result = new ArrayList<>(); for (int i = PCF_PAGE_START_NUM; i <= getJobPageCount(); i++) { List scheduleInfoPage = getSchedules(i) .collectList() .block(Duration.ofSeconds(this.deploymentProperties.getListTimeoutInSeconds())); if(scheduleInfoPage == null) { throw new SchedulerException(SCHEDULER_SERVICE_ERROR_MESSAGE); } result.addAll(scheduleInfoPage); } return result; } /** * Schedules the job for the application. * @param appName The name of the task app to be scheduled. * @param scheduleName the name of the schedule. * @param expression the cron expression. * @param command the command returned from the staging. */ private void scheduleTask(String appName, String scheduleName, String expression, String command) { logger.debug(String.format("Scheduling Task: ", appName)); ScheduleJobResponse response = getApplicationByAppName(appName) .flatMap(abstractApplicationSummary -> { return this.client.jobs().create(CreateJobRequest.builder() .applicationId(abstractApplicationSummary.getId()) // App GUID .command(command) .name(scheduleName) .build()); }).flatMap(createJobResponse -> { return this.client.jobs().schedule(ScheduleJobRequest. builder(). jobId(createJobResponse.getId()). expression(expression). expressionType(ExpressionType.CRON). enabled(true). build()); }) .onErrorMap(e -> { if (e instanceof SSLException) { throw new CloudFoundryScheduleSSLException("Failed to schedule" + scheduleName, e); } else { throw new CreateScheduleException(scheduleName, e); } }) .block(Duration.ofSeconds(this.deploymentProperties.getScheduleTimeoutInSeconds())); if(response == null) { throw new SchedulerException(SCHEDULER_SERVICE_ERROR_MESSAGE); } } /** * Stages the application specified in the {@link ScheduleRequest} on the CF server. * @param scheduleRequest {@link ScheduleRequest} containing the information required to schedule a task. * @return the command string for the scheduled task. */ private String stageTask(ScheduleRequest scheduleRequest) { logger.debug(String.format("Staging Task: ", scheduleRequest.getDefinition().getName())); AppDeploymentRequest request = new AppDeploymentRequest( scheduleRequest.getDefinition(), scheduleRequest.getResource(), scheduleRequest.getDeploymentProperties(), scheduleRequest.getCommandlineArguments()); SummaryApplicationResponse response = taskLauncher.stage(request); return taskLauncher.getCommand(response, request); } /** * Retrieve a {@link Mono} containing the {@link ApplicationSummary} associated with the appId. * @param appName the name of the {@link AbstractApplicationSummary} to search. */ private Mono getApplicationByAppName(String appName) { return requestListApplications() .filter(application -> appName.equals(application.getName())) .singleOrEmpty() .cast(AbstractApplicationSummary.class); } /** * Retrieve a {@link Flux} of {@link ApplicationSummary}s. */ private Flux requestListApplications() { return this.operations.applications() .list(); } /** * Retrieve a cached {@link Flux} of {@link ApplicationSummary}s. */ private Flux cacheAppSummaries() { return requestListApplications() .cache(); //cache results from first call. No need to re-retrieve each time. } /** * Retrieve a {@link Flux} containing the available {@link SpaceSummary}s. * @return {@link Flux} of {@link SpaceSummary}s. */ private Flux requestSpaces() { return this.operations.spaces() .list(); } /** * Retrieve a {@link Mono} containing a {@link SpaceSummary} for the specified name. * @param spaceName the name of space to search. * @return the {@link SpaceSummary} associated with the spaceName. */ private Mono getSpace(String spaceName) { return requestSpaces() .cache() //cache results from first call. .filter(space -> spaceName.equals(space.getName())) .singleOrEmpty() .cast(SpaceSummary.class); } /** * Retrieve a {@link Mono} containing the {@link ApplicationSummary} associated with the appId. * @param applicationSummaries {@link Flux} of {@link ApplicationSummary}s to filter. * @param appId the id of the {@link ApplicationSummary} to search. */ private Mono getApplication(Flux applicationSummaries, String appId) { return applicationSummaries .filter(application -> appId.equals(application.getId())) .singleOrEmpty(); } /** * Retrieve a Flux of {@link ScheduleInfo}s for the pageNumber specified. * The PCF-Scheduler returns all data in pages of 50 entries. This method * retrieves the specified page and transforms the {@link Flux} of {@link Job}s to * a {@link Flux} of {@link ScheduleInfo}s * * @param pageNumber integer containing the page offset for the {@link ScheduleInfo}s to retrieve. * @return {@link Flux} containing the {@link ScheduleInfo}s for the specified page number. */ private Flux getSchedules(int pageNumber) { Flux applicationSummaries = cacheAppSummaries(); return this.getSpace(this.properties.getSpace()).flatMap(requestSummary -> { return this.client.jobs().list(ListJobsRequest.builder() .spaceId(requestSummary.getId()) .page(pageNumber) .detailed(true).build());}) .flatMapIterable(jobs -> jobs.getResources())// iterate over the resources returned. .flatMap(job -> { return getApplication(applicationSummaries, job.getApplicationId()) // get the application name for each job. .map(optionalApp -> { ScheduleInfo scheduleInfo = new ScheduleInfo(); scheduleInfo.setScheduleProperties(new HashMap<>()); scheduleInfo.setScheduleName(job.getName()); scheduleInfo.setTaskDefinitionName(optionalApp.getName()); if (job.getJobSchedules() != null) { scheduleInfo.getScheduleProperties().put(SchedulerPropertyKeys.CRON_EXPRESSION, job.getJobSchedules().get(0).getExpression()); } else { logger.warn(String.format("Job %s does not have an associated schedule", job.getName())); } return scheduleInfo; }); }); } /** * Retrieves the number of pages that can be returned when retrieving a list of jobs. * @return an int containing the number of available pages. */ private int getJobPageCount() { ListJobsResponse response = this.getSpace(this.properties.getSpace()).flatMap(requestSummary -> { return this.client.jobs().list(ListJobsRequest.builder() .spaceId(requestSummary.getId()) .detailed(false).build()); }).block(); if(response == null) { throw new SchedulerException(SCHEDULER_SERVICE_ERROR_MESSAGE); } return response.getPagination().getTotalPages(); } /** * Retrieve a {@link Mono} that contains the {@link Job} for the jobName or null. * @param jobName - the name of the job to search search. * @param page - the page to search. * @return {@link Mono} containing the {@link Job} if found or null if not found. */ private Mono getJobMono(String jobName, int page) { return this.getSpace(this.properties.getSpace()).flatMap(requestSummary -> { return this.client .jobs() .list(ListJobsRequest.builder() .spaceId(requestSummary.getId()) .page(page) .build()); }) .flatMapIterable(jobs -> jobs.getResources()) .filter(job -> job.getName().equals(jobName)) .singleOrEmpty();// iterate over the resources returned. } /** * Retrieve the job id for the specified PCF Job Name. * @param jobName the name of the job to search. * @return The job id associated with the job. */ private String getJob(String jobName) { Job result = null; final int pageCount = getJobPageCount(); for (int pageNum = PCF_PAGE_START_NUM; pageNum <= pageCount; pageNum++) { result = getJobMono(jobName, pageNum) .block(); if (result != null) { break; } } if(result == null) { throw new UnScheduleException(String.format("schedule %s does not exist.", jobName)); } return result.getId(); } private RetryTemplate retryTemplate() { RetryTemplate retryTemplate = new RetryTemplate(); SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy( this.deploymentProperties.getScheduleSSLRetryCount(), Collections.singletonMap(CloudFoundryScheduleSSLException.class, true)); retryTemplate.setRetryPolicy(retryPolicy); return retryTemplate; } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/main/java/org/springframework/cloud/deployer/spi/scheduler/cloudfoundry/CloudFoundryScheduleSSLException.java ================================================ /* * Copyright 2018-2019 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.cloud.deployer.spi.scheduler.cloudfoundry; /** * A {@link RuntimeException} that wraps SSL based exceptions. * * @author Glenn Renfro */ public class CloudFoundryScheduleSSLException extends RuntimeException { public CloudFoundryScheduleSSLException(String message, Throwable t) { super(message, t); } public CloudFoundryScheduleSSLException(String t) { super(t); } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/main/java/org/springframework/cloud/deployer/spi/scheduler/cloudfoundry/CloudFoundrySchedulerProperties.java ================================================ /* * Copyright 2018-2021 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.cloud.deployer.spi.scheduler.cloudfoundry; import jakarta.validation.constraints.NotNull; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.validation.annotation.Validated; /** * Holds configuration properties for connecting to a Cloud Foundry Scheduler. * * @author Glenn Renfro */ @Validated @ConfigurationProperties(prefix = CloudFoundrySchedulerProperties.CLOUDFOUNDRY_PROPERTIES) @Deprecated public class CloudFoundrySchedulerProperties { /** * Top level prefix for Cloud Foundry related configuration properties. */ public static final String CLOUDFOUNDRY_PROPERTIES = "spring.cloud.scheduler.cloudfoundry"; /** * Location of the PCF scheduler REST API enpoint ot use. */ @NotNull private String schedulerUrl; /** * The number of retries allowed when scheduling a task if an {@link javax.net.ssl.SSLException} is thrown. */ private int scheduleSSLRetryCount = 5; /** * The number of seconds to wait for a unSchedule to complete. */ private int unScheduleTimeoutInSeconds = 30; /** * The number of seconds to wait for a schedule to complete. * This excludes the time it takes to stage the application on Cloud Foundry. */ private int scheduleTimeoutInSeconds = 30; /** * The number of seconds to wait for a list of schedules to be returned. */ private int listTimeoutInSeconds = 60; public String getSchedulerUrl() { return schedulerUrl; } public void setSchedulerUrl(String schedulerUrl) { this.schedulerUrl = schedulerUrl; } public int getScheduleSSLRetryCount() { return scheduleSSLRetryCount; } public void setScheduleSSLRetryCount(int scheduleSSLRetryCount) { this.scheduleSSLRetryCount = scheduleSSLRetryCount; } public int getUnScheduleTimeoutInSeconds() { return unScheduleTimeoutInSeconds; } public void setUnScheduleTimeoutInSeconds(int unScheduleTimeoutInSeconds) { this.unScheduleTimeoutInSeconds = unScheduleTimeoutInSeconds; } public int getScheduleTimeoutInSeconds() { return scheduleTimeoutInSeconds; } public void setScheduleTimeoutInSeconds(int scheduleTimeoutInSeconds) { this.scheduleTimeoutInSeconds = scheduleTimeoutInSeconds; } public int getListTimeoutInSeconds() { return listTimeoutInSeconds; } public void setListTimeoutInSeconds(int listTimeoutInSeconds) { this.listTimeoutInSeconds = listTimeoutInSeconds; } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/main/java/org/springframework/cloud/deployer/spi/scheduler/cloudfoundry/expression/QuartzCronExpression.java ================================================ /* * Copyright 2018-2019 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.cloud.deployer.spi.scheduler.cloudfoundry.expression; import java.text.ParseException; import java.util.Calendar; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.StringTokenizer; import java.util.TreeSet; /** * Provides a parser and evaluator for unix-like cron expressions. Cron * expressions provide the ability to specify complex time combinations such as * "At 8:00am every Monday through Friday" or "At 1:30am every * last Friday of the month". *

* Cron expressions are comprised of 6 required fields and one optional field * separated by white space. * * Based on the CronExpression from Quartz. * * @author Glenn Renfro */ public final class QuartzCronExpression { private static final int SECOND = 0; private static final int MINUTE = 1; private static final int HOUR = 2; private static final int DAY_OF_MONTH = 3; private static final int MONTH = 4; private static final int DAY_OF_WEEK = 5; private static final int YEAR = 6; private static final int ALL_SPEC_INT = 99; // '*' private static final int NO_SPEC_INT = 98; // '?' private static final Integer ALL_SPEC = ALL_SPEC_INT; private static final Integer NO_SPEC = NO_SPEC_INT; protected static final Map monthMap = new HashMap(20); protected static final Map dayMap = new HashMap(60); static { monthMap.put("JAN", 0); monthMap.put("FEB", 1); monthMap.put("MAR", 2); monthMap.put("APR", 3); monthMap.put("MAY", 4); monthMap.put("JUN", 5); monthMap.put("JUL", 6); monthMap.put("AUG", 7); monthMap.put("SEP", 8); monthMap.put("OCT", 9); monthMap.put("NOV", 10); monthMap.put("DEC", 11); dayMap.put("SUN", 1); dayMap.put("MON", 2); dayMap.put("TUE", 3); dayMap.put("WED", 4); dayMap.put("THU", 5); dayMap.put("FRI", 6); dayMap.put("SAT", 7); } private final String cronExpression; private TreeSet seconds; private TreeSet minutes; private TreeSet hours; private TreeSet daysOfMonth; private TreeSet months; private TreeSet daysOfWeek; private TreeSet years; private int nthdayOfWeek = 0; private boolean lastdayOfMonth = false; private int lastdayOffset = 0; public static final int MAX_YEAR = Calendar.getInstance().get(Calendar.YEAR) + 100; /** * Constructs a new {@link QuartzCronExpression} based on the specified * parameter. * * @param cronExpression String representation of the cron expression the * new object should represent * @throws ParseException * if the string expression cannot be parsed into a valid * {@link QuartzCronExpression} */ public QuartzCronExpression(String cronExpression) throws ParseException { if (cronExpression == null) { throw new IllegalArgumentException("cronExpression cannot be null"); } this.cronExpression = cronExpression.toUpperCase(Locale.ROOT); buildExpression(this.cronExpression); } /** * Returns the string representation of the {@link QuartzCronExpression} * * @return a string representation of the {@link QuartzCronExpression} */ @Override public String toString() { return cronExpression; } //////////////////////////////////////////////////////////////////////////// // // Expression Parsing Functions // //////////////////////////////////////////////////////////////////////////// protected void buildExpression(String expression) throws ParseException { try { if (seconds == null) { seconds = new TreeSet<>(); } if (minutes == null) { minutes = new TreeSet<>(); } if (hours == null) { hours = new TreeSet<>(); } if (daysOfMonth == null) { daysOfMonth = new TreeSet<>(); } if (months == null) { months = new TreeSet<>(); } if (daysOfWeek == null) { daysOfWeek = new TreeSet<>(); } if (years == null) { years = new TreeSet<>(); } int exprOn = SECOND; StringTokenizer exprsTok = new StringTokenizer(expression, " \t", false); while (exprsTok.hasMoreTokens() && exprOn <= YEAR) { String expr = exprsTok.nextToken().trim(); // throw an exception if L is used with other days of the month if(exprOn == DAY_OF_MONTH && expr.indexOf('L') != -1 && expr.length() > 1 && expr.contains(",")) { throw new ParseException("Support for specifying 'L' and 'LW' with other days of the month is not implemented", -1); } // throw an exception if L is used with other days of the week if(exprOn == DAY_OF_WEEK && expr.indexOf('L') != -1 && expr.length() > 1 && expr.contains(",")) { throw new ParseException("Support for specifying 'L' with other days of the week is not implemented", -1); } if(exprOn == DAY_OF_WEEK && expr.indexOf('#') != -1 && expr.indexOf('#', expr.indexOf('#') +1) != -1) { throw new ParseException("Support for specifying multiple \"nth\" days is not implemented.", -1); } StringTokenizer vTok = new StringTokenizer(expr, ","); while (vTok.hasMoreTokens()) { String v = vTok.nextToken(); storeExpressionVals(0, v, exprOn); } exprOn++; } if (exprOn <= DAY_OF_WEEK) { throw new ParseException("Unexpected end of expression.", expression.length()); } if (exprOn <= YEAR) { storeExpressionVals(0, "*", YEAR); } TreeSet dow = getSet(DAY_OF_WEEK); TreeSet dom = getSet(DAY_OF_MONTH); // Copying the logic from the UnsupportedOperationException below boolean dayOfMSpec = !dom.contains(NO_SPEC); boolean dayOfWSpec = !dow.contains(NO_SPEC); if (!dayOfMSpec || dayOfWSpec) { if (!dayOfWSpec || dayOfMSpec) { throw new ParseException( "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented.", 0); } } } catch (ParseException pe) { throw pe; } catch (Exception e) { throw new ParseException("Illegal cron expression format (" + e.toString() + ")", 0); } } private void checkIncrementRange(int incr, int type, int idxPos) throws ParseException { if (incr > 59 && (type == SECOND || type == MINUTE)) { throw new ParseException("Increment > 60 : " + incr, idxPos); } else if (incr > 23 && (type == HOUR)) { throw new ParseException("Increment > 24 : " + incr, idxPos); } else if (incr > 31 && (type == DAY_OF_MONTH)) { throw new ParseException("Increment > 31 : " + incr, idxPos); } else if (incr > 7 && (type == DAY_OF_WEEK)) { throw new ParseException("Increment > 7 : " + incr, idxPos); } else if (incr > 12 && (type == MONTH)) { throw new ParseException("Increment > 12 : " + incr, idxPos); } } protected int checkNext(int pos, String s, int val, int type) throws ParseException { int end = -1; int i = pos; if (i >= s.length()) { addToSet(val, end, -1, type); return i; } char c = s.charAt(pos); if (c == 'L') { if (type == DAY_OF_WEEK) { if(val < 1 || val > 7) throw new ParseException("Day-of-Week values must be between 1 and 7", -1); } else { throw new ParseException("'L' option is not valid here. (pos=" + i + ")", i); } TreeSet set = getSet(type); set.add(val); i++; return i; } if (c == 'W') { if (type != DAY_OF_MONTH) { throw new ParseException("'W' option is not valid here. (pos=" + i + ")", i); } if(val > 31) throw new ParseException("The 'W' option does not make sense with values larger than 31 (max number of days in a month)", i); TreeSet set = getSet(type); set.add(val); i++; return i; } if (c == '#') { if (type != DAY_OF_WEEK) { throw new ParseException("'#' option is not valid here. (pos=" + i + ")", i); } i++; try { nthdayOfWeek = Integer.parseInt(s.substring(i)); if (nthdayOfWeek < 1 || nthdayOfWeek > 5) { throw new Exception(); } } catch (Exception e) { throw new ParseException( "A numeric value between 1 and 5 must follow the '#' option", i); } TreeSet set = getSet(type); set.add(val); i++; return i; } if (c == '-') { i++; c = s.charAt(i); int v = Integer.parseInt(String.valueOf(c)); end = v; i++; if (i >= s.length()) { addToSet(val, end, 1, type); return i; } c = s.charAt(i); if (c >= '0' && c <= '9') { ValueSet vs = getValue(v, s, i); end = vs.value; i = vs.pos; } if (i < s.length() && ((c = s.charAt(i)) == '/')) { i++; c = s.charAt(i); int v2 = Integer.parseInt(String.valueOf(c)); i++; if (i >= s.length()) { addToSet(val, end, v2, type); return i; } c = s.charAt(i); if (c >= '0' && c <= '9') { ValueSet vs = getValue(v2, s, i); int v3 = vs.value; addToSet(val, end, v3, type); i = vs.pos; return i; } else { addToSet(val, end, v2, type); return i; } } else { addToSet(val, end, 1, type); return i; } } if (c == '/') { if ((i + 1) >= s.length() || s.charAt(i + 1) == ' ' || s.charAt(i + 1) == '\t') { throw new ParseException("'/' must be followed by an integer.", i); } i++; c = s.charAt(i); int v2 = Integer.parseInt(String.valueOf(c)); i++; if (i >= s.length()) { checkIncrementRange(v2, type, i); addToSet(val, end, v2, type); return i; } c = s.charAt(i); if (c >= '0' && c <= '9') { ValueSet vs = getValue(v2, s, i); int v3 = vs.value; checkIncrementRange(v3, type, i); addToSet(val, end, v3, type); i = vs.pos; return i; } else { throw new ParseException("Unexpected character '" + c + "' after '/'", i); } } addToSet(val, end, 0, type); i++; return i; } protected int skipWhiteSpace(int i, String s) { for (; i < s.length() && (s.charAt(i) == ' ' || s.charAt(i) == '\t'); i++) { ; } return i; } protected int findNextWhiteSpace(int i, String s) { for (; i < s.length() && (s.charAt(i) != ' ' || s.charAt(i) != '\t'); i++) { ; } return i; } protected void addToSet(int val, int end, int incr, int type) throws ParseException { TreeSet set = getSet(type); if (type == SECOND || type == MINUTE) { if ((val < 0 || val > 59 || end > 59) && (val != ALL_SPEC_INT)) { throw new ParseException( "Minute and Second values must be between 0 and 59", -1); } } else if (type == HOUR) { if ((val < 0 || val > 23 || end > 23) && (val != ALL_SPEC_INT)) { throw new ParseException( "Hour values must be between 0 and 23", -1); } } else if (type == DAY_OF_MONTH) { if ((val < 1 || val > 31 || end > 31) && (val != ALL_SPEC_INT) && (val != NO_SPEC_INT)) { throw new ParseException( "Day of month values must be between 1 and 31", -1); } } else if (type == MONTH) { if ((val < 1 || val > 12 || end > 12) && (val != ALL_SPEC_INT)) { throw new ParseException( "Month values must be between 1 and 12", -1); } } else if (type == DAY_OF_WEEK) { if ((val == 0 || val > 7 || end > 7) && (val != ALL_SPEC_INT) && (val != NO_SPEC_INT)) { throw new ParseException( "Day-of-Week values must be between 1 and 7", -1); } } if ((incr == 0 || incr == -1) && val != ALL_SPEC_INT) { if (val != -1) { set.add(val); } else { set.add(NO_SPEC); } return; } int startAt = val; int stopAt = end; if (val == ALL_SPEC_INT && incr <= 0) { incr = 1; set.add(ALL_SPEC); // put in a marker, but also fill values } if (type == SECOND || type == MINUTE) { if (stopAt == -1) { stopAt = 59; } if (startAt == -1 || startAt == ALL_SPEC_INT) { startAt = 0; } } else if (type == HOUR) { if (stopAt == -1) { stopAt = 23; } if (startAt == -1 || startAt == ALL_SPEC_INT) { startAt = 0; } } else if (type == DAY_OF_MONTH) { if (stopAt == -1) { stopAt = 31; } if (startAt == -1 || startAt == ALL_SPEC_INT) { startAt = 1; } } else if (type == MONTH) { if (stopAt == -1) { stopAt = 12; } if (startAt == -1 || startAt == ALL_SPEC_INT) { startAt = 1; } } else if (type == DAY_OF_WEEK) { if (stopAt == -1) { stopAt = 7; } if (startAt == -1 || startAt == ALL_SPEC_INT) { startAt = 1; } } else if (type == YEAR) { if (stopAt == -1) { stopAt = MAX_YEAR; } if (startAt == -1 || startAt == ALL_SPEC_INT) { startAt = 1970; } } // if the end of the range is before the start, then we need to overflow into // the next day, month etc. This is done by adding the maximum amount for that // type, and using modulus max to determine the value being added. int max = -1; if (stopAt < startAt) { switch (type) { case SECOND: max = 60; break; case MINUTE: max = 60; break; case HOUR: max = 24; break; case MONTH: max = 12; break; case DAY_OF_WEEK: max = 7; break; case DAY_OF_MONTH: max = 31; break; case YEAR: throw new IllegalArgumentException("Start year must be less than stop year"); default: throw new IllegalArgumentException("Unexpected type encountered"); } stopAt += max; } for (int i = startAt; i <= stopAt; i += incr) { if (max == -1) { // ie: there's no max to overflow over set.add(i); } else { // take the modulus to get the real value int i2 = i % max; // 1-indexed ranges should not include 0, and should include their max if (i2 == 0 && (type == MONTH || type == DAY_OF_WEEK || type == DAY_OF_MONTH) ) { i2 = max; } set.add(i2); } } } TreeSet getSet(int type) { switch (type) { case SECOND: return seconds; case MINUTE: return minutes; case HOUR: return hours; case DAY_OF_MONTH: return daysOfMonth; case MONTH: return months; case DAY_OF_WEEK: return daysOfWeek; case YEAR: return years; default: return null; } } protected ValueSet getValue(int v, String s, int i) { char c = s.charAt(i); StringBuilder s1 = new StringBuilder(String.valueOf(v)); while (c >= '0' && c <= '9') { s1.append(c); i++; if (i >= s.length()) { break; } c = s.charAt(i); } ValueSet val = new ValueSet(); val.pos = (i < s.length()) ? i : i + 1; val.value = Integer.parseInt(s1.toString()); return val; } protected int getNumericValue(String s, int i) { int endOfVal = findNextWhiteSpace(i, s); String val = s.substring(i, endOfVal); return Integer.parseInt(val); } protected int getMonthNumber(String s) { Integer integer = monthMap.get(s); if (integer == null) { return -1; } return integer; } protected int getDayOfWeekNumber(String s) { Integer integer = dayMap.get(s); if (integer == null) { return -1; } return integer; } protected int storeExpressionVals(int pos, String s, int type) throws ParseException { int incr = 0; int i = skipWhiteSpace(pos, s); if (i >= s.length()) { return i; } char c = s.charAt(i); if ((c >= 'A') && (c <= 'Z') && (!s.equals("L")) && (!s.equals("LW")) && (!s.matches("^L-[0-9]*[W]?"))) { String sub = s.substring(i, i + 3); int sval = -1; int eval = -1; if (type == MONTH) { sval = getMonthNumber(sub) + 1; if (sval <= 0) { throw new ParseException("Invalid Month value: '" + sub + "'", i); } if (s.length() > i + 3) { c = s.charAt(i + 3); if (c == '-') { i += 4; sub = s.substring(i, i + 3); eval = getMonthNumber(sub) + 1; if (eval <= 0) { throw new ParseException("Invalid Month value: '" + sub + "'", i); } } } } else if (type == DAY_OF_WEEK) { sval = getDayOfWeekNumber(sub); if (sval < 0) { throw new ParseException("Invalid Day-of-Week value: '" + sub + "'", i); } if (s.length() > i + 3) { c = s.charAt(i + 3); if (c == '-') { i += 4; sub = s.substring(i, i + 3); eval = getDayOfWeekNumber(sub); if (eval < 0) { throw new ParseException( "Invalid Day-of-Week value: '" + sub + "'", i); } } else if (c == '#') { try { i += 4; nthdayOfWeek = Integer.parseInt(s.substring(i)); if (nthdayOfWeek < 1 || nthdayOfWeek > 5) { throw new Exception(); } } catch (Exception e) { throw new ParseException( "A numeric value between 1 and 5 must follow the '#' option", i); } } else if (c == 'L') { i++; } } } else { throw new ParseException( "Illegal characters for this position: '" + sub + "'", i); } if (eval != -1) { incr = 1; } addToSet(sval, eval, incr, type); return (i + 3); } if (c == '?') { i++; if ((i + 1) < s.length() && (s.charAt(i) != ' ' && s.charAt(i + 1) != '\t')) { throw new ParseException("Illegal character after '?': " + s.charAt(i), i); } if (type != DAY_OF_WEEK && type != DAY_OF_MONTH) { throw new ParseException( "'?' can only be specified for Day-of-Month or Day-of-Week.", i); } if (type == DAY_OF_WEEK && !lastdayOfMonth) { int val = daysOfMonth.last(); if (val == NO_SPEC_INT) { throw new ParseException( "'?' can only be specified for Day-of-Month -OR- Day-of-Week.", i); } } addToSet(NO_SPEC_INT, -1, 0, type); return i; } if (c == '*' || c == '/') { if (c == '*' && (i + 1) >= s.length()) { addToSet(ALL_SPEC_INT, -1, incr, type); return i + 1; } else if (c == '/' && ((i + 1) >= s.length() || s.charAt(i + 1) == ' ' || s .charAt(i + 1) == '\t')) { throw new ParseException("'/' must be followed by an integer.", i); } else if (c == '*') { i++; } c = s.charAt(i); if (c == '/') { // is an increment specified? i++; if (i >= s.length()) { throw new ParseException("Unexpected end of string.", i); } incr = getNumericValue(s, i); i++; if (incr > 10) { i++; } checkIncrementRange(incr, type, i); } else { incr = 1; } addToSet(ALL_SPEC_INT, -1, incr, type); return i; } else if (c == 'L') { i++; if (type == DAY_OF_MONTH) { lastdayOfMonth = true; } if (type == DAY_OF_WEEK) { addToSet(7, 7, 0, type); } if(type == DAY_OF_MONTH && s.length() > i) { c = s.charAt(i); if(c == '-') { ValueSet vs = getValue(0, s, i+1); lastdayOffset = vs.value; if(lastdayOffset > 30) throw new ParseException("Offset from last day must be <= 30", i+1); i = vs.pos; } if(s.length() > i) { c = s.charAt(i); if(c == 'W') { i++; } } } return i; } else if (c >= '0' && c <= '9') { int val = Integer.parseInt(String.valueOf(c)); i++; if (i >= s.length()) { addToSet(val, -1, -1, type); } else { c = s.charAt(i); if (c >= '0' && c <= '9') { ValueSet vs = getValue(val, s, i); val = vs.value; i = vs.pos; } i = checkNext(i, s, val, type); return i; } } else { throw new ParseException("Unexpected character: " + c, i); } return i; } public static class ValueSet { public int value; public int pos; } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/main/resources/META-INF/additional-spring-configuration-metadata.json ================================================ { "hints": [ { "name": "spring.cloud.deployer.cloudfoundry.health-check", "values": [ { "value": "NONE" }, { "value": "HTTP" }, { "value": "PROCESS" }, { "value": "PORT" } ] } ] } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ org.springframework.cloud.deployer.spi.cloudfoundry.CloudFoundryDeployerAutoConfiguration ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/main/resources/META-INF/spring.factories ================================================ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.cloud.deployer.spi.cloudfoundry.CloudFoundryDeployerAutoConfiguration ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/test/java/org/springframework/cloud/deployer/spi/cloudfoundry/AbstractAppDeployerTestSupport.java ================================================ /* * Copyright 2022 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.cloud.deployer.spi.cloudfoundry; import java.util.HashMap; import java.util.Map; import org.cloudfoundry.logcache.v1.LogCacheClient; import org.cloudfoundry.operations.CloudFoundryOperations; import org.cloudfoundry.operations.applications.ApplicationDetail; import org.cloudfoundry.operations.applications.Applications; import org.cloudfoundry.operations.applications.DeleteApplicationRequest; import org.cloudfoundry.operations.applications.GetApplicationRequest; import org.cloudfoundry.operations.applications.PushApplicationManifestRequest; import org.cloudfoundry.operations.applications.ScaleApplicationRequest; import org.cloudfoundry.operations.services.Services; import org.junit.jupiter.api.BeforeEach; import org.mockito.Answers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import reactor.core.publisher.Mono; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; import org.springframework.context.ApplicationContext; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; /** * @author David Turanski */ public abstract class AbstractAppDeployerTestSupport { protected final CloudFoundryDeploymentProperties deploymentProperties = new CloudFoundryDeploymentProperties(); @Mock(answer = Answers.RETURNS_SMART_NULLS) protected AppNameGenerator applicationNameGenerator; @Mock(answer = Answers.RETURNS_SMART_NULLS) protected Applications applications; protected CloudFoundryAppDeployer deployer; @Mock(answer = Answers.RETURNS_SMART_NULLS) @MockBean protected CloudFoundryOperations operations; @Mock(answer = Answers.RETURNS_SMART_NULLS) protected Services services; @Mock(answer = Answers.RETURNS_SMART_NULLS) protected RuntimeEnvironmentInfo runtimeEnvironmentInfo; @Mock(answer = Answers.RETURNS_SMART_NULLS) @MockBean protected LogCacheClient reactorLogCacheClient; @Autowired protected ApplicationContext applicationContext; @Mock protected ApplicationLogAccessor applicationLogAccessor; @BeforeEach public void setUp() { MockitoAnnotations.openMocks(this); given(this.operations.applications()).willReturn(this.applications); given(this.operations.services()).willReturn(this.services); this.deployer = new CloudFoundryAppDeployer(this.applicationNameGenerator, this.deploymentProperties, this.operations, this.runtimeEnvironmentInfo, this.applicationLogAccessor); postSetUp(); } protected abstract void postSetUp(); protected void givenRequestScaleApplication(String id, Integer count, int memoryLimit, int diskLimit, Mono response) { given(this.operations.applications() .scale(ScaleApplicationRequest.builder().name(id).instances(count).memoryLimit(memoryLimit) .diskLimit(diskLimit) .startupTimeout(this.deploymentProperties.getStartupTimeout()) .stagingTimeout(this.deploymentProperties.getStagingTimeout()).build())).willReturn(response); } protected void givenRequestDeleteApplication(String id, Mono response) { given(this.operations.applications() .delete(DeleteApplicationRequest.builder().deleteRoutes(true).name(id).build())).willReturn(response); } @SuppressWarnings("unchecked") protected void givenRequestGetApplication(String id, Mono response, Mono... responses) { given(this.operations.applications().get(GetApplicationRequest.builder().name(id).build())).willReturn(response, responses); } protected void givenRequestPushApplication(PushApplicationManifestRequest request, Mono response) { given(this.operations.applications() .pushManifest(any(PushApplicationManifestRequest.class))) .willReturn(response); } protected Map defaultEnvironmentVariables() { Map environmentVariables = new HashMap<>(); environmentVariables.put("SPRING_APPLICATION_JSON", "{}"); addGuidAndIndex(environmentVariables); return environmentVariables; } protected void addGuidAndIndex(Map environmentVariables) { environmentVariables.put("SPRING_APPLICATION_INDEX", "${vcap.application.instance_index}"); environmentVariables.put("SPRING_CLOUD_APPLICATION_GUID", "${vcap.application.name}:${vcap.application.instance_index}"); } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/test/java/org/springframework/cloud/deployer/spi/cloudfoundry/ApplicationLogAccessorTests.java ================================================ /* * Copyright 2023 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.cloud.deployer.spi.cloudfoundry; import org.cloudfoundry.logcache.v1.Envelope; import org.cloudfoundry.logcache.v1.EnvelopeBatch; import org.cloudfoundry.logcache.v1.Log; import org.cloudfoundry.logcache.v1.LogCacheClient; import org.cloudfoundry.logcache.v1.ReadResponse; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import reactor.core.publisher.Mono; import java.time.Duration; import java.util.Base64; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class ApplicationLogAccessorTests { @Mock private LogCacheClient logCacheClient; @Test void testDefaultCase() { String sampleData = "foo\nbar\nbaz\nboo"; String exectedResult = "boo\nbaz\nbar\nfoo"; when(logCacheClient.read(any())).thenReturn(getSampleResponse(sampleData)); ApplicationLogAccessor applicationLogAccessor = new ApplicationLogAccessor(this.logCacheClient); assertThat(applicationLogAccessor.getLog("myDeploymentId", Duration.ofSeconds(5))).isEqualTo(exectedResult); } private Mono getSampleResponse(String sampleData) { Envelope envelope = Envelope.builder().log(getSampleLog(sampleData)).build(); EnvelopeBatch envelopeBatch = EnvelopeBatch.builder().batch(envelope).build(); return Mono.just(ReadResponse.builder().envelopes(envelopeBatch).build()); } private Log getSampleLog(String sampleData) { return Log.builder().payload(Base64.getEncoder().encodeToString(sampleData.getBytes())).build(); } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/test/java/org/springframework/cloud/deployer/spi/cloudfoundry/CfEnvAwareResourceTests.java ================================================ /* * Copyright 2020-2024 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.cloud.deployer.spi.cloudfoundry; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import org.junit.jupiter.api.Test; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; /** * @author David Turanski */ public class CfEnvAwareResourceTests { @Test public void testCfEnvResolverWithCfEnvJava17() throws IOException { // this task app is compiled from a spring-cloud-task sample using jdk17 // and cfenv added to build CfEnvAwareResource resource = CfEnvAwareResource.of(new ClassPathResource("timestamp-task-3.1.2-SNAPSHOT.jar")); assertThat(resource.hasCfEnv()).isTrue(); } @Test public void testCfEnvResolverWithCfEnv() throws IOException { CfEnvAwareResource resource = CfEnvAwareResource.of(new ClassPathResource("log-sink-rabbit-3.0.0.BUILD-SNAPSHOT.jar")); assertThat(resource.hasCfEnv()).isTrue(); } @Test public void testCfEnvResolverWithNoCfEnv() throws IOException { CfEnvAwareResource resource = CfEnvAwareResource.of(new ClassPathResource("batch-job-1.0.0.BUILD-SNAPSHOT.jar")); assertThat(resource.hasCfEnv()).isFalse(); } @Test public void testDoesNothingWithDocker() throws IOException, URISyntaxException { Resource docker = mock(Resource.class); when(docker.getURI()).thenReturn(new URI("docker://fake/news")); assertThat(CfEnvAwareResource.of(docker).hasCfEnv()).isFalse(); } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/test/java/org/springframework/cloud/deployer/spi/cloudfoundry/CfEnvConfigurerTests.java ================================================ /* * Copyright 2020-2021 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.cloud.deployer.spi.cloudfoundry; import java.util.Collections; import java.util.Map; import org.cloudfoundry.util.FluentMap; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; public class CfEnvConfigurerTests { @Test public void testCfEnvConfigurerActivateCloudProfile() { Map env = CfEnvConfigurer.activateCloudProfile( Collections.emptyMap(), CfEnvConfigurer.SPRING_PROFILES_ACTIVE_FQN); assertThat(env).containsEntry(CfEnvConfigurer.SPRING_PROFILES_ACTIVE_FQN, CfEnvConfigurer.CLOUD_PROFILE_NAME); assertThat(env).doesNotContainKeys( CfEnvConfigurer.SPRING_PROFILES_ACTIVE, CfEnvConfigurer.SPRING_PROFILES_ACTIVE_HYPHENATED); } @Test public void testCfEnvConfigurerDoNotActivateCloudProfileIfNoneExists() { Map env = CfEnvConfigurer.activateCloudProfile( Collections.emptyMap(), null); assertThat(env).doesNotContainKeys(CfEnvConfigurer.SPRING_PROFILES_ACTIVE_FQN, CfEnvConfigurer.SPRING_PROFILES_ACTIVE, CfEnvConfigurer.SPRING_PROFILES_ACTIVE_HYPHENATED); } @Test public void testCfEnvConfigurerActivateCloudProfileWithEmptyValue() { Map env = CfEnvConfigurer.activateCloudProfile( FluentMap.builder() .entry(CfEnvConfigurer.SPRING_PROFILES_ACTIVE, " ") .build(), CfEnvConfigurer.SPRING_PROFILES_ACTIVE); assertThat(env).containsEntry(CfEnvConfigurer.SPRING_PROFILES_ACTIVE, CfEnvConfigurer.CLOUD_PROFILE_NAME); assertThat(env).doesNotContainKeys(CfEnvConfigurer.SPRING_PROFILES_ACTIVE_FQN, CfEnvConfigurer.SPRING_PROFILES_ACTIVE_HYPHENATED); } @Test public void testCfEnvConfigurerAppendToActiveProfiles() { Map env = CfEnvConfigurer.activateCloudProfile( FluentMap.builder() .entry(CfEnvConfigurer.SPRING_PROFILES_ACTIVE_FQN, "foo,bar") .build(), null); assertThat(env).containsEntry(CfEnvConfigurer.SPRING_PROFILES_ACTIVE_FQN, "foo,bar,cloud"); assertThat(env).doesNotContainKeys(CfEnvConfigurer.SPRING_PROFILES_ACTIVE, CfEnvConfigurer.SPRING_PROFILES_ACTIVE_HYPHENATED); } @Test public void testCfEnvConfigurerDoesNotAppendCloudProfileIfAlreadyThere() { Map env = CfEnvConfigurer.activateCloudProfile( FluentMap.builder() .entry(CfEnvConfigurer.SPRING_PROFILES_ACTIVE_FQN, "foo,cloud") .build(), null); assertThat(env).containsEntry(CfEnvConfigurer.SPRING_PROFILES_ACTIVE_FQN, "foo,cloud"); env = CfEnvConfigurer.activateCloudProfile( FluentMap.builder() .entry(CfEnvConfigurer.SPRING_PROFILES_ACTIVE_FQN, "cloud,foo,bar") .build(), null); assertThat(env).containsEntry(CfEnvConfigurer.SPRING_PROFILES_ACTIVE_FQN, "cloud,foo,bar"); } @Test public void testCloudProfileAppendedToCommandLineArgs() { assertThat(CfEnvConfigurer.appendCloudProfileToSpringProfilesActiveArg("--spring-profiles-active=foo,bar")) .isEqualTo("--spring-profiles-active=foo,bar,cloud"); assertThat(CfEnvConfigurer.appendCloudProfileToSpringProfilesActiveArg("--spring.profiles.active=foo")) .isEqualTo("--spring.profiles.active=foo,cloud"); assertThat(CfEnvConfigurer.appendCloudProfileToSpringProfilesActiveArg("--spring.profiles.active=foo,cloud")) .isEqualTo("--spring.profiles.active=foo,cloud"); assertThat(CfEnvConfigurer.appendCloudProfileToSpringProfilesActiveArg("--spring.profiles.active=cloud")) .isEqualTo("--spring.profiles.active=cloud"); assertThat(CfEnvConfigurer.appendCloudProfileToSpringProfilesActiveArg("--baz=foo")) .isEqualTo("--baz=foo"); } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/test/java/org/springframework/cloud/deployer/spi/cloudfoundry/CloudFoundryActuatorTemplateTests.java ================================================ /* * Copyright 2022-2025 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.cloud.deployer.spi.cloudfoundry; import java.io.IOException; import java.net.ServerSocket; import java.util.Collections; import java.util.List; import java.util.Map; import org.cloudfoundry.operations.applications.ApplicationDetail; import org.cloudfoundry.operations.applications.InstanceDetail; import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.springframework.cloud.deployer.spi.app.ActuatorOperations; import org.springframework.cloud.deployer.spi.app.AppAdmin; import org.springframework.http.HttpEntity; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; import reactor.core.publisher.Mono; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; public class CloudFoundryActuatorTemplateTests extends AbstractAppDeployerTestSupport { @Mock private RestTemplate restTemplate; private ActuatorOperations actuatorOperations; @Override protected void postSetUp() { this.actuatorOperations = new CloudFoundryActuatorTemplate(restTemplate, this.deployer, new AppAdmin()); int port = findRandomOpenPort(); givenRequestGetApplication("test-application-id", Mono.just( ApplicationDetail.builder() .diskQuota(0) .id("test-application-id") .instances(1) .memoryLimit(0) .name("test-application") .requestedState("RUNNING") .runningInstances(1) .stack("test-stack") .urls("localhost:" + port) // No longer dynamic .instanceDetail(InstanceDetail.builder().state("RUNNING").index("1").build()) .build() )); MultiValueMap header = new LinkedMultiValueMap<>(); header.add("X-Cf-App-Instance", "test-application-id:0"); header.add("Accept", "application/json"); header.add("Content-Type", "application/json"); when(restTemplate.exchange(getUrl(port) + "/actuator/info", HttpMethod.GET,new HttpEntity<>(header), Map.class)) .thenReturn(new ResponseEntity<>(Collections.singletonMap("app", Collections.singletonMap("name", "log-sink-rabbit")), HttpStatus.OK)); when(restTemplate.exchange(getUrl(port) + "/actuator/bindings",HttpMethod.GET,new HttpEntity<>(header), List.class)) .thenReturn(new ResponseEntity<>(Collections.singletonList(Collections.singletonMap("bindingName", "input")), HttpStatus.OK)); when(restTemplate.exchange(getUrl(port) + "/actuator/bindings/input",HttpMethod.GET,new HttpEntity<>(header), Map.class)) .thenReturn(new ResponseEntity<>(Collections.singletonMap("bindingName", "input"), HttpStatus.OK)); when(restTemplate.exchange(getUrl(port) + "/actuator/bindings/input", HttpMethod.POST, new HttpEntity<>(Collections.singletonMap("state", "STOPPED"), header), Map.class)) .thenReturn(new ResponseEntity<>(Collections.singletonMap("state", "STOPPED"), HttpStatus.OK)); } @Test void actuatorInfo() { Map info = actuatorOperations .getFromActuator("test-application-id", "test-application:0", "/info", Map.class); assertThat(((Map) info.get("app")).get("name")).isEqualTo("log-sink-rabbit"); } @Test void actuatorBindings() { List bindings = actuatorOperations .getFromActuator("test-application-id", "test-application:0", "/bindings", List.class); assertThat(((Map) (bindings.get(0))).get("bindingName")).isEqualTo("input"); } @Test void actuatorBindingInput() { Map binding = actuatorOperations .getFromActuator("test-application-id", "test-application:0", "/bindings/input", Map.class); assertThat(binding.get("bindingName")).isEqualTo("input"); } @Test void actuatorPostBindingInput() { Map state = actuatorOperations .postToActuator("test-application-id", "test-application:0", "/bindings/input", Collections.singletonMap("state", "STOPPED"), Map.class); assertThat(state.get("state")).isEqualTo("STOPPED"); } public static String getUrl(int port) { return "http://localhost:" + port; } public static int findRandomOpenPort() { try (ServerSocket socket = new ServerSocket(0)) { socket.setReuseAddress(true); return socket.getLocalPort(); } catch (IOException e) { throw new RuntimeException("Failed to find a random open port", e); } } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/test/java/org/springframework/cloud/deployer/spi/cloudfoundry/CloudFoundryAppDeployerIntegrationIT.java ================================================ /* * Copyright 2016-2021 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.cloud.deployer.spi.cloudfoundry; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.deployer.spi.app.AppDeployer; import org.springframework.cloud.deployer.spi.test.AbstractAppDeployerIntegrationJUnit5Tests; import org.springframework.cloud.deployer.spi.test.Timeout; import org.springframework.context.annotation.Configuration; import org.springframework.test.context.ContextConfiguration; /** * Integration tests for CloudFoundryAppDeployer. * * @author Eric Bottard * @author Greg Turnquist */ @SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.NONE, properties = {"spring.cloud.deployer.cloudfoundry.enableRandomAppNamePrefix=false"}) @ContextConfiguration(classes = CloudFoundryAppDeployerIntegrationIT.Config.class) public class CloudFoundryAppDeployerIntegrationIT extends AbstractAppDeployerIntegrationJUnit5Tests { @Autowired private AppDeployer appDeployer; @Override protected AppDeployer provideAppDeployer() { return appDeployer; } /** * Execution environments may override this default value to have tests wait longer for a deployment, for example if * running in an environment that is known to be slow. */ protected double timeoutMultiplier = 1.0D; protected int maxRetries = 60; @BeforeEach public void init() { String multiplier = System.getenv("CF_DEPLOYER_TIMEOUT_MULTIPLIER"); if (multiplier != null) { timeoutMultiplier = Double.parseDouble(multiplier); } } @Override @Test @Disabled("Need to look into args escaping better. Disabling for the time being") public void testCommandLineArgumentsPassing() { } @Override protected String randomName() { // This will become the hostname part and is limited to 63 chars String name = super.randomName(); return name.substring(0, Math.min(63, name.length())); } @Override protected Timeout deploymentTimeout() { return new Timeout(maxRetries, (int) (5000 * timeoutMultiplier)); } @Override protected Timeout undeploymentTimeout() { return new Timeout(maxRetries, (int) (5000 * timeoutMultiplier)); } /** * This triggers the use of {@link CloudFoundryDeployerAutoConfiguration}. * * @author Eric Bottard */ @Configuration @EnableAutoConfiguration @EnableConfigurationProperties public static class Config { } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/test/java/org/springframework/cloud/deployer/spi/cloudfoundry/CloudFoundryAppDeployerTests.java ================================================ /* * Copyright 2016-2022 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.cloud.deployer.spi.cloudfoundry; import java.io.File; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.NoSuchElementException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.compress.utils.Sets; import org.assertj.core.data.MapEntry; import org.cloudfoundry.client.v2.ClientV2Exception; import org.cloudfoundry.operations.applications.ApplicationDetail; import org.cloudfoundry.operations.applications.ApplicationHealthCheck; import org.cloudfoundry.operations.applications.ApplicationManifest; import org.cloudfoundry.operations.applications.Docker; import org.cloudfoundry.operations.applications.InstanceDetail; import org.cloudfoundry.operations.applications.PushApplicationManifestRequest; import org.cloudfoundry.operations.applications.Route; import org.cloudfoundry.operations.applications.StartApplicationRequest; import org.cloudfoundry.operations.services.BindServiceInstanceRequest; import org.cloudfoundry.util.FluentMap; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import reactor.core.publisher.Mono; import org.springframework.cloud.deployer.resource.docker.DockerResource; import org.springframework.cloud.deployer.resource.maven.MavenResource; import org.springframework.cloud.deployer.spi.app.AppDeployer; import org.springframework.cloud.deployer.spi.app.AppScaleRequest; import org.springframework.cloud.deployer.spi.app.AppStatus; import org.springframework.cloud.deployer.spi.app.DeploymentState; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.core.io.UrlResource; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; /** * Unit tests for the {@link CloudFoundryAppDeployer}. * * @author Greg Turnquist * @author Eric Bottard * @author Ilayaperumal Gopinathan * @author Ben Hale * @author David Turanski */ public class CloudFoundryAppDeployerTests extends AbstractAppDeployerTestSupport { @TempDir public Path folder; @Override protected void postSetUp() { this.deploymentProperties.setServices(new HashSet<>(Arrays.asList("test-service-1", "test-service-2"))); this.deploymentProperties.setEnv(Collections.singletonMap("SOME_GLOBAL_PROPERTY", "someGlobalValue")); this.deploymentProperties.getAppAdmin().setUser("user"); this.deploymentProperties.getAppAdmin().setPassword("password"); } @SuppressWarnings("unchecked") @Test public void deploy() throws IOException { Resource resource = new FileSystemResource("src/test/resources/demo-0.0.1-SNAPSHOT.jar"); given(this.applicationNameGenerator.generateAppName("test-application")).willReturn("test-application-id"); givenRequestGetApplication("test-application-id", Mono.error(new IllegalArgumentException()), Mono.just( ApplicationDetail.builder() .diskQuota(0) .id("test-application-id") .instances(1) .memoryLimit(0) .name("test-application") .requestedState("RUNNING") .runningInstances(0) .stack("test-stack") .build())); givenRequestPushApplication(PushApplicationManifestRequest.builder() .manifest(ApplicationManifest.builder() .path(resource.getFile().toPath()) .buildpack(deploymentProperties.getBuildpack()) .disk(1024) .environmentVariables(defaultEnvironmentVariables()) .instances(1) .memory(1024) .name("test-application-id") .service("test-service-2") .service("test-service-1") .build()) .stagingTimeout(this.deploymentProperties.getStagingTimeout()) .startupTimeout(this.deploymentProperties.getStartupTimeout()) .build(), Mono.empty()); String deploymentId = this.deployer.deploy( new AppDeploymentRequest(new AppDefinition("test-application", Collections.emptyMap()), resource, Collections.EMPTY_MAP)); assertThat(deploymentId).isEqualTo("test-application-id"); } @Test void getLog() { final String LOG_RESULTS = "log results"; when(this.operations.applications().get(any())).thenReturn(applicationDetailRunningApp()); ApplicationLogAccessor applicationLogAccessor = mock(ApplicationLogAccessor.class); when(applicationLogAccessor.getLog(any(), any())).thenReturn(LOG_RESULTS); CloudFoundryAppDeployer deployer = new CloudFoundryAppDeployer(this.applicationNameGenerator, this.deploymentProperties, this.operations, this.runtimeEnvironmentInfo, applicationLogAccessor); assertThat(deployer.getLog("test-application-id")).isEqualTo(LOG_RESULTS); } @Test void getLogWithNoResult() { when(this.operations.applications().get(any())).thenThrow(new NoSuchElementException()); ApplicationLogAccessor applicationLogAccessor = mock(ApplicationLogAccessor.class); CloudFoundryAppDeployer deployer = new CloudFoundryAppDeployer(this.applicationNameGenerator, this.deploymentProperties, this.operations, this.runtimeEnvironmentInfo, applicationLogAccessor); assertThatThrownBy( () -> deployer.getLog("test-application-id")) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Unable to find cf-guid"); } @Test public void deployMavenArtifactShouldDeleteByDefault() throws IOException, URISyntaxException { MavenResource resource = mock(MavenResource.class); Path mavenPath = folder.resolve("maven"); Path artifactPath = mavenPath.resolve("artifact.jar"); File mavenArtifact = artifactPath.toFile(); given(resource.getFile()).willReturn(mavenArtifact); given(resource.getURI()).willReturn(new URI("maven://test:demo:0.0.1")); assertThat(this.deploymentProperties.isAutoDeleteMavenArtifacts()).isTrue(); deployResource(this.deployer, resource); assertThat(mavenArtifact.getParentFile().exists()).isFalse(); } @Test public void deployMavenArtifactShouldNotDeleteIfConfigured() throws IOException, URISyntaxException { MavenResource resource = mock(MavenResource.class); Path mavenPath = folder.resolve("maven"); Path artifactPath = mavenPath.resolve("artifact.jar"); File mavenArtifact = artifactPath.toFile(); Files.createDirectories(artifactPath.getParent()); given(resource.getFile()).willReturn(mavenArtifact); given(resource.getURI()).willReturn(new URI("maven://test:demo:0.0.1")); CloudFoundryDeploymentProperties deploymentProperties = new CloudFoundryDeploymentProperties(); deploymentProperties.setAutoDeleteMavenArtifacts(false); CloudFoundryAppDeployer deployer = new CloudFoundryAppDeployer(this.applicationNameGenerator, deploymentProperties, this.operations, this.runtimeEnvironmentInfo, this.applicationLogAccessor); deployResource(deployer, resource); assertThat(mavenArtifact.getParentFile().exists()).isTrue(); } @Test public void deployHttpArtifactShouldDelete() throws IOException, URISyntaxException { UrlResource resource = mock(UrlResource.class); Path downloadPath = folder.resolve("download"); Path artifactPath = downloadPath.resolve("artifact.jar"); File downloadedArtifact = artifactPath.toFile(); given(resource.getFile()).willReturn(downloadedArtifact); given(resource.getURI()).willReturn(new URI("http://somehost/artifact.jar")); assertThat(this.deploymentProperties.isAutoDeleteMavenArtifacts()).isTrue(); deployResource(this.deployer, resource); assertThat(downloadedArtifact.exists()).isFalse(); } @SuppressWarnings("unchecked") private void deployResource(CloudFoundryAppDeployer deployer, Resource resource) throws IOException { given(this.applicationNameGenerator.generateAppName("test-application")).willReturn("test-application-id"); givenRequestGetApplication("test-application-id", Mono.error(new IllegalArgumentException()), Mono.just( ApplicationDetail.builder() .diskQuota(0) .id("test-application-id") .instances(1) .memoryLimit(0) .name("test-application") .requestedState("RUNNING") .runningInstances(0) .stack("test-stack") .build())); givenRequestPushApplication(PushApplicationManifestRequest.builder() .manifest(ApplicationManifest.builder() .path(resource.getFile().toPath()) .buildpack(deploymentProperties.getBuildpack()) .disk(1024) .instances(1) .memory(1024) .name("test-application-id") .build()) .stagingTimeout(this.deploymentProperties.getStagingTimeout()) .startupTimeout(this.deploymentProperties.getStartupTimeout()) .build(), Mono.empty()); deployer.deploy( new AppDeploymentRequest(new AppDefinition("test-application", Collections.emptyMap()), resource, Collections.EMPTY_MAP)); } @SuppressWarnings("unchecked") @Test public void deployWithServiceParameters() throws IOException { Resource resource = new FileSystemResource("src/test/resources/demo-0.0.1-SNAPSHOT.jar"); given(this.applicationNameGenerator.generateAppName("test-application")).willReturn("test-application-id"); given(this.services.bind(any(BindServiceInstanceRequest.class))) .willReturn(Mono.empty()); given(this.applications.start(any(StartApplicationRequest.class))) .willReturn(Mono.empty()); givenRequestGetApplication("test-application-id", Mono.error(new IllegalArgumentException()), Mono.just( ApplicationDetail.builder() .diskQuota(0) .id("test-application-id") .instances(1) .memoryLimit(0) .name("test-application") .requestedState("RUNNING") .runningInstances(0) .stack("test-stack") .build())); givenRequestPushApplication(PushApplicationManifestRequest.builder() .manifest(ApplicationManifest.builder() .path(resource.getFile().toPath()) .buildpack(deploymentProperties.getBuildpack()) .disk(1024) .environmentVariables(defaultEnvironmentVariables()) .instances(1) .memory(1024) .name("test-application-id") .service("test-service-2") .service("test-service-1") .build()) .stagingTimeout(this.deploymentProperties.getStagingTimeout()) .startupTimeout(this.deploymentProperties.getStartupTimeout()) .build(), Mono.empty()); String deploymentId = this.deployer.deploy( new AppDeploymentRequest(new AppDefinition("test-application", Collections.emptyMap()), resource, Collections.singletonMap( CloudFoundryDeploymentProperties.SERVICES_PROPERTY_KEY, "'test-service-3 foo:bar'"))); assertThat(deploymentId).isEqualTo("test-application-id"); } @SuppressWarnings("unchecked") @Test public void deployWithServiceParametersAndBindingError() throws Exception { Resource resource = new FileSystemResource("src/test/resources/demo-0.0.1-SNAPSHOT.jar"); given(this.applicationNameGenerator.generateAppName("test-application")).willReturn("test-application-id"); AtomicInteger count = new AtomicInteger(); CountDownLatch okLatch = new CountDownLatch(1); ClientV2Exception e = new ClientV2Exception(500, 10001, "The service broker could not perform this operation in parallel with other running operations", "CF-ConcurrencyError"); // fail 2 times given(this.services.bind(any(BindServiceInstanceRequest.class))) .will(x -> { if (count.getAndIncrement() < 2) { return Mono.error(e); } okLatch.countDown(); return Mono.empty(); }); given(this.applications.start(any(StartApplicationRequest.class))) .willReturn(Mono.empty()); givenRequestGetApplication("test-application-id", Mono.error(new IllegalArgumentException()), Mono.just( ApplicationDetail.builder() .diskQuota(0) .id("test-application-id") .instances(1) .memoryLimit(0) .name("test-application") .requestedState("RUNNING") .runningInstances(0) .stack("test-stack") .build())); givenRequestPushApplication(PushApplicationManifestRequest.builder() .manifest(ApplicationManifest.builder() .path(resource.getFile().toPath()) .buildpack(deploymentProperties.getBuildpack()) .disk(1024) .environmentVariables(defaultEnvironmentVariables()) .instances(1) .memory(1024) .name("test-application-id") .service("test-service-2") .service("test-service-1") .build()) .stagingTimeout(this.deploymentProperties.getStagingTimeout()) .startupTimeout(this.deploymentProperties.getStartupTimeout()) .build(), Mono.empty()); String deploymentId = this.deployer.deploy( new AppDeploymentRequest(new AppDefinition("test-application", Collections.emptyMap()), resource, Collections.singletonMap( CloudFoundryDeploymentProperties.SERVICES_PROPERTY_KEY, "'test-service-3 foo:bar'"))); assertThat(deploymentId).isEqualTo("test-application-id"); // deploy is actually subscribe and forget so need to wait and // check that we got retries. assertThat(okLatch.await(30, TimeUnit.SECONDS)).isTrue(); assertThat(count.get()).isEqualTo(3); } @SuppressWarnings("unchecked") @Test public void deployWithAdditionalProperties() throws IOException { Resource resource = new FileSystemResource("src/test/resources/demo-0.0.1-SNAPSHOT.jar"); given(this.applicationNameGenerator.generateAppName("test-application")).willReturn("test-application-id"); givenRequestGetApplication("test-application-id", Mono.error(new IllegalArgumentException()), Mono.just( ApplicationDetail.builder() .diskQuota(0) .id("test-application-id") .instances(1) .memoryLimit(0) .name("test-application") .requestedState("RUNNING") .runningInstances(0) .stack("test-stack") .build())); Map environmentVariables = new HashMap<>(); environmentVariables.put("test-key-1", "test-value-1"); addGuidAndIndex(environmentVariables); givenRequestPushApplication(PushApplicationManifestRequest.builder() .manifest(ApplicationManifest.builder() .path(resource.getFile().toPath()) .buildpack(deploymentProperties.getBuildpack()) .disk(1024) .environmentVariables(environmentVariables) .instances(1) .memory(1024) .name("test-application-id") .service("test-service-2") .service("test-service-1") .build()) .stagingTimeout(this.deploymentProperties.getStagingTimeout()) .startupTimeout(this.deploymentProperties.getStartupTimeout()) .build(), Mono.empty()); String deploymentId = this.deployer.deploy(new AppDeploymentRequest( new AppDefinition("test-application", Collections.singletonMap("test-key-1", "test-value-1")), resource, FluentMap.builder().entry("test-key-2", "test-value-2") .entry(CloudFoundryDeploymentProperties.USE_SPRING_APPLICATION_JSON_KEY, String.valueOf(false)) .build())); assertThat(deploymentId).isEqualTo("test-application-id"); } @SuppressWarnings("unchecked") @Test public void deployWithAdditionalPropertiesInSpringApplicationJson() throws IOException { Resource resource = new FileSystemResource("src/test/resources/demo-0.0.1-SNAPSHOT.jar"); given(this.applicationNameGenerator.generateAppName("test-application")).willReturn("test-application-id"); givenRequestGetApplication("test-application-id", Mono.error(new IllegalArgumentException()), Mono.just( ApplicationDetail.builder() .diskQuota(0) .id("test-application-id") .instances(1) .memoryLimit(0) .name("test-application") .requestedState("RUNNING") .runningInstances(0) .stack("test-stack") .build())); Map environmentVariables = new HashMap<>(); environmentVariables.put("SPRING_APPLICATION_JSON", "{\"test-key-1\":\"test-value-1\"}"); addGuidAndIndex(environmentVariables); givenRequestPushApplication(PushApplicationManifestRequest.builder() .manifest(ApplicationManifest.builder() .path(resource.getFile().toPath()) .buildpack(deploymentProperties.getBuildpack()) .disk(1024) .environmentVariables(environmentVariables) .instances(1) .memory(1024) .name("test-application-id") .service("test-service-2") .service("test-service-1") .build()) .stagingTimeout(this.deploymentProperties.getStagingTimeout()) .startupTimeout(this.deploymentProperties.getStartupTimeout()) .build(), Mono.empty()); String deploymentId = this.deployer.deploy(new AppDeploymentRequest( new AppDefinition("test-application", Collections.singletonMap("test-key-1", "test-value-1")), resource, Collections.singletonMap("test-key-2", "test-value-2"))); assertThat(deploymentId).isEqualTo("test-application-id"); } @Test public void deployWithDeployerEnvironmentVariables() throws IOException { Resource resource = new FileSystemResource("src/test/resources/demo-0.0.1-SNAPSHOT.jar"); given(this.applicationNameGenerator.generateAppName("test-application")).willReturn("test-application-id"); givenRequestGetApplication("test-application-id", Mono.error(new IllegalArgumentException()), applicationDetailRunningApp()); givenRequestPushApplication(PushApplicationManifestRequest.builder() .manifest(ApplicationManifest.builder() .path(resource.getFile().toPath()) .buildpack("test-buildpack") .disk(0) .environmentVariables(defaultEnvironmentVariables()) .healthCheckType(ApplicationHealthCheck.NONE) .instances(0) .memory(0) .name("test-application-id") .noRoute(false) .host("test-host") .domain("test-domain") .service("test-service-2") .service("test-service-1") .build()) .build(), Mono.empty()); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(new AppDefinition("test-application", Collections.emptyMap()), resource, FluentMap.builder().entry(CloudFoundryDeploymentProperties.BUILDPACK_PROPERTY_KEY, "test-buildpack") .entry(AppDeployer.DISK_PROPERTY_KEY, "0") .entry(CloudFoundryDeploymentProperties.DOMAIN_PROPERTY, "test-domain") .entry(CloudFoundryDeploymentProperties.HEALTHCHECK_PROPERTY_KEY, "none") .entry(CloudFoundryDeploymentProperties.HOST_PROPERTY, "test-host") .entry(AppDeployer.COUNT_PROPERTY_KEY, "0") .entry(AppDeployer.MEMORY_PROPERTY_KEY, "0") .entry(CloudFoundryDeploymentProperties.NO_ROUTE_PROPERTY, "false") .entry(CloudFoundryDeploymentProperties.ROUTE_PATH_PROPERTY, "/test-route-path") .entry(CloudFoundryDeploymentProperties.ENV_KEY + ".JBP_CONFIG_SPRING_AUTO_RECONFIGURATION", CfEnvConfigurer.ENABLED_FALSE) .entry(CloudFoundryDeploymentProperties.ENV_KEY + ".SPRING_PROFILES_ACTIVE", "cloud,foo") .build()); String deploymentId = deployer.deploy(appDeploymentRequest); Map merged = this.deployer.mergeEnvironmentVariables("test-application-id", appDeploymentRequest); assertThat(merged).containsEntry("JBP_CONFIG_SPRING_AUTO_RECONFIGURATION", CfEnvConfigurer.ENABLED_FALSE); assertThat(merged).containsEntry("SPRING_PROFILES_ACTIVE", "cloud,foo"); assertThat(merged).containsEntry("SOME_GLOBAL_PROPERTY", "someGlobalValue"); assertThat(merged).containsKey("SPRING_APPLICATION_JSON"); assertThat(merged.get("SPRING_CLOUD_STREAMAPP_SECURITY_ADMIN-USER")).isEqualTo("user"); assertThat(merged.get("SPRING_CLOUD_STREAMAPP_SECURITY_ADMIN-PASSWORD")).isEqualTo("password"); assertThat(deploymentId).isEqualTo("test-application-id"); } private Mono applicationDetailRunningApp() { return Mono.just( ApplicationDetail.builder() .diskQuota(0) .id("test-application-id") .instances(1) .memoryLimit(0) .name("test-application") .requestedState("RUNNING") .runningInstances(0) .stack("test-stack") .build()); } @Test public void automaticallyConfigureForCfEnv() throws JsonProcessingException { Resource resource = new FileSystemResource("src/test/resources/log-sink-rabbit-3.0.0.BUILD-SNAPSHOT.jar"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(new AppDefinition("test-application", Collections.emptyMap()), resource, Collections.emptyMap()); Map env = deployer.mergeEnvironmentVariables("test-application-id", appDeploymentRequest); assertThat(env).containsEntry("SOME_GLOBAL_PROPERTY", "someGlobalValue"); assertThat(env).containsEntry(CfEnvConfigurer.JBP_CONFIG_SPRING_AUTO_RECONFIGURATION, CfEnvConfigurer.ENABLED_FALSE); assertThat(env).containsKey("SPRING_APPLICATION_JSON"); ObjectMapper objectMapper = new ObjectMapper(); Map saj = objectMapper.readValue(env.get("SPRING_APPLICATION_JSON"), HashMap.class); assertThat(saj).containsEntry(CfEnvConfigurer.SPRING_PROFILES_ACTIVE_FQN, CfEnvConfigurer.CLOUD_PROFILE_NAME); resource = new FileSystemResource("src/test/resources/log-sink-rabbit-2.1.5.RELEASE.jar"); appDeploymentRequest = new AppDeploymentRequest(new AppDefinition("test-application", Collections.emptyMap()), resource, Collections.emptyMap()); env = deployer.mergeEnvironmentVariables("test-application-id", appDeploymentRequest); assertThat(env).doesNotContainKey(CfEnvConfigurer.JBP_CONFIG_SPRING_AUTO_RECONFIGURATION); } @SuppressWarnings("unchecked") @Test public void deployWithApplicationDeploymentProperties() throws IOException { Resource resource = new FileSystemResource("src/test/resources/demo-0.0.1-SNAPSHOT.jar"); given(this.applicationNameGenerator.generateAppName("test-application")).willReturn("test-application-id"); givenRequestGetApplication("test-application-id", Mono.error(new IllegalArgumentException()), Mono.just( ApplicationDetail.builder() .diskQuota(0) .id("test-application-id") .instances(1) .memoryLimit(0) .name("test-application") .requestedState("RUNNING") .runningInstances(0) .stack("test-stack") .build())); givenRequestPushApplication(PushApplicationManifestRequest.builder() .manifest(ApplicationManifest.builder() .path(resource.getFile().toPath()) .buildpack("test-buildpack") .disk(0) .environmentVariables(defaultEnvironmentVariables()) .healthCheckType(ApplicationHealthCheck.NONE) .instances(0) .memory(0) .name("test-application-id") .noRoute(false) .host("test-host") .domain("test-domain") .routePath("/test-route-path") .service("test-service-2") .service("test-service-1") .build()) .stagingTimeout(this.deploymentProperties.getStagingTimeout()) .startupTimeout(this.deploymentProperties.getStartupTimeout()) .build(), Mono.empty()); String deploymentId = this.deployer.deploy( new AppDeploymentRequest(new AppDefinition("test-application", Collections.emptyMap()), resource, FluentMap.builder().entry(CloudFoundryDeploymentProperties.BUILDPACK_PROPERTY_KEY, "test-buildpack") .entry(AppDeployer.DISK_PROPERTY_KEY, "0") .entry(CloudFoundryDeploymentProperties.DOMAIN_PROPERTY, "test-domain") .entry(CloudFoundryDeploymentProperties.HEALTHCHECK_PROPERTY_KEY, "none") .entry(CloudFoundryDeploymentProperties.HOST_PROPERTY, "test-host") .entry(AppDeployer.COUNT_PROPERTY_KEY, "0") .entry(AppDeployer.MEMORY_PROPERTY_KEY, "0") .entry(CloudFoundryDeploymentProperties.NO_ROUTE_PROPERTY, "false") .entry(CloudFoundryDeploymentProperties.ROUTE_PATH_PROPERTY, "/test-route-path") .build())); assertThat(deploymentId).isEqualTo("test-application-id"); } @SuppressWarnings("unchecked") @Test public void deployWithInvalidRoutePathProperty() throws IOException { Resource resource = new FileSystemResource("src/test/resources/demo-0.0.1-SNAPSHOT.jar"); given(this.applicationNameGenerator.generateAppName("test-application")).willReturn("test-application-id"); givenRequestGetApplication("test-application-id", Mono.error(new IllegalArgumentException()), Mono.just(ApplicationDetail.builder() .diskQuota(0) .id("test-application-id") .instances(1) .memoryLimit(0) .name("test-application") .requestedState("RUNNING") .runningInstances(0) .stack("test-stack") .build())); givenRequestPushApplication(PushApplicationManifestRequest.builder() .manifest(ApplicationManifest.builder() .path(resource.getFile().toPath()) .buildpack("test-buildpack") .disk(0) .environmentVariables(defaultEnvironmentVariables()) .healthCheckType(ApplicationHealthCheck.NONE) .instances(0) .memory(0) .name("test-application-id") .noRoute(false) .host("test-host") .domain("test-domain") .routePath("/test-route-path") .service("test-service-2") .service("test-service-1") .build()) .stagingTimeout(this.deploymentProperties.getStagingTimeout()) .startupTimeout(this.deploymentProperties.getStartupTimeout()) .build(), Mono.empty()); try { assertThatThrownBy(() -> { this.deployer.deploy(new AppDeploymentRequest( new AppDefinition("test-application", Collections.emptyMap()), resource, FluentMap.builder() .entry(CloudFoundryDeploymentProperties.BUILDPACK_PROPERTY_KEY, "test-buildpack") .entry(AppDeployer.DISK_PROPERTY_KEY, "0") .entry(CloudFoundryDeploymentProperties.DOMAIN_PROPERTY, "test-domain") .entry(CloudFoundryDeploymentProperties.HEALTHCHECK_PROPERTY_KEY, "none") .entry(CloudFoundryDeploymentProperties.HOST_PROPERTY, "test-host") .entry(AppDeployer.COUNT_PROPERTY_KEY, "0") .entry(AppDeployer.MEMORY_PROPERTY_KEY, "0") .entry(CloudFoundryDeploymentProperties.NO_ROUTE_PROPERTY, "false") .entry(CloudFoundryDeploymentProperties.ROUTE_PATH_PROPERTY, "test-route-path") .build())); }).isInstanceOf(IllegalArgumentException.class).as("Illegal Argument exception is expected."); } catch (IllegalArgumentException e) { assertThat(e.getMessage()).isEqualTo("Cloud Foundry routes must start with \"/\". Route passed = [test-route-path]."); } } @SuppressWarnings("unchecked") @Test public void deployWithCustomDeploymentProperties() throws IOException { Resource resource = new FileSystemResource("src/test/resources/demo-0.0.1-SNAPSHOT.jar"); given(this.applicationNameGenerator.generateAppName("test-application")).willReturn("test-application-id"); givenRequestGetApplication("test-application-id", Mono.error(new IllegalArgumentException()), Mono.just( ApplicationDetail.builder() .diskQuota(0) .id("test-application-id") .instances(1) .memoryLimit(0) .name("test-application") .requestedState("RUNNING") .runningInstances(0) .stack("test-stack") .build())); this.deploymentProperties.setBuildpack("test-buildpack"); this.deploymentProperties.setDisk("0"); this.deploymentProperties.setDomain("test-domain"); this.deploymentProperties.setHealthCheck(ApplicationHealthCheck.NONE); this.deploymentProperties.setHost("test-host"); this.deploymentProperties.setInstances(0); this.deploymentProperties.setMemory("0"); givenRequestPushApplication(PushApplicationManifestRequest.builder() .manifest(ApplicationManifest.builder() .path(resource.getFile().toPath()) .buildpack("test-buildpack") .disk(0) .domain("test-domain") .environmentVariables(defaultEnvironmentVariables()) .healthCheckType(ApplicationHealthCheck.NONE) .host("test-host") .instances(0) .memory(0) .name("test-application-id") .service("test-service-2") .service("test-service-1") .build()) .stagingTimeout(this.deploymentProperties.getStagingTimeout()) .startupTimeout(this.deploymentProperties.getStartupTimeout()) .build(), Mono.empty()); String deploymentId = this.deployer.deploy( new AppDeploymentRequest(new AppDefinition("test-application", Collections.emptyMap()), resource, Collections.emptyMap())); assertThat(deploymentId).isEqualTo("test-application-id"); } @SuppressWarnings("unchecked") @Test public void deployWithMultipleRoutes() throws IOException { Resource resource = new FileSystemResource("src/test/resources/demo-0.0.1-SNAPSHOT.jar"); given(this.applicationNameGenerator.generateAppName("test-application")).willReturn("test-application-id"); givenRequestGetApplication("test-application-id", Mono.error(new IllegalArgumentException()), Mono.just(ApplicationDetail.builder() .diskQuota(0) .id("test-application-id") .instances(1) .memoryLimit(0) .name("test-application") .requestedState("RUNNING") .runningInstances(0) .stack("test-stack") .build())); this.deploymentProperties.setBuildpack("test-buildpack"); this.deploymentProperties.setDisk("0"); this.deploymentProperties.setHealthCheck(ApplicationHealthCheck.NONE); this.deploymentProperties.setRoutes(Sets.newHashSet("route1.test-domain", "route2.test-domain")); this.deploymentProperties.setInstances(0); this.deploymentProperties.setMemory("0"); givenRequestPushApplication(PushApplicationManifestRequest.builder() .manifest(ApplicationManifest.builder() .path(resource.getFile().toPath()) .buildpack("test-buildpack") .disk(0) .routes(Sets.newHashSet( Route.builder().route("route1.test-domain").build(), Route.builder().route("route2.test-domain").build())) .environmentVariables(defaultEnvironmentVariables()) .healthCheckType(ApplicationHealthCheck.NONE) .instances(0) .memory(0) .name("test-application-id") .service("test-service-2") .service("test-service-1") .build()) .stagingTimeout(this.deploymentProperties.getStagingTimeout()) .startupTimeout(this.deploymentProperties.getStartupTimeout()) .build(), Mono.empty()); String deploymentId = this.deployer.deploy(new AppDeploymentRequest( new AppDefinition("test-application", Collections.emptyMap()), resource, Collections.emptyMap())); assertThat(deploymentId).isEqualTo("test-application-id"); } @SuppressWarnings("unchecked") @Test public void deployWithMultipleRoutesAndHostOrDomainMutuallyExclusive() { Resource resource = new FileSystemResource("src/test/resources/demo-0.0.1-SNAPSHOT.jar"); given(this.applicationNameGenerator.generateAppName("test-application")).willReturn("test-application-id"); assertThatThrownBy(() -> { givenRequestGetApplication("test-application-id", Mono.error(new IllegalArgumentException()), Mono.just(ApplicationDetail.builder() .id("test-application-id") .name("test-application") .build())); }).isInstanceOf(IllegalStateException.class) .as("routes and hosts cannot be set, expect cf java client to throw an exception"); this.deploymentProperties.setHost("route0"); this.deploymentProperties.setDomain("test-domain"); this.deploymentProperties.setRoutes(Sets.newHashSet("route1.test-domain", "route2.test-domain")); assertThatThrownBy(() -> { this.deployer.deploy(new AppDeploymentRequest(new AppDefinition("test-application", Collections.emptyMap()), resource, Collections.emptyMap())); }).isInstanceOf(IllegalStateException.class) .as("routes and hosts cannot be set, expect cf java client to throw an exception"); } @SuppressWarnings("unchecked") @Test public void deployWithGroup() throws IOException { Resource resource = new FileSystemResource("src/test/resources/demo-0.0.1-SNAPSHOT.jar"); given(this.applicationNameGenerator.generateAppName("test-group-test-application")).willReturn( "test-group-test-application-id"); givenRequestGetApplication("test-group-test-application-id", Mono.error(new IllegalArgumentException()), Mono.just(ApplicationDetail.builder() .diskQuota(0) .id("test-group-test-application-id") .instances(1) .memoryLimit(0) .name("test-group-test-application") .requestedState("RUNNING") .runningInstances(0) .stack("test-stack") .build())); givenRequestPushApplication(PushApplicationManifestRequest.builder() .manifest(ApplicationManifest.builder() .path(resource.getFile().toPath()) .buildpack(deploymentProperties.getBuildpack()) .disk(1024) .environmentVariable("SPRING_CLOUD_APPLICATION_GROUP", "test-group") .environmentVariable("SPRING_APPLICATION_JSON", "{}") .environmentVariable("SPRING_APPLICATION_INDEX", "${vcap.application.instance_index}") .environmentVariable("SPRING_CLOUD_APPLICATION_GUID", "${vcap.application.name}:${vcap.application.instance_index}") .instances(1) .memory(1024) .name("test-group-test-application-id") .service("test-service-2") .service("test-service-1") .build()) .stagingTimeout(this.deploymentProperties.getStagingTimeout()) .startupTimeout(this.deploymentProperties.getStartupTimeout()) .build(), Mono.empty()); String deploymentId = this.deployer.deploy( new AppDeploymentRequest(new AppDefinition("test-application", Collections.emptyMap()), resource, Collections.singletonMap(AppDeployer.GROUP_PROPERTY_KEY, "test-group"))); assertThat(deploymentId).isEqualTo("test-group-test-application-id"); } @SuppressWarnings("unchecked") @Test public void deployDockerResource() { Resource resource = new DockerResource("somecorp/someimage:latest"); given(this.applicationNameGenerator.generateAppName("test-application")).willReturn("test-application-id"); givenRequestGetApplication("test-application-id", Mono.error(new IllegalArgumentException()), Mono.just( ApplicationDetail.builder() .diskQuota(0) .id("test-application-id") .instances(1) .memoryLimit(0) .name("test-application") .requestedState("RUNNING") .runningInstances(0) .stack("test-stack") .build())); givenRequestPushApplication(PushApplicationManifestRequest.builder() .manifest(ApplicationManifest.builder() .docker(Docker.builder().image("somecorp/someimage:latest").build()) .disk(1024) .environmentVariables(defaultEnvironmentVariables()) .instances(1) .memory(1024) .name("test-application-id") .service("test-service-2") .service("test-service-1") .build()) .stagingTimeout(this.deploymentProperties.getStagingTimeout()) .startupTimeout(this.deploymentProperties.getStartupTimeout()) .build(), Mono.empty()); String deploymentId = this.deployer.deploy( new AppDeploymentRequest(new AppDefinition("test-application", Collections.emptyMap()), resource, Collections.emptyMap())); assertThat(deploymentId).isEqualTo("test-application-id"); } @SuppressWarnings("unchecked") @Test public void statusOfCrashedApplicationIsFailed() { givenRequestGetApplication("test-application-id", Mono.just(ApplicationDetail.builder() .diskQuota(0) .id("test-application-id") .instances(1) .memoryLimit(0) .name("test-application") .requestedState("RUNNING") .runningInstances(1) .stack("test-stack") .instanceDetail(InstanceDetail.builder().state("CRASHED").index("1").build()) .build())); AppStatus status = this.deployer.status("test-application-id"); assertThat(status.getState()).isEqualTo(DeploymentState.failed); } @SuppressWarnings("unchecked") @Test public void statusOfDownApplicationIsDeploying() { givenRequestGetApplication("test-application-id", Mono.just(ApplicationDetail.builder() .diskQuota(0) .id("test-application-id") .instances(1) .memoryLimit(0) .name("test-application") .requestedState("RUNNING") .runningInstances(1) .stack("test-stack") .instanceDetail(InstanceDetail.builder().state("DOWN").index("1").build()) .build())); AppStatus status = this.deployer.status("test-application-id"); assertThat(status.getState()).isEqualTo(DeploymentState.deploying); } @SuppressWarnings("unchecked") @Test public void statusOfFlappingApplicationIsDeployed() { givenRequestGetApplication("test-application-id", Mono.just(ApplicationDetail.builder() .diskQuota(0) .id("test-application-id") .instances(1) .memoryLimit(0) .name("test-application") .requestedState("RUNNING") .runningInstances(1) .stack("test-stack") .instanceDetail(InstanceDetail.builder().state("FLAPPING").index("1").build()) .build())); AppStatus status = deployer.status("test-application-id"); assertThat(status.getState()).isEqualTo(DeploymentState.deployed); } @SuppressWarnings("unchecked") @Test public void statusOfRunningApplicationIsDeployed() { givenRequestGetApplication("test-application-id", Mono.just(ApplicationDetail.builder() .diskQuota(0) .id("test-application-id") .instances(1) .memoryLimit(0) .name("test-application") .requestedState("RUNNING") .runningInstances(1) .stack("test-stack") .instanceDetail(InstanceDetail.builder().state("RUNNING").index("1").build()) .build())); AppStatus status = this.deployer.status("test-application-id"); assertThat(status.getState()).isEqualTo(DeploymentState.deployed); assertThat(status.getInstances().get("test-application-0").toString()) .isEqualTo("CloudFoundryAppInstanceStatus[test-application-0 : deployed]"); assertThat(status.getInstances().get("test-application-0").getAttributes()) .containsOnly( MapEntry.entry(CloudFoundryAppInstanceStatus.GUID, "test-application:0"), MapEntry.entry(CloudFoundryAppInstanceStatus.INDEX, "0"), MapEntry.entry(CloudFoundryAppInstanceStatus.CF_GUID, "test-application-id")); } @SuppressWarnings("unchecked") @Test public void statusOfStartingApplicationIsDeploying() { givenRequestGetApplication("test-application-id", Mono.just(ApplicationDetail.builder() .diskQuota(0) .id("test-application-id") .instances(1) .memoryLimit(0) .name("test-application") .requestedState("RUNNING") .runningInstances(1) .stack("test-stack") .instanceDetail(InstanceDetail.builder().state("STARTING").index("1").build()) .build())); AppStatus status = this.deployer.status("test-application-id"); assertThat(status.getState()).isEqualTo(DeploymentState.deploying); } @SuppressWarnings("unchecked") @Test public void statusOfUnknownApplicationIsUnknown() { givenRequestGetApplication("test-application-id", Mono.just(ApplicationDetail.builder() .diskQuota(0) .id("test-application-id") .instances(1) .memoryLimit(0) .name("test-application") .requestedState("RUNNING") .runningInstances(1) .stack("test-stack") .instanceDetail(InstanceDetail.builder().state("UNKNOWN").index("1").build()) .build())); AppStatus status = this.deployer.status("test-application-id"); assertThat(status.getState()).isEqualTo(DeploymentState.unknown); } @SuppressWarnings("unchecked") @Test public void statusWithAbnormalInstanceStateThrowsException() { givenRequestGetApplication("test-application-id", Mono.just(ApplicationDetail.builder() .diskQuota(0) .id("test-application-id") .instances(1) .memoryLimit(0) .name("test-application") .requestedState("RUNNING") .runningInstances(1) .stack("test-stack") .instanceDetail(InstanceDetail.builder().state("ABNORMAL").index("1").build()) .build())); assertThatThrownBy(() -> { this.deployer.status("test-application-id").getState(); }).isInstanceOf(IllegalStateException.class).hasMessageContaining("Unsupported CF state"); } @SuppressWarnings("unchecked") @Test public void statusWithFailingCAPICallRetries() { AtomicInteger i = new AtomicInteger(); Mono m = Mono.create(s -> { if (i.incrementAndGet() == 2) { s.success(ApplicationDetail.builder() .diskQuota(0) .id("test-application-id") .instances(1) .memoryLimit(0) .name("test-application") .requestedState("RUNNING") .runningInstances(1) .stack("test-stack") .instanceDetail(InstanceDetail.builder().state("UNKNOWN").index("1").build()) .build()); } else { s.error(new RuntimeException("Simulated Server Side error")); } }); givenRequestGetApplication("test-application-id", m); DeploymentState state = this.deployer.status("test-application-id").getState(); assertThat(state).isEqualTo(DeploymentState.unknown); } @SuppressWarnings("unchecked") @Test public void statusWithFailingCAPICallRetriesEventualError() { AtomicInteger i = new AtomicInteger(); Mono m = Mono.create(s -> { if (i.incrementAndGet() == 12) { // 12 is more than the number of retries s.success(ApplicationDetail.builder() .diskQuota(0) .id("test-application-id") .instances(1) .memoryLimit(0) .name("test-application") .requestedState("RUNNING") .runningInstances(1) .stack("test-stack") .instanceDetail(InstanceDetail.builder().state("UNKNOWN").index("1").build()) .build()); } else { s.error(new RuntimeException("Simulated Server Side error")); } }); givenRequestGetApplication("test-application-id", m); this.deployer.deploymentProperties.setStatusTimeout(200); // Will cause wait of 20ms then 40ms,80ms DeploymentState state = this.deployer.status("test-application-id").getState(); assertThat(state).isEqualTo(DeploymentState.error); } @SuppressWarnings("unchecked") @Test public void statusWithErrorThrownOnBlocking() { AtomicInteger i = new AtomicInteger(); Mono m = Mono.delay(Duration.ofSeconds(30)).then(Mono.create(s -> { i.incrementAndGet(); s.success(ApplicationDetail.builder() .diskQuota(0) .id("test-application-id") .instances(1) .memoryLimit(0) .name("test-application") .requestedState("RUNNING") .runningInstances(1) .stack("test-stack") .instanceDetail(InstanceDetail.builder().state("UNKNOWN").index("1").build()) .build()); })); givenRequestGetApplication("test-application-id", m); this.deployer.deploymentProperties.setStatusTimeout(1);// Is less than the delay() above DeploymentState state = this.deployer.status("test-application-id").getState(); assertThat(state).isEqualTo(DeploymentState.error); assertThat(i.get()).isEqualTo(0); } @SuppressWarnings("unchecked") @Test public void undeploy() { givenRequestGetApplication("test-application-id", Mono.just(ApplicationDetail.builder() .diskQuota(0) .id("test-application-id") .instances(1) .memoryLimit(0) .name("test-application") .requestedState("RUNNING") .runningInstances(1) .stack("test-stack") .instanceDetail(InstanceDetail.builder().state("RUNNING").index("1").build()) .build())); givenRequestDeleteApplication("test-application-id", Mono.empty()); this.deployer.undeploy("test-application-id"); } @SuppressWarnings("unchecked") @Test public void scale() { givenRequestGetApplication("test-application-id", Mono.just(ApplicationDetail.builder() .diskQuota(0) .id("test-application-id") .instances(2) .memoryLimit(0) .name("test-application") .requestedState("RUNNING") .runningInstances(2) .stack("test-stack") .instanceDetail(InstanceDetail.builder().state("RUNNING").index("1").build()) .build())); givenRequestScaleApplication("test-application-id", 2, 1024, 1024, Mono.empty()); this.deployer.scale(new AppScaleRequest("test-application-id", 2)); } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/test/java/org/springframework/cloud/deployer/spi/cloudfoundry/CloudFoundryAppNameGeneratorTest.java ================================================ /* * Copyright 2016-2021 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.cloud.deployer.spi.cloudfoundry; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** * @author Soby Chacko * @author Mark Pollack */ public class CloudFoundryAppNameGeneratorTest { @Test public void testDeploymentIdWithAppNamePrefixAndRandomAppNamePrefixFalse() throws Exception { CloudFoundryDeploymentProperties properties = new CloudFoundryDeploymentProperties(); properties.setEnableRandomAppNamePrefix(false); properties.setAppNamePrefix("dataflow"); CloudFoundryAppNameGenerator deploymentCustomizer = new CloudFoundryAppNameGenerator(properties); deploymentCustomizer.afterPropertiesSet(); assertThat(deploymentCustomizer.generateAppName("foo")).isEqualTo("dataflow-foo"); } @Test public void testDeploymentIdWithAppNamePrefixAndRandomAppNamePrefixTrue() throws Exception { CloudFoundryDeploymentProperties properties = new CloudFoundryDeploymentProperties(); properties.setEnableRandomAppNamePrefix(true); properties.setAppNamePrefix("dataflow-longername"); CloudFoundryAppNameGenerator deploymentCustomizer = new CloudFoundryAppNameGenerator(properties); deploymentCustomizer.afterPropertiesSet(); String deploymentIdWithUniquePrefix = deploymentCustomizer.generateAppName("foo"); assertThat(deploymentIdWithUniquePrefix).matches("dataflow-longername-\\w+-foo"); String deploymentIdWithUniquePrefixAgain = deploymentCustomizer.generateAppName("foo"); assertThat(deploymentIdWithUniquePrefix).isEqualTo(deploymentIdWithUniquePrefixAgain); } @Test public void testDeploymentIdWithoutAppNamePrefixAndRandomAppNamePrefixTrue() throws Exception { CloudFoundryDeploymentProperties properties = new CloudFoundryDeploymentProperties(); properties.setEnableRandomAppNamePrefix(true); properties.setAppNamePrefix(""); CloudFoundryAppNameGenerator deploymentCustomizer = new CloudFoundryAppNameGenerator(properties); deploymentCustomizer.afterPropertiesSet(); String deploymentIdWithUniquePrefix = deploymentCustomizer.generateAppName("foo"); assertThat(deploymentIdWithUniquePrefix).matches("\\w+-foo"); } @Test public void testDeploymentIdWithoutAppNamePrefixAndRandomAppNamePrefixFalse() throws Exception { CloudFoundryDeploymentProperties properties = new CloudFoundryDeploymentProperties(); properties.setEnableRandomAppNamePrefix(false); properties.setAppNamePrefix(""); CloudFoundryAppNameGenerator deploymentCustomizer = new CloudFoundryAppNameGenerator(properties); deploymentCustomizer.afterPropertiesSet(); assertThat(deploymentCustomizer.generateAppName("foo")).isEqualTo("foo"); } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/test/java/org/springframework/cloud/deployer/spi/cloudfoundry/CloudFoundryConnectionPropertiesTests.java ================================================ /* * Copyright 2019-2021 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.cloud.deployer.spi.cloudfoundry; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.core.env.StandardEnvironment; import org.springframework.core.env.SystemEnvironmentPropertySource; import static org.assertj.core.api.Assertions.assertThat; public class CloudFoundryConnectionPropertiesTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner(); @Test public void setAllProperties() { this.contextRunner .withInitializer(context -> { Map map = new HashMap<>(); map.put("spring.cloud.deployer.cloudfoundry.org", "org"); map.put("spring.cloud.deployer.cloudfoundry.space", "space"); map.put("spring.cloud.deployer.cloudfoundry.url", "http://example.com"); map.put("spring.cloud.deployer.cloudfoundry.username", "username"); map.put("spring.cloud.deployer.cloudfoundry.password", "password"); map.put("spring.cloud.deployer.cloudfoundry.client-id", "id"); map.put("spring.cloud.deployer.cloudfoundry.client-secret", "secret"); map.put("spring.cloud.deployer.cloudfoundry.login-hint", "hint"); map.put("spring.cloud.deployer.cloudfoundry.skip-ssl-validation", "true"); context.getEnvironment().getPropertySources().addLast(new SystemEnvironmentPropertySource( StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, map)); }) .withUserConfiguration(Config1.class) .run((context) -> { CloudFoundryConnectionProperties properties = context.getBean(CloudFoundryConnectionProperties.class); assertThat(properties.getOrg()).isEqualTo("org"); assertThat(properties.getSpace()).isEqualTo("space"); assertThat(properties.getUrl().toString()).isEqualTo("http://example.com"); assertThat(properties.getUsername()).isEqualTo("username"); assertThat(properties.getPassword()).isEqualTo("password"); assertThat(properties.getClientId()).isEqualTo("id"); assertThat(properties.getClientSecret()).isEqualTo("secret"); assertThat(properties.getLoginHint()).isEqualTo("hint"); assertThat(properties.isSkipSslValidation()).isTrue(); }); } @EnableConfigurationProperties private static class Config1 { @Bean @ConfigurationProperties(prefix = CloudFoundryConnectionProperties.CLOUDFOUNDRY_PROPERTIES) public TestCloudFoundryConnectionProperties testCloudFoundryConnectionProperties() { return new TestCloudFoundryConnectionProperties(); } } private static class TestCloudFoundryConnectionProperties extends CloudFoundryConnectionProperties { // not to get Configuration Processor @Bean Duplicate Prefix Definition error } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/test/java/org/springframework/cloud/deployer/spi/cloudfoundry/CloudFoundryDeployerTests.java ================================================ /* * Copyright 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.cloud.deployer.spi.cloudfoundry; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.junit.jupiter.api.Test; import org.mockito.Answers; import org.mockito.Mock; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import static org.assertj.core.api.Assertions.assertThat; public class CloudFoundryDeployerTests { @Mock(answer = Answers.RETURNS_SMART_NULLS) private RuntimeEnvironmentInfo runtimeEnvironmentInfo; private Resource resource = new FileSystemResource("src/test/resources/demo-0.0.1-SNAPSHOT.jar"); private AppDefinition definition = new AppDefinition("test-application", Collections.emptyMap()); @Test public void testBuildpacksDefault() { CloudFoundryDeploymentProperties props = new CloudFoundryDeploymentProperties(); TestCloudFoundryDeployer deployer = new TestCloudFoundryDeployer(props, runtimeEnvironmentInfo); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); Set buildpacks = deployer.buildpacks(request); assertThat(buildpacks).hasSize(1); } @Test public void testBuildpacksSingleMultiLogic() { CloudFoundryDeploymentProperties props = new CloudFoundryDeploymentProperties(); props.setBuildpack("buildpack1"); TestCloudFoundryDeployer deployer = new TestCloudFoundryDeployer(props, runtimeEnvironmentInfo); Map deploymentProperties = new HashMap<>(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, deploymentProperties, null); Set buildpacks = deployer.buildpacks(request); assertThat(buildpacks).hasSize(1); assertThat(buildpacks).contains("buildpack1"); props.setBuildpacks(new HashSet<>(Arrays.asList("buildpack2", "buildpack3"))); buildpacks = deployer.buildpacks(request); assertThat(buildpacks).hasSize(2); assertThat(buildpacks).contains("buildpack2", "buildpack3"); deploymentProperties.put(CloudFoundryDeploymentProperties.BUILDPACK_PROPERTY_KEY, "buildpack4"); buildpacks = deployer.buildpacks(request); assertThat(buildpacks).hasSize(1); assertThat(buildpacks).contains("buildpack4"); deploymentProperties.put(CloudFoundryDeploymentProperties.BUILDPACKS_PROPERTY_KEY, "buildpack5,buildpack6"); buildpacks = deployer.buildpacks(request); assertThat(buildpacks).hasSize(2); assertThat(buildpacks).contains("buildpack5", "buildpack6"); } private static class TestCloudFoundryDeployer extends AbstractCloudFoundryDeployer { TestCloudFoundryDeployer(CloudFoundryDeploymentProperties deploymentProperties, RuntimeEnvironmentInfo runtimeEnvironmentInfo) { super(deploymentProperties, runtimeEnvironmentInfo); } } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/test/java/org/springframework/cloud/deployer/spi/cloudfoundry/CloudFoundryTaskLauncherCachingTests.java ================================================ /* * Copyright 2020-2021 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.cloud.deployer.spi.cloudfoundry; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.Metadata; import org.cloudfoundry.client.v2.organizations.ListOrganizationsResponse; import org.cloudfoundry.client.v2.organizations.OrganizationResource; import org.cloudfoundry.client.v2.organizations.Organizations; import org.cloudfoundry.client.v2.spaces.ListSpacesResponse; import org.cloudfoundry.client.v2.spaces.SpaceResource; import org.cloudfoundry.client.v2.spaces.Spaces; import org.cloudfoundry.client.v3.Pagination; import org.cloudfoundry.client.v3.tasks.ListTasksResponse; import org.cloudfoundry.client.v3.tasks.TaskResource; import org.cloudfoundry.client.v3.tasks.TaskState; import org.cloudfoundry.client.v3.tasks.Tasks; import org.cloudfoundry.logcache.v1.LogCacheClient; import org.cloudfoundry.operations.CloudFoundryOperations; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; public class CloudFoundryTaskLauncherCachingTests { @Test public void testOrgSpaceCachingRetries() { CloudFoundryClient client = mock(CloudFoundryClient.class); AtomicBoolean spaceError = new AtomicBoolean(true); AtomicBoolean orgError = new AtomicBoolean(true); Spaces spaces = mock(Spaces.class); given(client.spaces()).willReturn(spaces); given(spaces.list(any())).willReturn(listSpacesResponse(spaceError)); Organizations organizations = mock(Organizations.class); given(client.organizations()).willReturn(organizations); given(organizations.list(any())).willReturn(listOrganizationsResponse(orgError)); Tasks tasks = mock(Tasks.class); given(client.tasks()).willReturn(tasks); given(tasks.list(any())).willReturn(runningTasksResponse()); LogCacheClient logCacheClient = mock(LogCacheClient.class); CloudFoundryDeploymentProperties deploymentProperties = new CloudFoundryDeploymentProperties(); CloudFoundryOperations operations = mock(CloudFoundryOperations.class); RuntimeEnvironmentInfo runtimeEnvironmentInfo = mock(RuntimeEnvironmentInfo.class); Map orgAndSpace = new HashMap<>(); orgAndSpace.put(CloudFoundryPlatformSpecificInfo.ORG, "this-org"); orgAndSpace.put(CloudFoundryPlatformSpecificInfo.SPACE, "this-space"); given(runtimeEnvironmentInfo.getPlatformSpecificInfo()).willReturn(orgAndSpace); CloudFoundryTaskLauncher launcher = new CloudFoundryTaskLauncher(client, deploymentProperties, operations, runtimeEnvironmentInfo, new ApplicationLogAccessor(logCacheClient)); Throwable thrown1 = catchThrowable(() -> { launcher.getRunningTaskExecutionCount(); }); assertThat(thrown1).isInstanceOf(RuntimeException.class).hasNoCause(); // space should still error orgError.set(false); Throwable thrown2 = catchThrowable(() -> { launcher.getRunningTaskExecutionCount(); }); assertThat(thrown2).isInstanceOf(RuntimeException.class).hasNoCause(); // cache should now be getting cleared as space doesn't error spaceError.set(false); Throwable thrown3 = catchThrowable(() -> { launcher.getRunningTaskExecutionCount(); }); assertThat(thrown3).doesNotThrowAnyException(); assertThat(launcher.getRunningTaskExecutionCount()).isEqualTo(1); } private Mono listOrganizationsResponse(AtomicBoolean error) { // defer so that we can conditionally throw within mono return Mono.defer(() -> { if (error.get()) { throw new RuntimeException(); } ListOrganizationsResponse response = ListOrganizationsResponse.builder() .addAllResources(Collections.singletonList( OrganizationResource.builder() .metadata(Metadata.builder().id("123").build()).build()) ) .build(); return Mono.just(response); }); } private Mono listSpacesResponse(AtomicBoolean error) { // defer so that we can conditionally throw within mono return Mono.defer(() -> { if (error.get()) { throw new RuntimeException(); } ListSpacesResponse response = ListSpacesResponse.builder() .addAllResources(Collections.singletonList( SpaceResource.builder() .metadata(Metadata.builder().id("123").build()).build()) ) .build(); return Mono.just(response); }); } private Mono runningTasksResponse() { List taskResources = new ArrayList<>(); for (int i = 0; i < 1; i++) { taskResources.add(TaskResource.builder() .name("task-" + i) .dropletId(UUID.randomUUID().toString()) .id(UUID.randomUUID().toString()) .diskInMb(2048) .sequenceId(i) .state(TaskState.RUNNING) .memoryInMb(2048) .createdAt(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))) .build()); } ListTasksResponse listTasksResponse = ListTasksResponse.builder().resources(taskResources) .pagination(Pagination.builder().totalResults(taskResources.size()).build()) .build(); return Mono.just(listTasksResponse); } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/test/java/org/springframework/cloud/deployer/spi/cloudfoundry/CloudFoundryTaskLauncherIntegrationIT.java ================================================ /* * Copyright 2016-2021 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.cloud.deployer.spi.cloudfoundry; import com.github.zafarkhaja.semver.Version; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.deployer.spi.task.TaskLauncher; import org.springframework.cloud.deployer.spi.test.AbstractTaskLauncherIntegrationJUnit5Tests; import org.springframework.cloud.deployer.spi.test.Timeout; import org.springframework.context.annotation.Configuration; import org.springframework.test.context.ContextConfiguration; /** * Runs integration tests for {@link CloudFoundryTaskLauncher}, using the production configuration, * that may be configured via {@link CloudFoundryConnectionProperties}. * * Tests are only run if a successful connection can be made at startup. * * @author Eric Bottard * @author Greg Turnquist * @author Michael Minella * @author Ben Hale */ @ContextConfiguration(classes=CloudFoundryTaskLauncherIntegrationIT.Config.class) public class CloudFoundryTaskLauncherIntegrationIT extends AbstractTaskLauncherIntegrationJUnit5Tests { @Autowired private TaskLauncher taskLauncher; @Autowired private Version cloudControllerAPIVersion; /** * Execution environments may override this default value to have tests wait longer for a deployment, for example if * running in an environment that is known to be slow. */ protected double timeoutMultiplier = 1.0D; protected int maxRetries = 60; @BeforeEach public void init() { Assumptions.assumeTrue(cloudControllerAPIVersion.greaterThanOrEqualTo(Version.forIntegers(2, 65, 0)), "Skipping TaskLauncher ITs on PCF<1.9 (2.65.0). Actual API version is " + cloudControllerAPIVersion); String multiplier = System.getenv("CF_DEPLOYER_TIMEOUT_MULTIPLIER"); if (multiplier != null) { timeoutMultiplier = Double.parseDouble(multiplier); } } @Override protected TaskLauncher provideTaskLauncher() { return taskLauncher; } /* * Allow for a small pause so that each each TL.destroy() at the end of tests actually completes, * as this is asynchronous. */ @AfterEach public void pause() throws InterruptedException { Thread.sleep(500); } @Test @Override @Disabled("CF Deployer incorrectly reports status as failed instead of canceled") public void testSimpleCancel() throws InterruptedException { super.testSimpleCancel(); } @Override protected Timeout deploymentTimeout() { return new Timeout(maxRetries, (int) (5000 * timeoutMultiplier)); } @Override protected Timeout undeploymentTimeout() { return new Timeout(maxRetries, (int) (5000 * timeoutMultiplier)); } /** * This triggers the use of {@link CloudFoundryDeployerAutoConfiguration}. * * @author Eric Bottard */ @Configuration @EnableAutoConfiguration @EnableConfigurationProperties public static class Config { } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/test/java/org/springframework/cloud/deployer/spi/cloudfoundry/CloudFoundryTaskLauncherTests.java ================================================ /* * Copyright 2016-2023 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.cloud.deployer.spi.cloudfoundry; import java.io.IOException; import java.time.Duration; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v2.Metadata; import org.cloudfoundry.client.v2.applications.ApplicationsV2; import org.cloudfoundry.client.v2.applications.SummaryApplicationResponse; import org.cloudfoundry.client.v2.organizations.ListOrganizationsResponse; import org.cloudfoundry.client.v2.organizations.OrganizationResource; import org.cloudfoundry.client.v2.organizations.Organizations; import org.cloudfoundry.client.v2.spaces.ListSpacesResponse; import org.cloudfoundry.client.v2.spaces.SpaceResource; import org.cloudfoundry.client.v2.spaces.Spaces; import org.cloudfoundry.client.v3.Pagination; import org.cloudfoundry.client.v3.Relationship; import org.cloudfoundry.client.v3.ToOneRelationship; import org.cloudfoundry.client.v3.tasks.CancelTaskRequest; import org.cloudfoundry.client.v3.tasks.CancelTaskResponse; import org.cloudfoundry.client.v3.tasks.CreateTaskRequest; import org.cloudfoundry.client.v3.tasks.CreateTaskResponse; import org.cloudfoundry.client.v3.tasks.GetTaskRequest; import org.cloudfoundry.client.v3.tasks.GetTaskResponse; import org.cloudfoundry.client.v3.tasks.ListTasksResponse; import org.cloudfoundry.client.v3.tasks.TaskRelationships; import org.cloudfoundry.client.v3.tasks.TaskResource; import org.cloudfoundry.client.v3.tasks.TaskState; import org.cloudfoundry.client.v3.tasks.Tasks; import org.cloudfoundry.doppler.Envelope; import org.cloudfoundry.doppler.EventType; import org.cloudfoundry.doppler.LogMessage; import org.cloudfoundry.doppler.MessageType; import org.cloudfoundry.logcache.v1.EnvelopeBatch; import org.cloudfoundry.logcache.v1.LogCacheClient; import org.cloudfoundry.logcache.v1.ReadResponse; import org.cloudfoundry.operations.CloudFoundryOperations; import org.cloudfoundry.operations.applications.ApplicationDetail; import org.cloudfoundry.operations.applications.ApplicationHealthCheck; import org.cloudfoundry.operations.applications.ApplicationManifest; import org.cloudfoundry.operations.applications.ApplicationSummary; import org.cloudfoundry.operations.applications.Applications; import org.cloudfoundry.operations.applications.DeleteApplicationRequest; import org.cloudfoundry.operations.applications.GetApplicationRequest; import org.cloudfoundry.operations.applications.PushApplicationManifestRequest; import org.cloudfoundry.operations.applications.StopApplicationRequest; import org.cloudfoundry.operations.services.Services; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Answers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; import org.springframework.cloud.deployer.spi.task.LaunchState; import org.springframework.cloud.deployer.spi.task.TaskStatus; import org.springframework.cloud.deployer.spi.util.ByteSizeUtils; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; /** * @author Michael Minella * @author Ben Hale * @author Glenn Renfro * @author David Turanski * @author David Bernard */ public class CloudFoundryTaskLauncherTests { private final static int TASK_EXECUTION_COUNT = 10; private final static String LOG_RESPONSE = "Test Log Response"; private final CloudFoundryDeploymentProperties deploymentProperties = new CloudFoundryDeploymentProperties(); @Mock(answer = Answers.RETURNS_SMART_NULLS) private Applications applications; @Mock(answer = Answers.RETURNS_SMART_NULLS) private ApplicationsV2 applicationsV2; @Mock(answer = Answers.RETURNS_SMART_NULLS) private CloudFoundryClient client; private CloudFoundryTaskLauncher launcher; @Mock(answer = Answers.RETURNS_SMART_NULLS) private CloudFoundryOperations operations; @Mock(answer = Answers.RETURNS_SMART_NULLS) private Services services; @Mock(answer = Answers.RETURNS_SMART_NULLS) private Spaces spaces; @Mock(answer = Answers.RETURNS_SMART_NULLS) private Organizations organizations; @Mock(answer = Answers.RETURNS_SMART_NULLS) private Tasks tasks; private Resource resource = new FileSystemResource("src/test/resources/demo-0.0.1-SNAPSHOT.jar"); @BeforeEach void setUp() { MockitoAnnotations.initMocks(this); given(this.tasks.list(any())).willReturn(this.runningTasksResponse()); given(this.client.applicationsV2()).willReturn(this.applicationsV2); given(this.client.tasks()).willReturn(this.tasks); given(this.operations.applications()).willReturn(this.applications); given(this.operations.services()).willReturn(this.services); given(this.client.spaces()).willReturn(this.spaces); given(this.client.organizations()).willReturn(this.organizations); Mono getTaskResponse = getDefaultGetTaskResponse(); given(this.tasks.get(any())).willReturn(getTaskResponse); RuntimeEnvironmentInfo runtimeEnvironmentInfo = mock(RuntimeEnvironmentInfo.class); Map orgAndSpace = new HashMap<>(); orgAndSpace.put(CloudFoundryPlatformSpecificInfo.ORG, "this-org"); orgAndSpace.put(CloudFoundryPlatformSpecificInfo.SPACE, "this-space"); given(runtimeEnvironmentInfo.getPlatformSpecificInfo()).willReturn(orgAndSpace); given(this.organizations.list(any())).willReturn(listOrganizationsResponse()); given(this.spaces.list(any())).willReturn(listSpacesResponse()); ApplicationLogAccessor applicationLogAccessor = mock(ApplicationLogAccessor.class); given(applicationLogAccessor.getLog(any(), any())).willReturn(LOG_RESPONSE); this.launcher = getCloudFoundryTaskLauncher(applicationLogAccessor); } private CloudFoundryTaskLauncher getCloudFoundryTaskLauncher(ApplicationLogAccessor applicationLogAccessor) { RuntimeEnvironmentInfo runtimeEnvironmentInfo = mock(RuntimeEnvironmentInfo.class); Map orgAndSpace = new HashMap<>(); orgAndSpace.put(CloudFoundryPlatformSpecificInfo.ORG, "this-org"); orgAndSpace.put(CloudFoundryPlatformSpecificInfo.SPACE, "this-space"); given(runtimeEnvironmentInfo.getPlatformSpecificInfo()).willReturn(orgAndSpace); given(this.organizations.list(any())).willReturn(listOrganizationsResponse()); given(this.spaces.list(any())).willReturn(listSpacesResponse()); this.deploymentProperties.setApiTimeout(1); this.deploymentProperties.setStatusTimeout(1_250L); return new CloudFoundryTaskLauncher(this.client, this.deploymentProperties, this.operations, runtimeEnvironmentInfo, applicationLogAccessor); } @Test void cancel() { givenRequestCancelTask("test-task-id", Mono.just(CancelTaskResponse.builder() .id("test-task-id") .memoryInMb(1024) .diskInMb(1024) .dropletId("1") .createdAt(new Date().toString()) .updatedAt(new Date().toString()) .sequenceId(1) .name("test-task-id") .state(TaskState.CANCELING) .build())); this.launcher.cancel("test-task-id"); } @Test void currentExecutionCount() { assertThat(this.launcher.getRunningTaskExecutionCount()).isEqualTo(this.TASK_EXECUTION_COUNT); } @Test void launchTaskApplicationExists() throws IOException{ setupExistingAppSuccessful(); givenRequestPushApplication( PushApplicationManifestRequest.builder() .manifest(ApplicationManifest.builder() .path(resource.getFile().toPath()) .buildpack(deploymentProperties.getBuildpack()) .command("echo '*** First run of container to allow droplet creation.***' && sleep 300") .disk((int) ByteSizeUtils.parseToMebibytes(this.deploymentProperties.getDisk())) .environmentVariable("SPRING_APPLICATION_JSON", "{}") .healthCheckType(ApplicationHealthCheck.NONE) .memory((int) ByteSizeUtils.parseToMebibytes(this.deploymentProperties.getMemory())) .name("test-application") .noRoute(true) .services(Collections.emptySet()) .build()) .stagingTimeout(this.deploymentProperties.getStagingTimeout()) .startupTimeout(this.deploymentProperties.getStartupTimeout()) .build(), Mono.empty()); String taskId = this.launcher.launch(defaultRequest()); assertThat(taskId).isEqualTo("test-task-id"); } @Test void stageTaskApplicationExists() throws IOException{ setupExistingAppSuccessful(); givenRequestPushApplication( PushApplicationManifestRequest.builder() .manifest(ApplicationManifest.builder() .path(resource.getFile().toPath()) .buildpack(deploymentProperties.getBuildpack()) .command("echo '*** First run of container to allow droplet creation.***' && sleep 300") .disk((int) ByteSizeUtils.parseToMebibytes(this.deploymentProperties.getDisk())) .environmentVariable("SPRING_APPLICATION_JSON", "{}") .healthCheckType(ApplicationHealthCheck.NONE) .memory((int) ByteSizeUtils.parseToMebibytes(this.deploymentProperties.getMemory())) .name("test-application") .noRoute(true) .services(Collections.emptySet()) .build()) .stagingTimeout(this.deploymentProperties.getStagingTimeout()) .startupTimeout(this.deploymentProperties.getStartupTimeout()) .build(), Mono.empty()); SummaryApplicationResponse response = this.launcher.stage(defaultRequest()); assertThat(response.getId()).isEqualTo("test-application-id"); assertThat(response.getDetectedStartCommand()).isEqualTo("test-command"); } @Test void launchTaskWithNonExistentApplication() throws IOException { setupTaskWithNonExistentApplication(this.resource); String taskId = this.launcher.launch(defaultRequest()); assertThat(taskId).isEqualTo("test-task-id"); } @Test void launchExistingTaskApplicationWithPushDisabled() { setupExistingAppSuccessful(); deploymentProperties.setPushTaskAppsEnabled(false); String taskId = this.launcher.launch(defaultRequest()); assertThat(taskId).isEqualTo("test-task-id"); } @Test void launchNonExistingTaskApplicationWithPushDisabled() throws IOException { setupTaskWithNonExistentApplication(this.resource); deploymentProperties.setPushTaskAppsEnabled(false); assertThatThrownBy(() -> { this.launcher.launch(defaultRequest()); }).isInstanceOf(IllegalStateException.class); } @Test void stageTaskWithNonExistentApplication() throws IOException { setupTaskWithNonExistentApplication(this.resource); SummaryApplicationResponse response = this.launcher.stage(defaultRequest()); assertThat(response.getId()).isEqualTo("test-application-id"); assertThat(response.getDetectedStartCommand()).isEqualTo("test-command"); } @Test void automaticallyConfigureForCfEnv() throws JsonProcessingException { Resource resource = new FileSystemResource("src/test/resources/log-sink-rabbit-3.0.0.BUILD-SNAPSHOT.jar"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(new AppDefinition("test-application", Collections.emptyMap()), resource, Collections.emptyMap()); Map env = launcher.mergeEnvironmentVariables("test-application-id", appDeploymentRequest); // MatcherAssert.assertThat(env, hasEntry(CfEnvConfigurer.JBP_CONFIG_SPRING_AUTO_RECONFIGURATION, CfEnvConfigurer.ENABLED_FALSE)); // MatcherAssert.assertThat(env, hasKey(CoreMatchers.equalTo("SPRING_APPLICATION_JSON"))); // ObjectMapper objectMapper = new ObjectMapper(); // Map saj = objectMapper.readValue(env.get("SPRING_APPLICATION_JSON"),HashMap.class); // MatcherAssert.assertThat(saj, hasEntry(CfEnvConfigurer.SPRING_PROFILES_ACTIVE_FQN, CfEnvConfigurer.CLOUD_PROFILE_NAME)); assertThat(env).containsEntry(CfEnvConfigurer.JBP_CONFIG_SPRING_AUTO_RECONFIGURATION, CfEnvConfigurer.ENABLED_FALSE); assertThat(env).containsKeys("SPRING_APPLICATION_JSON"); ObjectMapper objectMapper = new ObjectMapper(); Map saj = objectMapper.readValue(env.get("SPRING_APPLICATION_JSON"), HashMap.class); assertThat(saj).containsEntry(CfEnvConfigurer.SPRING_PROFILES_ACTIVE_FQN, CfEnvConfigurer.CLOUD_PROFILE_NAME); } @Test void launchTaskWithNonExistentApplicationAndApplicationListingFails() { givenRequestListApplications(Flux.error(new UnsupportedOperationException())); given(this.operations.applications().pushManifest(any())).willThrow(new UnsupportedOperationException()); assertThatThrownBy(() -> { this.launcher.launch(defaultRequest()); }).isInstanceOf(UnsupportedOperationException.class); } @Test void stageTaskWithNonExistentApplicationAndApplicationListingFails() { givenRequestListApplications(Flux.error(new UnsupportedOperationException())); given(this.operations.applications().pushManifest(any())).willThrow(new UnsupportedOperationException()); assertThatThrownBy(() -> { this.launcher.stage(defaultRequest()); }).isInstanceOf(UnsupportedOperationException.class); } @Test void launchTaskWithNonExistentApplicationAndPushFails() throws IOException { setupFailedPush(this.resource); assertThatThrownBy(() -> { this.launcher.launch(defaultRequest()); }).isInstanceOf(UnsupportedOperationException.class); } @Test void stageTaskWithNonExistentApplicationAndPushFails() throws IOException { setupFailedPush(this.resource); assertThatThrownBy(() -> { this.launcher.stage(defaultRequest()); }).isInstanceOf(UnsupportedOperationException.class); } @Test void launchTaskWithNonExistentApplicationAndRetrievingApplicationSummaryFails() throws IOException { setupTaskWithNonExistentApplicationAndRetrievingApplicationSummaryFails(this.resource); assertThatThrownBy(() -> { this.launcher.launch(defaultRequest()); }).isInstanceOf(UnsupportedOperationException.class); } @Test void stageTaskWithNonExistentApplicationAndRetrievingApplicationSummaryFails() throws IOException { setupTaskWithNonExistentApplicationAndRetrievingApplicationSummaryFails(this.resource); assertThatThrownBy(() -> { this.launcher.stage(defaultRequest()); }).isInstanceOf(UnsupportedOperationException.class); } @Test void launchTaskWithNonExistentApplicationAndStoppingApplicationFails() throws IOException { setupTaskWithNonExistentApplicationAndStoppingApplicationFails(this.resource); assertThatThrownBy(() -> { this.launcher.launch(defaultRequest()); }).isInstanceOf(UnsupportedOperationException.class); } @Test void stageTaskWithNonExistentApplicationAndStoppingApplicationFails() throws IOException { setupTaskWithNonExistentApplicationAndStoppingApplicationFails(this.resource); assertThatThrownBy(() -> { this.launcher.stage(defaultRequest()); }).isInstanceOf(UnsupportedOperationException.class); } @Test void launchTaskWithNonExistentApplicationAndTaskCreationFails() throws IOException { givenRequestListApplications(Flux.empty()); givenRequestPushApplication(PushApplicationManifestRequest.builder() .manifest(ApplicationManifest.builder() .path(this.resource.getFile().toPath()) .buildpack(deploymentProperties.getBuildpack()) .command("echo '*** First run of container to allow droplet creation.***' && sleep 300") .disk((int) ByteSizeUtils.parseToMebibytes(this.deploymentProperties.getDisk())) .environmentVariable("SPRING_APPLICATION_JSON", "{}") .healthCheckType(ApplicationHealthCheck.NONE) .memory((int) ByteSizeUtils.parseToMebibytes(this.deploymentProperties.getMemory())) .name("test-application") .noRoute(true) .services(Collections.emptySet()) .build()) .stagingTimeout(this.deploymentProperties.getStagingTimeout()) .startupTimeout(this.deploymentProperties.getStartupTimeout()) .build(), Mono.empty()); givenRequestGetApplication("test-application", Mono.just(ApplicationDetail.builder() .diskQuota(0) .id("test-application-id") .instances(1) .memoryLimit(0) .name("test-application") .requestedState("RUNNING") .runningInstances(1) .stack("test-stack") .build())); givenRequestStopApplication("test-application", Mono.empty()); givenRequestGetApplicationSummary("test-application-id", Mono.just(SummaryApplicationResponse.builder() .id("test-application-id") .detectedStartCommand("test-command") .build())); givenRequestCreateTask("test-application-id", "test-command", this.deploymentProperties.getMemory(), this.deploymentProperties.getDisk(), "test-application", Mono.error(new UnsupportedOperationException())); assertThatThrownBy(() -> { this.launcher.launch(defaultRequest()); }).isInstanceOf(UnsupportedOperationException.class); } @Test void launchTaskWithNonExistentApplicationBindingOneService() throws IOException { setupTaskWithNonExistentApplicationBindingOneService(this.resource); AppDeploymentRequest request = deploymentRequest(this.resource, Collections.singletonMap(CloudFoundryDeploymentProperties.SERVICES_PROPERTY_KEY, "test-service-instance-2")); String taskId = this.launcher.launch(request); assertThat(taskId).isEqualTo("test-task-id"); } @Test void stageTaskWithNonExistentApplicationBindingOneService() throws IOException { setupTaskWithNonExistentApplicationBindingOneService(this.resource); AppDeploymentRequest request = deploymentRequest(this.resource, Collections.singletonMap(CloudFoundryDeploymentProperties.SERVICES_PROPERTY_KEY, "test-service-instance-2")); SummaryApplicationResponse response = this.launcher.stage(request); assertThat(response.getId()).isEqualTo("test-application-id"); assertThat(response.getDetectedStartCommand()).isEqualTo("test-command"); } @Test void launchTaskWithNonExistentApplicationBindingThreeServices() throws IOException { setupTaskWithNonExistentApplicationBindingThreeServices(this.resource); AppDeploymentRequest request = deploymentRequest(this.resource, Collections.singletonMap(CloudFoundryDeploymentProperties.SERVICES_PROPERTY_KEY, "test-service-instance-1,test-service-instance-2,test-service-instance-3")); String taskId = this.launcher.launch(request); assertThat(taskId).isEqualTo("test-task-id"); } @Test void stageTaskWithNonExistentApplicationBindingThreeServices() throws IOException { setupTaskWithNonExistentApplicationBindingThreeServices(this.resource); AppDeploymentRequest request = deploymentRequest(this.resource, Collections.singletonMap(CloudFoundryDeploymentProperties.SERVICES_PROPERTY_KEY, "test-service-instance-1,test-service-instance-2,test-service-instance-3")); SummaryApplicationResponse response = this.launcher.stage(request); assertThat(response.getId()).isEqualTo("test-application-id"); assertThat(response.getDetectedStartCommand()).isEqualTo("test-command"); } @Test void launchTaskWithNonExistentApplicationRetrievalFails() throws IOException { setupTaskWithNonExistentApplicationRetrievalFails(this.resource); assertThatThrownBy(() -> { this.launcher.launch(defaultRequest()); }).isInstanceOf(UnsupportedOperationException.class); } @Test void stageTaskWithNonExistentApplicationRetrievalFails() throws IOException { setupTaskWithNonExistentApplicationRetrievalFails(this.resource); assertThatThrownBy(() -> { this.launcher.stage(defaultRequest()); }).isInstanceOf(UnsupportedOperationException.class); } @Test void status() { ToOneRelationship toOneRelationship = ToOneRelationship.builder() .data(Relationship.builder().id("task-app-guid").build()).build(); TaskRelationships taskRelationships = TaskRelationships.builder().app(toOneRelationship).build(); givenRequestGetTask("test-task-id", getDefaultGetTaskResponse()); TaskStatus status = this.launcher.status("test-task-id"); assertThat(status.getState()).isEqualTo(LaunchState.complete); } private Mono getDefaultGetTaskResponse() { ToOneRelationship toOneRelationship = ToOneRelationship.builder() .data(Relationship.builder().id("task-app-guid").build()).build(); TaskRelationships taskRelationships = TaskRelationships.builder().app(toOneRelationship).build(); return Mono.just(GetTaskResponse.builder() .id("test-task-id") .memoryInMb(1024) .diskInMb(1024) .dropletId("1") .createdAt(new Date().toString()) .updatedAt(new Date().toString()) .sequenceId(1) .taskRelationships(taskRelationships) .name("test-task-id") .state(TaskState.SUCCEEDED) .build()); } @Test void statusTimeout() { // Delay twice as much as 40% of statusTimeout, which is what the deployer uses long delay = (long) (this.deploymentProperties.getStatusTimeout() * .4f * 2); givenRequestGetTask("test-task-id", Mono .delay(Duration.ofMillis(delay)) .then(Mono.just(GetTaskResponse.builder() .id("test-task-id") .memoryInMb(1024) .diskInMb(1024) .dropletId("1") .createdAt(new Date().toString()) .updatedAt(new Date().toString()) .sequenceId(1) .name("test-task-id") .state(TaskState.SUCCEEDED) .build()))); assertThat(this.launcher.status("test-task-id").getState()).isEqualTo(LaunchState.error); } @Test void destroyApp() { givenRequestDeleteApplication("test-application"); this.launcher.destroy("test-application"); } @Test void commandProperlyConfigured() { AppDeploymentRequest request = new AppDeploymentRequest(new AppDefinition( "test-app-1", null), this.resource, Collections.singletonMap("test-key-1", "test-val-1"), Collections.singletonList("test-command-arg-1")); String command = this.launcher.getCommand(SummaryApplicationResponse .builder() .detectedStartCommand("command-val") .build(), request); assertThat(command).isEqualTo("command-val test-command-arg-1"); List args = new ArrayList<>(); args.add("test-command-arg-1"); args.add("a=b"); args.add("run.id=1"); args.add("run.id(long)=1"); args.add("run.id(long=1"); args.add("run.id)=1"); request = new AppDeploymentRequest(new AppDefinition( "test-app-1", null), this.resource, Collections.singletonMap("test-key-1", "test-val-1"), args); command = this.launcher.getCommand(SummaryApplicationResponse .builder() .detectedStartCommand("command-val") .build(), request); assertThat(command).isEqualTo("command-val test-command-arg-1 a=b run.id=1 run.id\\\\\\(long\\\\\\)=1 run.id\\\\\\(long=1 run.id\\\\\\)=1"); } @Test void getLog() { assertThat(this.launcher.getLog("agcd")).isEqualTo(LOG_RESPONSE); } @Test void getLogForUnknownId() { LogCacheClient logCacheClient = mock(LogCacheClient.class); ApplicationLogAccessor applicationLogAccessor = new ApplicationLogAccessor(logCacheClient); Envelope.builder().logMessage(LogMessage.builder().message("").messageType(MessageType.OUT).timestamp(0l) .build()).eventType(EventType.LOG_MESSAGE).origin("foo").build(); EnvelopeBatch envelopeBatch = EnvelopeBatch.builder().batch().build(); ReadResponse response = ReadResponse.builder().envelopes(envelopeBatch).build(); given(logCacheClient.read(any())).willReturn(Mono.just(response)); this.launcher = getCloudFoundryTaskLauncher(applicationLogAccessor); assertThat(this.launcher.getLog("agcd")).isEmpty(); } @Test void getLogWithUnknownTaskAppGuid() { ToOneRelationship toOneRelationship = ToOneRelationship.builder() .data(Relationship.builder().id("task-app-guid").build()).build(); TaskRelationships taskRelationships = TaskRelationships.builder().app(toOneRelationship).build(); Mono getTaskResponse = Mono.just(GetTaskResponse.builder() .id("test-task-id") .memoryInMb(1024) .diskInMb(1024) .dropletId("1") .createdAt(new Date().toString()) .updatedAt(new Date().toString()) .sequenceId(1) .state(TaskState.FAILED) .name("TaskAppNotPresent") .build()); given(this.tasks.get(any())).willReturn(getTaskResponse); assertThatThrownBy(() -> { assertThat(this.launcher.getLog("agcd")).isEqualTo(LOG_RESPONSE); }).isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("could not find a GUID app id for the task guid id agcd"); } private void givenRequestCancelTask(String taskId, Mono response) { given(this.client.tasks() .cancel(CancelTaskRequest.builder() .taskId(taskId) .build())) .willReturn(response); } private void givenRequestCreateTask(String applicationId, String command, String memory, String disk, String name, Mono response) { given(this.client.tasks() .create(CreateTaskRequest.builder() .applicationId(applicationId) .command(command) .memoryInMb((int) ByteSizeUtils.parseToMebibytes(memory)) .diskInMb((int) ByteSizeUtils.parseToMebibytes(disk)) .name(name) .build())) .willReturn(response); } private void givenRequestDeleteApplication(String appName) { given(this.operations.applications() .delete(DeleteApplicationRequest.builder() .name(appName) .deleteRoutes(true) .build())) .willReturn(Mono.empty()); } private void givenRequestGetApplication(String name, Mono response) { given(this.operations.applications() .get(GetApplicationRequest.builder() .name(name) .build())) .willReturn(response); } private void givenRequestGetApplicationSummary(String applicationId, Mono response) { given(this.client.applicationsV2() .summary(org.cloudfoundry.client.v2.applications.SummaryApplicationRequest.builder() .applicationId(applicationId) .build())) .willReturn(response); } private void givenRequestGetTask(String taskId, Mono response) { given(this.client.tasks() .get(GetTaskRequest.builder() .taskId(taskId) .build())) .willReturn(response); } private void givenRequestListApplications(Flux response) { given(this.operations.applications() .list()) .willReturn(response); } private void givenRequestPushApplication(PushApplicationManifestRequest request, Mono response) { given(this.operations.applications() .pushManifest(any(PushApplicationManifestRequest.class))) .willReturn(response); } private void givenRequestStopApplication(String name, Mono response) { given(this.operations.applications() .stop(StopApplicationRequest.builder() .name(name) .build())) .willReturn(response); } private void setupExistingAppSuccessful() { givenRequestListApplications(Flux.just(ApplicationSummary.builder() .diskQuota(0) .id("test-application-id") .instances(1) .memoryLimit(0) .name("test-application") .requestedState("RUNNING") .runningInstances(1) .build())); givenRequestGetApplicationSummary("test-application-id", Mono.just(SummaryApplicationResponse.builder() .id("test-application-id") .detectedStartCommand("test-command") .build())); givenRequestCreateTask("test-application-id", "test-command", this.deploymentProperties.getMemory(), this.deploymentProperties.getDisk(), "test-application", Mono.just(CreateTaskResponse.builder() .id("test-task-id") .memoryInMb(1024) .diskInMb(1024) .dropletId("1") .createdAt(new Date().toString()) .updatedAt(new Date().toString()) .sequenceId(1) .name("test-task-id") .state(TaskState.FAILED) .build())); } private void setupTaskWithNonExistentApplication(Resource resource) throws IOException{ givenRequestListApplications(Flux.empty()); givenRequestPushApplication( PushApplicationManifestRequest.builder() .manifest(ApplicationManifest.builder() .path(resource.getFile().toPath()) .buildpack(deploymentProperties.getBuildpack()) .command("echo '*** First run of container to allow droplet creation.***' && sleep 300") .disk((int) ByteSizeUtils.parseToMebibytes(this.deploymentProperties.getDisk())) .environmentVariable("SPRING_APPLICATION_JSON", "{}") .healthCheckType(ApplicationHealthCheck.NONE) .memory((int) ByteSizeUtils.parseToMebibytes(this.deploymentProperties.getMemory())) .name("test-application") .noRoute(true) .services(Collections.emptySet()) .build()) .stagingTimeout(this.deploymentProperties.getStagingTimeout()) .startupTimeout(this.deploymentProperties.getStartupTimeout()) .build(), Mono.empty()); givenRequestGetApplication("test-application", Mono.just(ApplicationDetail.builder() .diskQuota(0) .id("test-application-id") .instances(1) .memoryLimit(0) .name("test-application") .requestedState("RUNNING") .runningInstances(1) .stack("test-stack") .build())); givenRequestStopApplication("test-application", Mono.empty()); givenRequestGetApplicationSummary("test-application-id", Mono.just(SummaryApplicationResponse.builder() .id("test-application-id") .detectedStartCommand("test-command") .build())); givenRequestCreateTask("test-application-id", "test-command", this.deploymentProperties.getMemory(), this.deploymentProperties.getDisk(), "test-application", Mono.just(CreateTaskResponse.builder() .id("test-task-id") .memoryInMb(1024) .diskInMb(1024) .dropletId("1") .createdAt(new Date().toString()) .updatedAt(new Date().toString()) .sequenceId(1) .name("test-task-id") .state(TaskState.SUCCEEDED) .build())); } private void setupFailedPush(Resource resource) throws IOException{ givenRequestListApplications(Flux.empty()); givenRequestPushApplication( PushApplicationManifestRequest.builder() .manifest(ApplicationManifest.builder() .path(resource.getFile().toPath()) .buildpack(deploymentProperties.getBuildpack()) .command("echo '*** First run of container to allow droplet creation.***' && sleep 300") .disk((int) ByteSizeUtils.parseToMebibytes(this.deploymentProperties.getDisk())) .environmentVariable("SPRING_APPLICATION_JSON", "{}") .healthCheckType(ApplicationHealthCheck.NONE) .memory((int) ByteSizeUtils.parseToMebibytes(this.deploymentProperties.getMemory())) .name("test-application") .noRoute(true) .services(Collections.emptySet()) .build()) .stagingTimeout(this.deploymentProperties.getStagingTimeout()) .startupTimeout(this.deploymentProperties.getStartupTimeout()) .build(), Mono.error(new UnsupportedOperationException())); } private void setupTaskWithNonExistentApplicationAndRetrievingApplicationSummaryFails(Resource resource) throws IOException { givenRequestListApplications(Flux.empty()); givenRequestPushApplication( PushApplicationManifestRequest.builder() .manifest(ApplicationManifest.builder() .path(resource.getFile().toPath()) .buildpack(deploymentProperties.getBuildpack()) .command("echo '*** First run of container to allow droplet creation.***' && sleep 300") .disk((int) ByteSizeUtils.parseToMebibytes(this.deploymentProperties.getDisk())) .environmentVariable("SPRING_APPLICATION_JSON", "{}") .healthCheckType(ApplicationHealthCheck.NONE) .memory((int) ByteSizeUtils.parseToMebibytes(this.deploymentProperties.getMemory())) .name("test-application") .noRoute(true) .services(Collections.emptySet()) .build()) .stagingTimeout(this.deploymentProperties.getStagingTimeout()) .startupTimeout(this.deploymentProperties.getStartupTimeout()) .build(), Mono.empty()); givenRequestGetApplication("test-application", Mono.just(ApplicationDetail.builder() .diskQuota(0) .id("test-application-id") .instances(1) .memoryLimit(0) .name("test-application") .requestedState("RUNNING") .runningInstances(1) .stack("test-stack") .build())); givenRequestStopApplication("test-application", Mono.empty()); givenRequestGetApplicationSummary("test-application-id", Mono.error(new UnsupportedOperationException())); } private void setupTaskWithNonExistentApplicationAndStoppingApplicationFails(Resource resource) throws IOException { givenRequestListApplications(Flux.empty()); givenRequestPushApplication( PushApplicationManifestRequest.builder() .manifest(ApplicationManifest.builder() .path(resource.getFile().toPath()) .buildpack(deploymentProperties.getBuildpack()) .command("echo '*** First run of container to allow droplet creation.***' && sleep 300") .disk((int) ByteSizeUtils.parseToMebibytes(this.deploymentProperties.getDisk())) .environmentVariable("SPRING_APPLICATION_JSON", "{}") .healthCheckType(ApplicationHealthCheck.NONE) .memory((int) ByteSizeUtils.parseToMebibytes(this.deploymentProperties.getMemory())) .name("test-application") .noRoute(true) .services(Collections.emptySet()) .build()) .stagingTimeout(this.deploymentProperties.getStagingTimeout()) .startupTimeout(this.deploymentProperties.getStartupTimeout()) .build(), Mono.empty()); givenRequestGetApplication("test-application", Mono.just(ApplicationDetail.builder() .diskQuota(0) .id("test-application-id") .instances(1) .memoryLimit(0) .name("test-application") .requestedState("RUNNING") .runningInstances(1) .stack("test-stack") .build())); givenRequestStopApplication("test-application", Mono.error(new UnsupportedOperationException())); } private void setupTaskWithNonExistentApplicationBindingOneService(Resource resource) throws IOException { givenRequestListApplications(Flux.empty()); givenRequestPushApplication(PushApplicationManifestRequest.builder() .manifest(ApplicationManifest.builder() .path(resource.getFile().toPath()) .buildpack(deploymentProperties.getBuildpack()) .command("echo '*** First run of container to allow droplet creation.***' && sleep 300") .disk((int) ByteSizeUtils.parseToMebibytes(this.deploymentProperties.getDisk())) .environmentVariable("SPRING_APPLICATION_JSON", "{}") .healthCheckType(ApplicationHealthCheck.NONE) .memory((int) ByteSizeUtils.parseToMebibytes(this.deploymentProperties.getMemory())) .name("test-application") .noRoute(true) .service("test-service-instance-2") .build()) .stagingTimeout(this.deploymentProperties.getStagingTimeout()) .startupTimeout(this.deploymentProperties.getStartupTimeout()) .build(), Mono.empty()); givenRequestGetApplication("test-application", Mono.just(ApplicationDetail.builder() .diskQuota(0) .id("test-application-id") .instances(1) .memoryLimit(0) .name("test-application") .requestedState("RUNNING") .runningInstances(1) .stack("test-stack") .build())); givenRequestStopApplication("test-application", Mono.empty()); givenRequestGetApplicationSummary("test-application-id", Mono.just(SummaryApplicationResponse.builder() .id("test-application-id") .detectedStartCommand("test-command") .build())); givenRequestCreateTask("test-application-id", "test-command", this.deploymentProperties.getMemory(), this.deploymentProperties.getDisk(), "test-application", Mono.just(CreateTaskResponse.builder() .id("test-task-id") .memoryInMb(1024) .diskInMb(1024) .dropletId("1") .createdAt(new Date().toString()) .updatedAt(new Date().toString()) .sequenceId(1) .name("test-task-id") .state(TaskState.SUCCEEDED) .build())); } private void setupTaskWithNonExistentApplicationBindingThreeServices(Resource resource) throws IOException { givenRequestListApplications(Flux.empty()); givenRequestPushApplication(PushApplicationManifestRequest.builder() .manifest(ApplicationManifest.builder() .path(resource.getFile().toPath()) .buildpack(deploymentProperties.getBuildpack()) .command("echo '*** First run of container to allow droplet creation.***' && sleep 300") .disk((int) ByteSizeUtils.parseToMebibytes(this.deploymentProperties.getDisk())) .environmentVariable("SPRING_APPLICATION_JSON", "{}") .healthCheckType(ApplicationHealthCheck.NONE) .memory((int) ByteSizeUtils.parseToMebibytes(this.deploymentProperties.getMemory())) .name("test-application") .noRoute(true) .service("test-service-instance-1") .service("test-service-instance-2") .service("test-service-instance-3") .build()) .stagingTimeout(this.deploymentProperties.getStagingTimeout()) .startupTimeout(this.deploymentProperties.getStartupTimeout()) .build(), Mono.empty()); givenRequestGetApplication("test-application", Mono.just(ApplicationDetail.builder() .diskQuota(0) .id("test-application-id") .instances(1) .memoryLimit(0) .name("test-application") .requestedState("RUNNING") .runningInstances(1) .stack("test-stack") .build())); givenRequestStopApplication("test-application", Mono.empty()); givenRequestGetApplicationSummary("test-application-id", Mono.just(SummaryApplicationResponse.builder() .id("test-application-id") .detectedStartCommand("test-command") .build())); givenRequestCreateTask("test-application-id", "test-command", this.deploymentProperties.getMemory(), this.deploymentProperties.getDisk(), "test-application", Mono.just(CreateTaskResponse.builder() .id("test-task-id") .memoryInMb(1024) .diskInMb(1024) .dropletId("1") .createdAt(new Date().toString()) .updatedAt(new Date().toString()) .sequenceId(1) .name("test-task-id") .state(TaskState.SUCCEEDED) .build())); } public void setupTaskWithNonExistentApplicationRetrievalFails(Resource resource) throws IOException { givenRequestListApplications(Flux.empty()); givenRequestPushApplication(PushApplicationManifestRequest.builder() .manifest(ApplicationManifest.builder() .path(resource.getFile().toPath()) .buildpack(deploymentProperties.getBuildpack()) .command("echo '*** First run of container to allow droplet creation.***' && sleep 300") .disk((int) ByteSizeUtils.parseToMebibytes(this.deploymentProperties.getDisk())) .environmentVariable("SPRING_APPLICATION_JSON", "{}") .healthCheckType(ApplicationHealthCheck.NONE) .memory((int) ByteSizeUtils.parseToMebibytes(this.deploymentProperties.getMemory())) .name("test-application") .noRoute(true) .services(Collections.emptySet()) .build()) .stagingTimeout(this.deploymentProperties.getStagingTimeout()) .startupTimeout(this.deploymentProperties.getStartupTimeout()) .build(), Mono.empty()); givenRequestStopApplication("test-application", Mono.empty()); givenRequestGetApplication("test-application", Mono.error(new UnsupportedOperationException())); } private AppDeploymentRequest defaultRequest() { return deploymentRequest(this.resource, Collections.emptyMap()); } private AppDeploymentRequest deploymentRequest(Resource resource, Map deploymentProperties) { AppDefinition definition = new AppDefinition("test-application", null); return new AppDeploymentRequest(definition, resource, deploymentProperties); } private Mono runningTasksResponse() { List taskResources = new ArrayList<>(); for (int i=0; i< TASK_EXECUTION_COUNT; i++) { taskResources.add(TaskResource.builder() .name("task-" + i) .dropletId(UUID.randomUUID().toString()) .id(UUID.randomUUID().toString()) .diskInMb(2048) .sequenceId(i) .state(TaskState.RUNNING) .memoryInMb(2048) .createdAt(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))) .build()); } ListTasksResponse listTasksResponse = ListTasksResponse.builder().resources(taskResources) .pagination(Pagination.builder().totalResults(taskResources.size()).build()) .build(); return Mono.just(listTasksResponse); } private Mono listOrganizationsResponse() { ListOrganizationsResponse response = ListOrganizationsResponse.builder() .addAllResources(Collections.singletonList( OrganizationResource.builder() .metadata(Metadata.builder().id("123").build()).build()) ).build(); return Mono.just(response); } private Mono listSpacesResponse() { ListSpacesResponse response = ListSpacesResponse.builder() .addAllResources(Collections.singletonList( SpaceResource.builder() .metadata(Metadata.builder().id("123").build()).build()) ).build(); return Mono.just(response); } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/test/java/org/springframework/cloud/deployer/spi/cloudfoundry/ServiceParserTests.java ================================================ /* * Copyright 2019-2021 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.cloud.deployer.spi.cloudfoundry; import java.util.Collections; 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.assertThatThrownBy; import static org.assertj.core.api.Assertions.entry; /** @author David Turanski */ public class ServiceParserTests { @Test public void plainService() { assertThat(ServiceParser.getServiceParameters("test-service").isPresent()).isFalse(); assertThat(ServiceParser.getServiceInstanceName("test-service")).isEqualTo("test-service"); } @Test public void plainServiceWithSpecialCharacters() { assertThat(ServiceParser.getServiceParameters("test.service.$$").isPresent()).isFalse(); } @Test public void serviceWithParameters() { String serviceSpec = "test-service foo:bar"; assertThat(ServiceParser.getServiceParameters(serviceSpec).get()) .isEqualTo(Collections.singletonMap("foo", "bar")); } @Test public void getServiceInstanceName() { String serviceSpec = "test-service foo:bar"; assertThat(ServiceParser.getServiceInstanceName(serviceSpec)).isEqualTo("test-service"); } @Test public void serviceWithSpacesParameters() { assertThat(ServiceParser.getServiceParameters("test-service foo : bar").get()) .isEqualTo(Collections.singletonMap("foo", "bar")); } @Test public void serviceWithEqualsInParameters() { assertThat(ServiceParser.getServiceParameters("test-service foo=bar").get()) .isEqualTo(Collections.singletonMap("foo", "bar")); } @Test public void realWorldExample() { Map params = ServiceParser.getServiceParameters( "nfs share:1.2.3.4/export, uid:65534, gid:65534, mount:/var/scdf") .get(); assertThat(params).containsOnly( entry("share","1.2.3.4/export"), entry("uid","65534"), entry("gid","65534"), entry("mount","/var/scdf") ); } @Test public void anotherRealWorldExample() { Map params = ServiceParser.getServiceParameters( "nfs share=1.2.3.4/export, uid=65534, gid=65534, mount=/var/scdf") .get(); assertThat(params).containsOnly( entry("share","1.2.3.4/export"), entry("uid","65534"), entry("gid","65534"), entry("mount","/var/scdf") ); } @Test public void serviceWithInvalidParameters() { assertThatThrownBy(() -> { ServiceParser.getServiceParameters("test-service foo bar"); }).isInstanceOf(IllegalArgumentException.class).hasMessageContaining( "invalid service specification: test-service foo bar"); } @Test public void splitServiceProperties() { assertThat(ServiceParser.splitServiceProperties( "'nfs share:10.194.2.6/export,uid:65534,gid:65534,mount:/var/scdf',mysql,'foo bar:baz'")) .containsExactlyInAnyOrder( "nfs share:10.194.2.6/export,uid:65534,gid:65534,mount:/var/scdf", "mysql", "foo bar:baz"); assertThat(ServiceParser.splitServiceProperties("mysql,rabbit,redis")) .containsExactlyInAnyOrder( "mysql", "rabbit", "redis"); assertThat(ServiceParser.splitServiceProperties( "redis, 'nfs share:10.194.2.6/export,uid:65534,gid:65534,mount:/var/scdf', mysql , 'foo bar:baz'")) .containsExactlyInAnyOrder( "redis", "nfs share:10.194.2.6/export,uid:65534,gid:65534,mount:/var/scdf", "mysql", "foo bar:baz"); } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/test/java/org/springframework/cloud/deployer/spi/scheduler/cloudfoundry/CloudFoundryAppSchedulerTests.java ================================================ /* * Copyright 2018-2021 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.cloud.deployer.spi.scheduler.cloudfoundry; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import io.pivotal.scheduler.SchedulerClient; import io.pivotal.scheduler.v1.Pagination; import io.pivotal.scheduler.v1.calls.Calls; import io.pivotal.scheduler.v1.jobs.CreateJobRequest; import io.pivotal.scheduler.v1.jobs.CreateJobResponse; import io.pivotal.scheduler.v1.jobs.DeleteJobRequest; import io.pivotal.scheduler.v1.jobs.DeleteJobScheduleRequest; import io.pivotal.scheduler.v1.jobs.ExecuteJobRequest; import io.pivotal.scheduler.v1.jobs.ExecuteJobResponse; import io.pivotal.scheduler.v1.jobs.GetJobRequest; import io.pivotal.scheduler.v1.jobs.GetJobResponse; import io.pivotal.scheduler.v1.jobs.Job; import io.pivotal.scheduler.v1.jobs.JobSchedule; import io.pivotal.scheduler.v1.jobs.Jobs; import io.pivotal.scheduler.v1.jobs.ListJobHistoriesRequest; import io.pivotal.scheduler.v1.jobs.ListJobHistoriesResponse; import io.pivotal.scheduler.v1.jobs.ListJobScheduleHistoriesRequest; import io.pivotal.scheduler.v1.jobs.ListJobScheduleHistoriesResponse; import io.pivotal.scheduler.v1.jobs.ListJobSchedulesRequest; import io.pivotal.scheduler.v1.jobs.ListJobSchedulesResponse; import io.pivotal.scheduler.v1.jobs.ListJobsRequest; import io.pivotal.scheduler.v1.jobs.ListJobsResponse; import io.pivotal.scheduler.v1.jobs.ScheduleJobRequest; import io.pivotal.scheduler.v1.jobs.ScheduleJobResponse; import io.pivotal.scheduler.v1.schedules.ExpressionType; import org.cloudfoundry.client.CloudFoundryClient; import org.cloudfoundry.client.v3.applications.ApplicationsV3; import org.cloudfoundry.client.v3.tasks.Tasks; import org.cloudfoundry.operations.CloudFoundryOperations; import org.cloudfoundry.operations.applications.ApplicationSummary; import org.cloudfoundry.operations.applications.Applications; import org.cloudfoundry.operations.spaces.SpaceSummary; import org.cloudfoundry.operations.spaces.Spaces; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Answers; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.cloud.deployer.spi.cloudfoundry.CloudFoundryConnectionProperties; import org.springframework.cloud.deployer.spi.cloudfoundry.CloudFoundryDeploymentProperties; import org.springframework.cloud.deployer.spi.cloudfoundry.CloudFoundryTaskLauncher; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.scheduler.CreateScheduleException; import org.springframework.cloud.deployer.spi.scheduler.ScheduleInfo; import org.springframework.cloud.deployer.spi.scheduler.ScheduleRequest; import org.springframework.cloud.deployer.spi.scheduler.SchedulerException; import org.springframework.cloud.deployer.spi.scheduler.SchedulerPropertyKeys; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; /** * Test the core features of the Spring Cloud Scheduler implementation. * * @author Glenn Renfro * @author Ilayaperumal Gopinathan */ public class CloudFoundryAppSchedulerTests { public static final String DEFAULT_CRON_EXPRESSION = "0/5 * ? * *"; public static final String CRON_EXPRESSION_FOR_SIX_MIN = "0/6 * ? * *"; public static final String BAD_CRON_EXPRESSION = "FOOBAD"; @Mock(answer = Answers.RETURNS_SMART_NULLS) private Applications applications; @Mock(answer = Answers.RETURNS_SMART_NULLS) private CloudFoundryOperations operations; @Mock(answer = Answers.RETURNS_SMART_NULLS) private Spaces spaces; @Mock(answer = Answers.RETURNS_SMART_NULLS) private ApplicationsV3 applicationsV3; @Mock(answer = Answers.RETURNS_SMART_NULLS) private CloudFoundryClient cloudFoundryClient; @Mock(answer = Answers.RETURNS_SMART_NULLS) private Tasks tasks; @Mock(answer = Answers.RETURNS_SMART_NULLS) private CloudFoundryTaskLauncher taskLauncher; private CloudFoundryAppScheduler deprecatedCloudFoundryAppScheduler; private CloudFoundryAppScheduler deprecatedNoServiceCloudFoundryAppScheduler; private CloudFoundryAppScheduler cloudFoundryAppScheduler; private CloudFoundryAppScheduler noServiceCloudFoundryAppScheduler; private SchedulerClient client; private SchedulerClient noServiceClient; private CloudFoundryConnectionProperties properties = new CloudFoundryConnectionProperties(); private CloudFoundrySchedulerProperties schedulerProperties = new CloudFoundrySchedulerProperties(); private CloudFoundryDeploymentProperties deploymentProperties = new CloudFoundryDeploymentProperties(); @BeforeEach public void setUp() { MockitoAnnotations.initMocks(this); given(this.cloudFoundryClient.applicationsV3()).willReturn(this.applicationsV3); given(this.cloudFoundryClient.tasks()).willReturn(this.tasks); given(this.spaces.list()).willReturn(getTestSpaces()); this.properties.setSpace("test-space"); given(this.operations.applications()).willReturn(this.applications); given(this.operations.spaces()).willReturn(this.spaces); this.client = new TestSchedulerClient(); this.noServiceClient = new NoServiceTestSchedulerClient(); this.deprecatedCloudFoundryAppScheduler = new CloudFoundryAppScheduler(this.client, this.operations, this.properties, taskLauncher, schedulerProperties); this.deprecatedNoServiceCloudFoundryAppScheduler = new CloudFoundryAppScheduler(this.noServiceClient, this.operations, this.properties, taskLauncher, schedulerProperties); this.cloudFoundryAppScheduler = new CloudFoundryAppScheduler(this.client, this.operations, this.properties, taskLauncher, deploymentProperties); this.noServiceCloudFoundryAppScheduler = new CloudFoundryAppScheduler(this.noServiceClient, this.operations, this.properties, taskLauncher, deploymentProperties); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testEmptySchedulerProperties(boolean isDeprecated) { Resource resource = new FileSystemResource("src/test/resources/demo-0.0.1-SNAPSHOT.jar"); AppDefinition definition = new AppDefinition("bar", null); ScheduleRequest request = (isDeprecated) ? new ScheduleRequest(definition, null, null, null, "testschedule", resource) : new ScheduleRequest(definition, null, (List) null, "testschedule", resource); assertThatThrownBy(() -> { getCloudFoundryAppScheduler(isDeprecated).schedule(request); }).isInstanceOf(IllegalArgumentException.class); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testCreateNoCommandLineArgs(boolean isDeprecated) { Resource resource = new FileSystemResource("src/test/resources/demo-0.0.1-SNAPSHOT.jar"); mockAppResultsInAppList(); AppDefinition definition = new AppDefinition("test-application-1", null); ScheduleRequest request = (isDeprecated) ? new ScheduleRequest(definition, getDefaultScheduleProperties(),null, null, "test-schedule", resource) : new ScheduleRequest(definition, getDefaultDeploymentProperties(), (List) null, "test-schedule", resource); getCloudFoundryAppScheduler(isDeprecated).schedule(request); assertThat(((TestJobs) this.client.jobs()).getCreateJobResponse().getId()).isEqualTo("test-job-id-1"); assertThat(((TestJobs) this.client.jobs()).getCreateJobResponse().getApplicationId()).isEqualTo("test-application-id-1"); assertThat(((TestJobs) this.client.jobs()).getCreateJobResponse().getCommand()).isEmpty(); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testInvalidCron(boolean isDeprecated) { Resource resource = new FileSystemResource("src/test/resources/demo-0.0.1-SNAPSHOT.jar"); mockAppResultsInAppList(); AppDefinition definition = new AppDefinition("test-application-1", null); Map badCronMap = new HashMap<>(); badCronMap.put(SchedulerPropertyKeys.CRON_EXPRESSION, BAD_CRON_EXPRESSION); ScheduleRequest request = (isDeprecated) ? new ScheduleRequest(definition, Collections.singletonMap(SchedulerPropertyKeys.CRON_EXPRESSION, BAD_CRON_EXPRESSION), null, null, "test-schedule", resource) : new ScheduleRequest(definition, Collections.singletonMap(CloudFoundryAppScheduler.CRON_EXPRESSION_KEY, BAD_CRON_EXPRESSION), (List) null, "test-schedule", resource); assertThatThrownBy(() -> { getCloudFoundryAppScheduler(isDeprecated).schedule(request); }).isInstanceOf(CreateScheduleException.class).hasMessageContaining( "Illegal characters for this position: 'FOO'"); assertThat(((TestJobs) this.client.jobs()).getCreateJobResponse()).isNull(); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testNameTooLong(boolean isDeprecated) { Resource resource = new FileSystemResource("src/test/resources/demo-0.0.1-SNAPSHOT.jar"); mockAppResultsInAppList(); AppDefinition definition = new AppDefinition("test-application-1", null); Map cronMap = new HashMap<>(); cronMap.put(SchedulerPropertyKeys.CRON_EXPRESSION, DEFAULT_CRON_EXPRESSION); ScheduleRequest request = new ScheduleRequest(definition, cronMap, (List) null, "j1-scdf-itcouldbesaidthatthisislongtoowaytoo-oopsitcouldbesaidthatthisis" + "longtoowaytoo-oopsitcouldbesaidthatthisislongtoowaytoo-oopsitcouldbe" + "saidthatthisislongtoowaytoo-oopsitcouldbesaidthatthisislongtoowaytoo-" + "oopsitcouldbesaidthatthisislongtoowaytoo-oops12", resource); assertThatThrownBy(() -> { getCloudFoundryAppScheduler(isDeprecated).schedule(request); }).isInstanceOf(CreateScheduleException.class).hasMessageContaining( "Schedule can not be created because its name " + "'j1-scdf-itcouldbesaidthatthisislongtoowaytoo-oopsitcouldbesaidthatthisis" + "longtoowaytoo-oopsitcouldbesaidthatthisislongtoowaytoo-oopsitcouldbe" + "saidthatthisislongtoowaytoo-oopsitcouldbesaidthatthisislongtoowaytoo-" + "oopsitcouldbesaidthatthisislongtoowaytoo-oops12' has too many characters. " + "Schedule name length must be 255 characters or less"); assertThat(((TestJobs) this.client.jobs()).getCreateJobResponse()).isNull(); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testSuccessJobCreateFailedSchedule(boolean isDeprecated) { Resource resource = new FileSystemResource("src/test/resources/demo-0.0.1-SNAPSHOT.jar"); mockAppResultsInAppList(); AppDefinition definition = new AppDefinition("test-application-1", null); ScheduleRequest request = (isDeprecated) ? new ScheduleRequest(definition, Collections.singletonMap(SchedulerPropertyKeys.CRON_EXPRESSION, CRON_EXPRESSION_FOR_SIX_MIN), null, null, "test-schedule", resource) : new ScheduleRequest(definition, Collections.singletonMap(CloudFoundryAppScheduler.CRON_EXPRESSION_KEY, CRON_EXPRESSION_FOR_SIX_MIN), (List) null, "test-schedule", resource); assertThatThrownBy(() -> { getCloudFoundryAppScheduler(isDeprecated).schedule(request); }).isInstanceOf(CreateScheduleException.class); assertThat(((TestJobs) this.client.jobs()).getCreateJobResponse()).isNull(); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testCreateWithCommandLineArgs(boolean isDeprecated) { Resource resource = new FileSystemResource("src/test/resources/demo-0.0.1-SNAPSHOT.jar"); mockAppResultsInAppList(); AppDefinition definition = new AppDefinition("test-application-1", null); ScheduleRequest request = isDeprecated ? new ScheduleRequest(definition, getDefaultScheduleProperties(), null, Collections.singletonList("TestArg"), "test-schedule", resource) : new ScheduleRequest(definition, getDefaultDeploymentProperties(), Collections.singletonList("TestArg"), "test-schedule", resource); getCloudFoundryAppScheduler(isDeprecated).schedule(request); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(AppDeploymentRequest.class); verify(this.taskLauncher).stage(argumentCaptor.capture()); assertThat(argumentCaptor.getValue().getCommandlineArguments().get(0)).isEqualTo("TestArg"); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testList(boolean isDeprecated) { setupMockResults(); List result = getCloudFoundryAppScheduler(isDeprecated).list(); assertThat(result.size()).isEqualTo(2); verifyScheduleInfo(result.get(0), "test-application-1", "test-job-name-1", DEFAULT_CRON_EXPRESSION); verifyScheduleInfo(result.get(1), "test-application-2", "test-job-name-2", DEFAULT_CRON_EXPRESSION); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testListWithJobsNoAssociatedSchedule(boolean isDeprecated) { setupMockResultsNoScheduleForJobs(); List result = getCloudFoundryAppScheduler(isDeprecated).list(); assertThat(result.size()).isEqualTo(2); verifyScheduleInfo(result.get(0), "test-application-1", "test-job-name-1", null); verifyScheduleInfo(result.get(1), "test-application-2", "test-job-name-2", null); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testListWithNoSchedules(boolean isDeprecated) { given(this.operations.applications() .list()) .willReturn(Flux.empty()); List result = getCloudFoundryAppScheduler(isDeprecated).list(); assertThat(result.size()).isEqualTo(0); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testListSchedulesWithAppName(boolean isDeprecated) { setupMockResults(); List result = getCloudFoundryAppScheduler(isDeprecated).list("test-application-2"); assertThat(result.size()).isEqualTo(1); verifyScheduleInfo(result.get(0), "test-application-2", "test-job-name-2", DEFAULT_CRON_EXPRESSION); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testListSchedulesWithInvalidAppName(boolean isDeprecated) { setupMockResults(); List result = getCloudFoundryAppScheduler(isDeprecated).list("not-here"); assertThat(result.size()).isEqualTo(0); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testUnschedule(boolean isDeprecated) { setupMockResults(); List result = getCloudFoundryAppScheduler(isDeprecated).list(); assertThat(result.size()).isEqualTo(2); getCloudFoundryAppScheduler(isDeprecated).unschedule("test-job-name-1"); result = getCloudFoundryAppScheduler(isDeprecated).list(); assertThat(result.size()).isEqualTo(1); assertThat(result.get(0).getScheduleName()).isEqualTo("test-job-name-2"); assertThat(result.get(0).getTaskDefinitionName()).isEqualTo("test-application-2"); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testMissingScheduleDelete(boolean isDeprecated) { boolean exceptionFired = false; setupMockResults(); try { getCloudFoundryAppScheduler(isDeprecated).unschedule("test-job-name-3"); } catch (SchedulerException se) { assertThat(se.getMessage()).isEqualTo("Failed to unschedule schedule test-job-name-3 does not exist."); exceptionFired = true; } assertThat(exceptionFired).isTrue(); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testNoServiceList(boolean isDeprecated) { assertThatThrownBy(() -> { getNoServiceCloudFoundryAppScheduler(isDeprecated).list(); }).isInstanceOf(SchedulerException.class).hasMessageContaining( "Scheduler Service returned a null response."); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testNoServiceListSchedulesWithAppName(boolean isDeprecated) { assertThatThrownBy(() -> { getNoServiceCloudFoundryAppScheduler(isDeprecated).list("test-application-2"); }).isInstanceOf(SchedulerException.class).hasMessageContaining( "Scheduler Service returned a null response."); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testNoServiceCreate(boolean isDeprecated) { Resource resource = new FileSystemResource("src/test/resources/demo-0.0.1-SNAPSHOT.jar"); mockAppResultsInAppList(); AppDefinition definition = new AppDefinition("test-application-1", null); ScheduleRequest request = (isDeprecated) ? new ScheduleRequest(definition, getDefaultScheduleProperties(), null, null, "test-schedule", resource) : new ScheduleRequest(definition, getDefaultDeploymentProperties(), (List) null, "test-schedule", resource); assertThatThrownBy(() -> { getNoServiceCloudFoundryAppScheduler(isDeprecated).schedule(request); }).isInstanceOf(SchedulerException.class).hasMessageContaining( "Scheduler Service returned a null response."); } private void givenRequestListApplications(Flux response) { given(this.operations.applications() .list()) .willReturn(response); } private void verifyScheduleInfo(ScheduleInfo scheduleInfo, String taskDefinitionName, String scheduleName, String expression) { assertThat(scheduleInfo.getTaskDefinitionName()).isEqualTo(taskDefinitionName); assertThat(scheduleInfo.getScheduleName()).isEqualTo(scheduleName); if (expression != null) { assertThat(scheduleInfo.getScheduleProperties().size()).isEqualTo(1); assertThat(scheduleInfo.getScheduleProperties().get(SchedulerPropertyKeys.CRON_EXPRESSION)).isEqualTo(expression); } else { assertThat(scheduleInfo.getScheduleProperties().size()).isEqualTo(0); } } private static class TestSchedulerClient implements SchedulerClient { private Jobs jobs; public TestSchedulerClient() { jobs = new TestJobs(); } @Override public Calls calls() { return null; } @Override public Jobs jobs() { return jobs; } } private static class NoServiceTestSchedulerClient implements SchedulerClient { private Jobs jobs; public NoServiceTestSchedulerClient() { jobs = new NoServiceTestJobs(); } @Override public Calls calls() { return null; } @Override public Jobs jobs() { return jobs; } } private static class NoServiceTestJobs extends TestJobs { @Override public Mono list(ListJobsRequest request) { return Mono.justOrEmpty(null); } @Override public Mono create(CreateJobRequest request) { return Mono.justOrEmpty(null); } } private static class TestJobs implements Jobs { private CreateJobResponse createJobResponse; private List jobResources = new ArrayList<>(); private List jobScheduleResources = new ArrayList<>(); @Override public Mono create(CreateJobRequest request) { this.createJobResponse = CreateJobResponse.builder() .applicationId(request.getApplicationId()) .name(request.getName()) .id("test-job-id-1") .command(request.getCommand()) .build(); this.jobResources.add(Job.builder().applicationId(request.getApplicationId()) .command(request.getCommand()) .id("test-job-1") .name(request.getName()) .build()); return Mono.just(createJobResponse); } @Override public Mono delete(DeleteJobRequest request) { for (int i = 0; i < this.jobResources.size(); i++) { if (this.jobResources.get(i).getId().equals(request.getJobId())) { jobResources.remove(i); break; } } return Mono.justOrEmpty(null); } @Override public Mono deleteSchedule(DeleteJobScheduleRequest request) { return null; } @Override public Mono execute(ExecuteJobRequest request) { return null; } @Override public Mono get(GetJobRequest request) { return null; } @Override public Mono list(ListJobsRequest request) { ListJobsResponse response = ListJobsResponse.builder() .addAllResources(jobResources) .pagination(Pagination.builder().totalPages(1).build()) .build(); return Mono.just(response); } @Override public Mono listHistories(ListJobHistoriesRequest request) { return null; } @Override public Mono listScheduleHistories(ListJobScheduleHistoriesRequest request) { return null; } @Override public Mono listSchedules(ListJobSchedulesRequest request) { ListJobSchedulesResponse response = ListJobSchedulesResponse.builder() .addAllResources(jobScheduleResources.stream().filter(jobScheduleResource -> jobScheduleResource.getJobId().equals(request.getJobId())).collect(Collectors.toList())) .build(); return Mono.just(response); } @Override public Mono schedule(ScheduleJobRequest request) { if(request.getExpression().equals(CRON_EXPRESSION_FOR_SIX_MIN)) { throw new IllegalStateException(); } return Mono.just(ScheduleJobResponse.builder().expression(request.getExpression()) .expressionType(request.getExpressionType()) .enabled(true) .jobId(request.getJobId()) .id("schedule-1234") .build()); } public CreateJobResponse getCreateJobResponse() { if(this.jobResources.size() == 0) { this.createJobResponse = null; } return createJobResponse; } } private Flux getTestSpaces() { return Flux.just(SpaceSummary.builder().id("test-space-1") .name("test-space") .build()); } private void setupMockResults() { mockJobsInJobList(); mockAppResultsInAppList(); } private void setupMockResultsNoScheduleForJobs() { mockJobsInJobListNoSchedule(); mockAppResultsInAppList(); } private void mockAppResultsInAppList() { givenRequestListApplications(Flux.just(ApplicationSummary.builder() .diskQuota(0) .id("test-application-id-1") .instances(1) .memoryLimit(0) .name("test-application-1") .requestedState("RUNNING") .runningInstances(1) .build(), ApplicationSummary.builder() .diskQuota(0) .id("test-application-id-2") .instances(1) .memoryLimit(0) .name("test-application-2") .requestedState("RUNNING") .runningInstances(1) .build())); } private void mockJobsInJobListNoSchedule() { TestJobs localJobs = (TestJobs) client.jobs(); localJobs.jobResources.add(Job.builder().applicationId("test-application-id-1") .command("test-command") .id("test-job-1") .name("test-job-name-1") .build()); localJobs.jobResources.add(Job.builder().applicationId("test-application-id-2") .command("test-command") .id("test-job-2") .name("test-job-name-2") .build()); } private void mockJobsInJobList() { TestJobs localJobs = (TestJobs) client.jobs(); localJobs.jobResources.add(Job.builder().applicationId("test-application-id-1") .command("test-command") .id("test-job-1") .name("test-job-name-1") .jobSchedules(createJobScheduleList("test-job-1", "test-schedule-1")) .build()); localJobs.jobResources.add(Job.builder().applicationId("test-application-id-2") .command("test-command") .id("test-job-2") .name("test-job-name-2") .jobSchedules(createJobScheduleList("test-job-2", "test-schedule-2")) .build()); } private List createJobScheduleList(String jobId, String scheduleId) { List jobSchedules = new ArrayList<>(); jobSchedules.add(JobSchedule.builder() .enabled(true) .expression(DEFAULT_CRON_EXPRESSION) .expressionType(ExpressionType.CRON) .id(scheduleId) .jobId(jobId) .build()); return jobSchedules; } private Map getDefaultScheduleProperties() { Map result = new HashMap<>(); result.put(SchedulerPropertyKeys.CRON_EXPRESSION, DEFAULT_CRON_EXPRESSION); return result; } private Map getDefaultDeploymentProperties() { Map result = new HashMap<>(); result.put(CloudFoundryAppScheduler.CRON_EXPRESSION_KEY, DEFAULT_CRON_EXPRESSION); return result; } private CloudFoundryAppScheduler getCloudFoundryAppScheduler(boolean isDeprecated) { return isDeprecated ? this.deprecatedCloudFoundryAppScheduler : this.cloudFoundryAppScheduler; } private CloudFoundryAppScheduler getNoServiceCloudFoundryAppScheduler(boolean isDeprecated) { return isDeprecated ? this.deprecatedNoServiceCloudFoundryAppScheduler : this.noServiceCloudFoundryAppScheduler; } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/test/java/org/springframework/cloud/deployer/spi/scheduler/cloudfoundry/CloudFoundryScheduleSSLExceptionTests.java ================================================ /* * Copyright 2018-2021 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.cloud.deployer.spi.scheduler.cloudfoundry; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** * Verify that {@linkCloudFoundryScheduleSSLException} has the expected behavior. * * @author Glenn Renfro */ public class CloudFoundryScheduleSSLExceptionTests { @Test public void testExceptionMessageOnly() { try { throw new CloudFoundryScheduleSSLException("oops"); } catch (CloudFoundryScheduleSSLException cfe) { assertThat(cfe.getMessage()).isEqualTo("oops"); } } @Test public void testExceptionMessageWithException() { RuntimeException rte = new RuntimeException("RTE"); try { throw new CloudFoundryScheduleSSLException("oops", rte); } catch (CloudFoundryScheduleSSLException cfe) { assertThat(cfe.getMessage()).isEqualTo("oops"); assertThat(cfe.getCause()).isEqualTo(rte); } } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/test/java/org/springframework/cloud/deployer/spi/scheduler/cloudfoundry/CloudFoundrySchedulerPropertiesTest.java ================================================ /* * Copyright 2018-2021 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.cloud.deployer.spi.scheduler.cloudfoundry; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** * Validate the basic behavior of the {@link CloudFoundrySchedulerProperties}. * * @author Glenn Renfro */ public class CloudFoundrySchedulerPropertiesTest { @Test public void testProperties() { CloudFoundrySchedulerProperties props = new CloudFoundrySchedulerProperties(); props.setSchedulerUrl("testProperty"); props.setScheduleSSLRetryCount(10); props.setListTimeoutInSeconds(5); props.setUnScheduleTimeoutInSeconds(10); props.setScheduleTimeoutInSeconds(15); assertThat(props.getSchedulerUrl()).isEqualTo("testProperty"); assertThat(props.getScheduleSSLRetryCount()).isEqualTo(10); assertThat(props.getScheduleTimeoutInSeconds()).isEqualTo(15); assertThat(props.getUnScheduleTimeoutInSeconds()).isEqualTo(10); assertThat(props.getListTimeoutInSeconds()).isEqualTo(5); } @Test public void testEmptyProperties() { CloudFoundrySchedulerProperties props = new CloudFoundrySchedulerProperties(); assertThat(props.getSchedulerUrl()).isNull(); assertThat(props.getScheduleSSLRetryCount()).isEqualTo(5); assertThat(props.getListTimeoutInSeconds()).isEqualTo(60); assertThat(props.getScheduleTimeoutInSeconds()).isEqualTo(30); assertThat(props.getUnScheduleTimeoutInSeconds()).isEqualTo(30); } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/test/java/org/springframework/cloud/deployer/spi/scheduler/cloudfoundry/SpringCloudSchedulerIntegrationIT.java ================================================ /* * Copyright 2018-2021 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.cloud.deployer.spi.scheduler.cloudfoundry; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import io.pivotal.reactor.scheduler.ReactorSchedulerClient; import org.cloudfoundry.operations.CloudFoundryOperations; import org.cloudfoundry.operations.applications.DeleteApplicationRequest; import org.cloudfoundry.reactor.ConnectionContext; import org.cloudfoundry.reactor.TokenProvider; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.extension.ExtendWith; import reactor.core.publisher.Mono; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.cloud.deployer.resource.maven.MavenProperties; import org.springframework.cloud.deployer.spi.cloudfoundry.CloudFoundryConnectionProperties; import org.springframework.cloud.deployer.spi.cloudfoundry.CloudFoundryDeploymentProperties; import org.springframework.cloud.deployer.spi.cloudfoundry.CloudFoundryTaskLauncher; import org.springframework.cloud.deployer.spi.scheduler.Scheduler; import org.springframework.cloud.deployer.spi.scheduler.SchedulerPropertyKeys; import org.springframework.cloud.deployer.spi.scheduler.test.AbstractSchedulerIntegrationJUnit5Tests; import org.springframework.cloud.deployer.spi.task.TaskLauncher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; /** * Integration tests for CloudFoundryAppScheduler. * * @author Glenn Renfro */ @ExtendWith(SpringExtension.class) @SpringBootTest(webEnvironment = WebEnvironment.NONE) @ContextConfiguration(classes = {SpringCloudSchedulerIntegrationIT.Config.class}) public class SpringCloudSchedulerIntegrationIT extends AbstractSchedulerIntegrationJUnit5Tests { @Autowired protected MavenProperties mavenProperties; @Autowired private Scheduler scheduler; @Value("${spring.cloud.deployer.cloudfoundry.services}") private String deployerProps; @Override protected Scheduler provideScheduler() { return this.scheduler; } @Autowired private CloudFoundryOperations operations; @Override protected List getCommandLineArgs() { return null; } @Override protected Map getSchedulerProperties() { return Collections.singletonMap(SchedulerPropertyKeys.CRON_EXPRESSION,"41 17 ? * *"); } @Override protected Map getDeploymentProperties() { Map deploymentProperties = new HashMap<>(); deploymentProperties.put(CloudFoundryDeploymentProperties.SERVICES_PROPERTY_KEY, deployerProps); deploymentProperties.put(CloudFoundryAppScheduler.CRON_EXPRESSION_KEY, "57 13 ? * *"); return deploymentProperties; } @Override protected Map getAppProperties() { return null; } /** * Remove all pushed apps. This in turn removes the associated schedules. */ @AfterEach public void tearDown() { try { operations.applications().list().flatMap(applicationSummary -> { if (applicationSummary.getName().startsWith("testList") || applicationSummary.getName().startsWith("testDuplicateSchedule") || applicationSummary.getName().startsWith("testUnschedule") || applicationSummary.getName().startsWith("testMultiple") || applicationSummary.getName().startsWith("testSimpleSchedule")) { return operations.applications().delete(DeleteApplicationRequest .builder() .name(applicationSummary.getName()) .build()); } return Mono.justOrEmpty(applicationSummary); }).blockLast(); } catch (Exception ex) { log.warn("Attempted cleanup and exception occured: " + ex.getMessage()); } } @Configuration @EnableAutoConfiguration @EnableConfigurationProperties public static class Config { @Bean @ConditionalOnMissingBean public ReactorSchedulerClient reactorSchedulerClient(ConnectionContext context, TokenProvider passwordGrantTokenProvider, CloudFoundryDeploymentProperties taskDeploymentProperties) { return ReactorSchedulerClient.builder() .connectionContext(context) .tokenProvider(passwordGrantTokenProvider) .root(Mono.just(taskDeploymentProperties.getSchedulerUrl())) .build(); } @Bean @ConditionalOnMissingBean public Scheduler scheduler(ReactorSchedulerClient client, CloudFoundryOperations operations, CloudFoundryConnectionProperties properties, TaskLauncher taskLauncher, CloudFoundryDeploymentProperties taskDeploymentProperties) { return new CloudFoundryAppScheduler(client, operations, properties, (CloudFoundryTaskLauncher) taskLauncher, taskDeploymentProperties); } } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/test/java/org/springframework/cloud/deployer/spi/scheduler/cloudfoundry/expression/QuartzCronExpressionTests.java ================================================ /* * Copyright 2018-2021 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.cloud.deployer.spi.scheduler.cloudfoundry.expression; import java.text.ParseException; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; public class QuartzCronExpressionTests { /* * Verifies that storeExpressionVals correctly calculates the month number */ @Test public void testStoreExpressionVal() { assertExpression("* * * * Foo ? ", "Invalid Month value:", "Expected ParseException did not fire for non-existent month"); assertExpression("* * * * Jan-Foo ? ", "Invalid Month value:", "Expected ParseException did not fire for non-existent month"); } @Test public void testWildCard() { assertExpression("0 0 * * * *", "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented.", "Expected ParseException did not fire for wildcard day-of-month and day-of-week"); assertExpression("0 0 * 4 * *", "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented.", "Expected ParseException did not fire for specified day-of-month and wildcard day-of-week"); assertExpression("0 0 * * * 4", "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented.", "Expected ParseException did not fire for wildcard day-of-month and specified day-of-week"); } @Test public void testForInvalidLInCronExpression() { assertExpression("0 43 9 1,5,29,L * ?", "Support for specifying 'L' and 'LW' with other days of the month is not implemented", "Expected ParseException did not fire for L combined with other days of the month"); assertExpression("0 43 9 ? * SAT,SUN,L", "Support for specifying 'L' with other days of the week is not implemented", "Expected ParseException did not fire for L combined with other days of the week"); assertExpression("0 43 9 ? * 6,7,L", "Support for specifying 'L' with other days of the week is not implemented", "Expected ParseException did not fire for L combined with other days of the week"); assertThatCode(() -> { new QuartzCronExpression("0 43 9 ? * 5L"); }).as("Unexpected ParseException thrown for supported '5L' expression.").doesNotThrowAnyException(); } @Test public void testForLargeWVal() { assertExpression("0/5 * * 32W 1 ?", "The 'W' option does not make sense with values larger than", "Expected ParseException did not fire for W with value larger than 31"); } @Test public void testSecRangeIntervalAfterSlash() { // Test case 1 assertExpression("/120 0 8-18 ? * 2-6", "Increment > 60 : 120", "Cron did not validate bad range interval in '_blank/xxx' form"); // Test case 2 assertExpression("0/120 0 8-18 ? * 2-6", "Increment > 60 : 120", "Cron did not validate bad range interval in in '0/xxx' form"); // Test case 3 assertExpression("/ 0 8-18 ? * 2-6", "'/' must be followed by an integer.", "Cron did not validate bad range interval in '_blank/_blank'"); // Test case 4 assertExpression("0/ 0 8-18 ? * 2-6", "'/' must be followed by an integer.", "Cron did not validate bad range interval in '0/_blank'"); } @Test public void testMinRangeIntervalAfterSlash() { // Test case 1 assertExpression("0 /120 8-18 ? * 2-6", "Increment > 60 : 120", "Cron did not validate bad range interval in '_blank/xxx' form"); // Test case 2 assertExpression("0 0/120 8-18 ? * 2-6", "Increment > 60 : 120", "Cron did not validate bad range interval in in '0/xxx' form"); // Test case 3 assertExpression("0 / 8-18 ? * 2-6", "'/' must be followed by an integer.", "Cron did not validate bad range interval in '_blank/_blank'"); // Test case 4 assertExpression("0 0/ 8-18 ? * 2-6", "'/' must be followed by an integer.", "Cron did not validate bad range interval in '0/_blank'"); } @Test public void testHourRangeIntervalAfterSlash() { // Test case 1 assertExpression("0 0 /120 ? * 2-6", "Increment > 24 : 120", "Cron did not validate bad range interval in '_blank/xxx' form"); // Test case 2 assertExpression("0 0 0/120 ? * 2-6", "Increment > 24 : 120", "Cron did not validate bad range interval in in '0/xxx' form"); // Test case 3 assertExpression("0 0 / ? * 2-6", "'/' must be followed by an integer.", "Cron did not validate bad range interval in '_blank/_blank'"); // Test case 4 assertExpression("0 0 0/ ? * 2-6", "'/' must be followed by an integer.", "Cron did not validate bad range interval in '0/_blank'"); } @Test public void testDayOfMonthRangeIntervalAfterSlash() { // Test case 1 assertExpression("0 0 0 /120 * 2-6", "Increment > 31 : 120", "Cron did not validate bad range interval in '_blank/xxx' form"); // Test case 2 assertExpression("0 0 0 0/120 * 2-6", "Increment > 31 : 120", "Cron did not validate bad range interval in in '0/xxx' form"); // Test case 3 assertExpression("0 0 0 / * 2-6", "'/' must be followed by an integer.", "Cron did not validate bad range interval in '_blank/_blank'"); // Test case 4 assertExpression("0 0 0 0/ * 2-6", "'/' must be followed by an integer.", "Cron did not validate bad range interval in '0/_blank'"); } @Test public void testMonthRangeIntervalAfterSlash() { // Test case 1 assertExpression("0 0 0 ? /120 2-6", "Increment > 12 : 120", "Cron did not validate bad range interval in '_blank/xxx' form"); // Test case 2 assertExpression("0 0 0 ? 0/120 2-6", "Increment > 12 : 120", "Cron did not validate bad range interval in in '0/xxx' form"); // Test case 3 assertExpression("0 0 0 ? / 2-6", "'/' must be followed by an integer.", "Cron did not validate bad range interval in '_blank/_blank'"); // Test case 4 assertExpression("0 0 0 ? 0/ 2-6", "'/' must be followed by an integer.", "Cron did not validate bad range interval in '0/_blank'"); } @Test public void testDayOfWeekRangeIntervalAfterSlash() { // Test case 1 assertExpression("0 0 0 ? * /120", "Increment > 7 : 120", "Cron did not validate bad range interval in '_blank/xxx' form"); // Test case 2 assertExpression("0 0 0 ? * 0/120", "Increment > 7 : 120", "Cron did not validate bad range interval in in '0/xxx' form"); // Test case 3 assertExpression("0 0 0 ? * /", "'/' must be followed by an integer.", "Cron did not validate bad range interval in '_blank/_blank'"); // Test case 4 assertExpression("0 0 0 ? * 0/", "'/' must be followed by an integer.", "Cron did not validate bad range interval in '0/_blank'"); } private static void assertExpression(String expression, String messageContains, String as) { assertThatThrownBy(() -> { new QuartzCronExpression(expression); }).isInstanceOf(ParseException.class).hasMessageContaining(messageContains).as(as); } } ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/test/resources/batch-job-1.0.0.BUILD-SNAPSHOT.jar ================================================ [File too large to display: 11.8 MB] ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/test/resources/http-source-rabbit-2.1.5.RELEASE.jar ================================================ [File too large to display: 44.7 MB] ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/test/resources/log-sink-rabbit-3.0.0.BUILD-SNAPSHOT.jar ================================================ [File too large to display: 37.2 MB] ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/test/resources/logback-test.xml ================================================ %date{HH:mm:ss.SSS} %-25thread %-37logger %msg%n ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/test/resources/long-running-task-1.0.0.BUILD-SNAPSHOT.jar ================================================ [File too large to display: 11.4 MB] ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/test/resources/timestamp-task-1.0.0.BUILD-SNAPSHOT-exec.jar ================================================ [File too large to display: 10.9 MB] ================================================ FILE: spring-cloud-deployer-cloudfoundry/src/test/resources/timestamp-task-3.1.2-SNAPSHOT.jar ================================================ [File too large to display: 18.6 MB] ================================================ FILE: spring-cloud-deployer-dependencies/pom.xml ================================================ 4.0.0 org.springframework.cloud spring-cloud-deployer-dependencies 3.0.0-SNAPSHOT pom Spring Cloud Deployer Dependencies Spring Cloud Deployer Dependencies Pivotal Software, Inc. https://www.spring.io https://github.com/spring-cloud/spring-cloud-deployer Apache License, Version 2.0 https://www.apache.org/licenses/LICENSE-2.0 Copyright 2014-2021 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. https://github.com/spring-cloud/spring-cloud-deployer scm:git:git://github.com/spring-cloud/spring-cloud-deployer.git scm:git:ssh://git@github.com/spring-cloud/spring-cloud-deployer.git HEAD scdf-team Data Flow Team https://github.com/spring-cloud/spring-cloud-deployer/graphs/contributors org.springframework.cloud spring-cloud-deployer-resource-docker 3.0.0-SNAPSHOT org.springframework.cloud spring-cloud-deployer-resource-maven 3.0.0-SNAPSHOT org.springframework.cloud spring-cloud-deployer-resource-support 3.0.0-SNAPSHOT org.springframework.cloud spring-cloud-deployer-spi 3.0.0-SNAPSHOT org.springframework.cloud spring-cloud-deployer-autoconfigure 3.0.0-SNAPSHOT spring true maven-central Maven Central https://repo.maven.apache.org/maven2 false spring-snapshots Spring Snapshots https://repo.spring.io/libs-snapshot true spring-milestones Spring Milestones https://repo.spring.io/libs-milestone false maven-central Maven Central https://repo.maven.apache.org/maven2 false spring-snapshots Spring Snapshots https://repo.spring.io/libs-snapshot true spring-milestones Spring Milestones https://repo.spring.io/libs-milestone false ================================================ FILE: spring-cloud-deployer-kubernetes/README.md ================================================ # Spring Cloud Deployer Kubernetes A [Spring Cloud Deployer](https://github.com/spring-cloud/spring-cloud-deployer) implementation for deploying long-lived streaming applications and short-lived tasks to Kubernetes. ## Kubernetes Compatibility | Deployer \ Kubernetes | 1.20 | 1.21 | 1.22 | 1.23 | 1.24 | 1.25 | 1.26 | 1.27 | 1.28 | 1.29 | 1.30 | |-----------------------|------|------|------|------|-------|-------|------|------|------|------|------| | **2.6.x** | `✕` | `✕` | `✕` | `✕` | `✕` | `✕` |`✕` | `✕` | `✕` | `✕` | `✕` | | **2.7.x** | `✓` | `✓` | `?` | `?` | `?` | `✕` |`✕` | `✕` | `✕` | `✕` | `✕` | | **2.8.x** | `✓` | `✓` | `✓` | `✓` | `✓` | `✕` |`✕` | `✕` | `✕` | `✕` | `✕` | | **2.9.x** | `✓` | `✓` | `✓` | `✓` | `✓` | `✓` |`✓` | `✓` | `✓` | `✓` | `✓` | | **3.0.x** | `✓` | `✓` | `✓` | `✓` | `✓` | `✓` |`✓` | `✓` | `✓` | `✓` | `✓` | - `✓` Fully supported version - `?` Due to breaking changes might not work _(e.g., ABAC vs RBAC)_. Also, we haven't thoroughly tested against this version. - `✕` Unsupported version. ## Building Build the project without running tests using: ``` ./mvnw clean install -DskipTests ``` ## Integration tests The integration tests require a running Kubernetes cluster. A couple of options are listed below. ### Minkube [Minikube](https://github.com/kubernetes/minikube) is a tool that makes it easy to run Kubernetes locally. It runs a single-node Kubernetes cluster inside a VM on your laptop for users looking to try out Kubernetes or develop with it day-to-day. Follow the [getting started](https://minikube.sigs.k8s.io/docs/start/) guide to install Minikube. 1. Start Minikube ```shell minkube start ``` 2. Run the tests ```shell ./mvnw clean test ``` 3. Stop Minikube ```shell minkube stop ``` ### Google Container Engine While Minikube is very easy to run and test against, it is preferred to test against a GKE cluster. Minikube is not as useful since we test some parts of the external IP features that a LoadBalancer service provides. Create a test cluster and target it using something like (use your own project name, substitute --zone if needed): ``` gcloud container --project {your-project-name} clusters create "spring-test" --zone "us-central1-b" --machine-type "n1-highcpu-2" --scopes "https://www.googleapis.com/auth/compute","https://www.googleapis.com/auth/devstorage.read_only","https://www.googleapis.com/auth/logging.write" --network "default" --enable-cloud-logging --enable-cloud-monitoring gcloud config set container/cluster spring-test gcloud config set compute/zone us-central1-b gcloud container clusters get-credentials spring-test kubectl version ``` > :information_source: the last command causes the access token to be generated and saved to the kubeconfig file - it can be any valid kubectl command #### Running the tests Once the test cluster has been created, you can run all integration tests. As long as your `kubectl` config files are set to point to your cluster, you should be able to just run the tests. Verify your config using `kubectl config get-contexts` and check that your test cluster is the current context. Now run the tests: ``` $ ./mvnw test ``` NOTE: if you get authentication errors, try setting basic auth credentials: Navigate to your project and cluster on https://console.cloud.google.com/ and click on `show credentials` ```bash $export KUBERNETES_AUTH_BASIC_PASSWORD= $export KUBERNETES_AUTH_BASIC_USERNAME= ``` ================================================ FILE: spring-cloud-deployer-kubernetes/pom.xml ================================================ 4.0.0 spring-cloud-deployer-kubernetes 3.0.0-SNAPSHOT jar Spring Cloud Deployer Kubernetes org.springframework.cloud spring-cloud-deployer-parent 3.0.0-SNAPSHOT .. 2.0.2 org.springframework.cloud spring-cloud-deployer-spi org.springframework.cloud spring-cloud-deployer-resource-docker org.springframework.boot spring-boot-autoconfigure org.springframework spring-web io.fabric8 kubernetes-client org.hashids hashids ${hashids.version} org.springframework.boot spring-boot-configuration-processor true org.springframework.cloud spring-cloud-deployer-spi-test ${project.version} test org.powermock powermock-module-junit4 ${powermock.version} test junit junit org.mockito mockito-core test spring spring-snapshots Spring Snapshots https://repo.spring.io/snapshot true spring-milestones Spring Milestones https://repo.spring.io/milestone false maven-central Maven Central https://repo.maven.apache.org/maven2 false spring-snapshots Spring Snapshots https://repo.spring.io/snapshot true spring-milestones Spring Milestones https://repo.spring.io/milestone false maven-central Maven Central https://repo.maven.apache.org/maven2 false failsafe org.apache.maven.plugins maven-failsafe-plugin 3.5.0 **/*IT.* verify integration-test org.apache.maven.plugins maven-checkstyle-plugin ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/AbstractKubernetesDeployer.java ================================================ /* * Copyright 2015-2023 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.cloud.deployer.spi.kubernetes; import java.util.Collection; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; import io.fabric8.kubernetes.api.model.Affinity; import io.fabric8.kubernetes.api.model.Container; import io.fabric8.kubernetes.api.model.ContainerStatus; import io.fabric8.kubernetes.api.model.Lifecycle; import io.fabric8.kubernetes.api.model.LifecycleHandlerBuilder; import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.api.model.PodList; import io.fabric8.kubernetes.api.model.PodSecurityContext; import io.fabric8.kubernetes.api.model.PodSpec; import io.fabric8.kubernetes.api.model.PodSpecBuilder; import io.fabric8.kubernetes.api.model.ResourceRequirements; import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.api.model.SecurityContext; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.ServiceList; import io.fabric8.kubernetes.client.KubernetesClient; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.cloud.deployer.spi.app.AppDeployer; import org.springframework.cloud.deployer.spi.app.AppStatus; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; import org.springframework.cloud.deployer.spi.kubernetes.support.PropertyParserUtils; import org.springframework.cloud.deployer.spi.util.RuntimeVersionUtils; import org.springframework.core.io.Resource; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; /** * Abstract base class for a deployer that targets Kubernetes. * * @author Florian Rosenberg * @author Thomas Risberg * @author Mark Fisher * @author Donovan Muller * @author David Turanski * @author Chris Schaefer * @author Enrique Medina Montenegro * @author Ilayaperumal Gopinathan * @author Chris Bono * @author Corneil du Plessis */ public class AbstractKubernetesDeployer { protected static final String SPRING_DEPLOYMENT_KEY = "spring-deployment-id"; protected static final String SPRING_GROUP_KEY = "spring-group-id"; protected static final String SPRING_APP_KEY = "spring-app-id"; protected static final String SPRING_MARKER_KEY = "role"; protected static final String SPRING_MARKER_VALUE = "spring-app"; protected static final String APP_NAME_PROPERTY_KEY = AppDeployer.PREFIX + "appName"; protected static final String APP_NAME_KEY = "spring-application-name"; private static final String SERVER_PORT_KEY = "server.port"; protected final Log logger = LogFactory.getLog(getClass().getName()); protected ContainerFactory containerFactory; protected KubernetesClient client; protected KubernetesDeployerProperties properties; protected DeploymentPropertiesResolver deploymentPropertiesResolver; /** * Create the RuntimeEnvironmentInfo. * * @param spiClass the SPI interface class * @param implementationClass the SPI implementation class * @return the Kubernetes runtime environment info */ protected RuntimeEnvironmentInfo createRuntimeEnvironmentInfo(Class spiClass, Class implementationClass) { return new RuntimeEnvironmentInfo.Builder() .spiClass(spiClass) .implementationName(implementationClass.getSimpleName()) .implementationVersion(RuntimeVersionUtils.getVersion(implementationClass)) .platformType("Kubernetes") .platformApiVersion(client.getApiVersion()) .platformClientVersion(RuntimeVersionUtils.getVersion(client.getClass())) .platformHostVersion("unknown") .addPlatformSpecificInfo("master-url", String.valueOf(client.getMasterUrl())) .addPlatformSpecificInfo("namespace", client.getNamespace()) .build(); } /** * Creates a map of labels for a given application ID. * * @param appId the application id * @param request The {@link AppDeploymentRequest} * @return the built id map of labels */ Map createIdMap(String appId, AppDeploymentRequest request) { Map map = new HashMap<>(); map.put(SPRING_APP_KEY, appId); String groupId = request.getDeploymentProperties().get(AppDeployer.GROUP_PROPERTY_KEY); if (groupId != null) { map.put(SPRING_GROUP_KEY, groupId); } map.put(SPRING_DEPLOYMENT_KEY, appId); // un-versioned app name provided by skipper String appName = request.getDeploymentProperties().get(APP_NAME_PROPERTY_KEY); if (StringUtils.hasText(appName)) { map.put(APP_NAME_KEY, appName); } return map; } protected AppStatus buildAppStatus(String id, PodList podList, ServiceList services) { AppStatus.Builder statusBuilder = AppStatus.of(id); Service service = null; if (podList != null && podList.getItems() != null) { for (Pod pod : podList.getItems()) { String deploymentKey = pod.getMetadata().getLabels().get(SPRING_DEPLOYMENT_KEY); for (Service svc : services.getItems()) { // handle case of when the version provided by skipper has been removed if(deploymentKey.startsWith(svc.getMetadata().getName())) { service = svc; break; } } //find the container with the correct env var for(Container container : pod.getSpec().getContainers()) { if(container.getEnv().stream().anyMatch(envVar -> "SPRING_CLOUD_APPLICATION_GUID".equals(envVar.getName()))) { //find container status for this container Optional containerStatusOptional = pod.getStatus().getContainerStatuses() .stream().filter(containerStatus -> container.getName().equals(containerStatus.getName())) .findFirst(); statusBuilder.with(new KubernetesAppInstanceStatus(pod, service, properties, containerStatusOptional.orElse(null))); break; } } } } return statusBuilder.build(); } protected void logPossibleDownloadResourceMessage(Resource resource) { if (logger.isInfoEnabled()) { logger.info("Preparing to run a container from " + resource + ". This may take some time if the image must be downloaded from a remote container registry."); } } /** * Create PodSpec for the given {@link AppDeploymentRequest} * @param appDeploymentRequest the app deployment request to use to create the PodSpec * @return the PodSpec */ PodSpec createPodSpec(AppDeploymentRequest appDeploymentRequest) { String appId = createDeploymentId(appDeploymentRequest); Map deploymentProperties = appDeploymentRequest.getDeploymentProperties(); PodSpecBuilder podSpec = new PodSpecBuilder(); String imagePullSecret = this.deploymentPropertiesResolver.getImagePullSecret(deploymentProperties); if (imagePullSecret != null) { podSpec.addNewImagePullSecret(imagePullSecret); } List imagePullSecrets = this.deploymentPropertiesResolver.getImagePullSecrets(deploymentProperties); if (imagePullSecrets != null) { imagePullSecrets.forEach(imgPullsecret -> podSpec.addNewImagePullSecret(imgPullsecret)); } boolean hostNetwork = this.deploymentPropertiesResolver.getHostNetwork(deploymentProperties); ContainerConfiguration containerConfiguration = new ContainerConfiguration(appId, appDeploymentRequest) .withProbeCredentialsSecret(getProbeCredentialsSecret(deploymentProperties)) .withHostNetwork(hostNetwork); if (KubernetesAppDeployer.class.isAssignableFrom(this.getClass())) { containerConfiguration.withExternalPort(getExternalPort(appDeploymentRequest)); } Container container = containerFactory.create(containerConfiguration); // add memory and cpu resource limits ResourceRequirements req = new ResourceRequirements(); req.setLimits(this.deploymentPropertiesResolver.deduceResourceLimits(deploymentProperties)); req.setRequests(this.deploymentPropertiesResolver.deduceResourceRequests(deploymentProperties)); container.setResources(req); ImagePullPolicy pullPolicy = this.deploymentPropertiesResolver.deduceImagePullPolicy(deploymentProperties); container.setImagePullPolicy(pullPolicy.name()); KubernetesDeployerProperties.Lifecycle lifecycle = this.deploymentPropertiesResolver.getLifeCycle(deploymentProperties); Lifecycle f8Lifecycle = new Lifecycle(); if (lifecycle.getPostStart() != null) { f8Lifecycle.setPostStart(new LifecycleHandlerBuilder() .withNewExec() .addAllToCommand(lifecycle.getPostStart().getExec().getCommand()).and().build()); } if (lifecycle.getPreStop() != null) { f8Lifecycle.setPreStop(new LifecycleHandlerBuilder() .withNewExec() .addAllToCommand(lifecycle.getPreStop().getExec().getCommand()).and().build()); } if (f8Lifecycle.getPostStart() != null || f8Lifecycle.getPreStop() != null) { container.setLifecycle(f8Lifecycle); } Long termGracePeriod = this.deploymentPropertiesResolver.determineTerminationGracePeriodSeconds(deploymentProperties); if (termGracePeriod != null) { podSpec.withTerminationGracePeriodSeconds(termGracePeriod); } Map nodeSelectors = this.deploymentPropertiesResolver.getNodeSelectors(deploymentProperties); if (!nodeSelectors.isEmpty()) { podSpec.withNodeSelector(nodeSelectors); } podSpec.withTolerations(this.deploymentPropertiesResolver.getTolerations(deploymentProperties)); if (hostNetwork) { podSpec.withHostNetwork(true); } SecurityContext containerSecurityContext = this.deploymentPropertiesResolver.getContainerSecurityContext(deploymentProperties); if (containerSecurityContext != null) { container.setSecurityContext(containerSecurityContext); } podSpec.addToContainers(container); podSpec.withRestartPolicy(this.deploymentPropertiesResolver.getRestartPolicy(deploymentProperties).name()); String deploymentServiceAccountName = this.deploymentPropertiesResolver.getDeploymentServiceAccountName(deploymentProperties); if (deploymentServiceAccountName != null) { podSpec.withServiceAccountName(deploymentServiceAccountName); } PodSecurityContext podSecurityContext = this.deploymentPropertiesResolver.getPodSecurityContext(deploymentProperties); if (podSecurityContext != null) { podSpec.withSecurityContext(podSecurityContext); } Affinity affinity = this.deploymentPropertiesResolver.getAffinityRules(deploymentProperties); // Make sure there is at least some rule. if (affinity.getNodeAffinity() != null || affinity.getPodAffinity() != null || affinity.getPodAntiAffinity() != null) { podSpec.withAffinity(affinity); } Collection initContainers = this.deploymentPropertiesResolver.getInitContainers(deploymentProperties); if (initContainers != null && !initContainers.isEmpty()) { for (Container initContainer : initContainers) { if (initContainer.getSecurityContext() == null && containerSecurityContext != null) { initContainer.setSecurityContext(containerSecurityContext); } podSpec.addToInitContainers(initContainer); } } Boolean shareProcessNamespace = this.deploymentPropertiesResolver.getShareProcessNamespace(deploymentProperties); if (shareProcessNamespace != null) { podSpec.withShareProcessNamespace(shareProcessNamespace); } String priorityClassName = this.deploymentPropertiesResolver.getPriorityClassName(deploymentProperties); if (StringUtils.hasText(priorityClassName)) { podSpec.withPriorityClassName(priorityClassName); } List additionalContainers = this.deploymentPropertiesResolver.getAdditionalContainers(deploymentProperties); if (containerSecurityContext != null && !CollectionUtils.isEmpty(additionalContainers)) { additionalContainers.stream().filter((c) -> c.getSecurityContext() == null) .forEach((c) -> c.setSecurityContext(containerSecurityContext)); } podSpec.addAllToContainers(additionalContainers); List allContainers = new ArrayList<>(); allContainers.add(container); allContainers.addAll(additionalContainers); // only add volumes with corresponding volume mounts in any container. podSpec.withVolumes(this.deploymentPropertiesResolver.getVolumes(deploymentProperties).stream() .filter(volume -> allContainers.stream() .anyMatch(c -> c.getVolumeMounts() .stream() .anyMatch(volumeMount -> volumeMount.getName().equals(volume.getName())) ) ) .collect(Collectors.toList())); return podSpec.build(); } int getExternalPort(final AppDeploymentRequest request) { int externalPort = 8080; Map parameters = request.getDefinition().getProperties(); if (parameters.containsKey(SERVER_PORT_KEY)) { externalPort = Integer.valueOf(parameters.get(SERVER_PORT_KEY)); } return externalPort; } String createDeploymentId(AppDeploymentRequest request) { String groupId = request.getDeploymentProperties().get(AppDeployer.GROUP_PROPERTY_KEY); String deploymentId; if (groupId == null) { deploymentId = String.format("%s", request.getDefinition().getName()); } else { deploymentId = String.format("%s-%s", groupId, request.getDefinition().getName()); } // Kubernetes does not allow . in the name and does not allow uppercase in the name return deploymentId.replace('.', '-').toLowerCase(Locale.ROOT); } /** * Return the Secret corresponds to the name of ProbeCredentialSecret * @param kubernetesDeployerProperties the kubernetes deployer properties * @return the Secret Object */ Secret getProbeCredentialsSecret(Map kubernetesDeployerProperties) { String secretName = PropertyParserUtils.getDeploymentPropertyValue(kubernetesDeployerProperties, this.deploymentPropertiesResolver.getPropertyPrefix() + ".probeCredentialsSecret"); if (StringUtils.hasText(secretName)) { return this.client.secrets().withName(secretName).get(); } return null; } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/CommandProbeCreator.java ================================================ /* * Copyright 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.cloud.deployer.spi.kubernetes; import io.fabric8.kubernetes.api.model.ExecActionBuilder; import io.fabric8.kubernetes.api.model.Probe; import io.fabric8.kubernetes.api.model.ProbeBuilder; /** * Base class for command based probe creators * * @author Chris Schaefer * @author Corneil du Plessis * @since 2.5 */ abstract class CommandProbeCreator extends ProbeCreator { CommandProbeCreator(KubernetesDeployerProperties kubernetesDeployerProperties, ContainerConfiguration containerConfiguration) { super(kubernetesDeployerProperties, containerConfiguration); } abstract String[] getCommand(); protected Probe create() { ExecActionBuilder execActionBuilder = new ExecActionBuilder() .withCommand(getCommand()); return new ProbeBuilder() .withExec(execActionBuilder.build()) .withInitialDelaySeconds(getInitialDelay()) .withPeriodSeconds(getPeriod()) .withSuccessThreshold(getSuccess()) .withFailureThreshold(getFailure()) .build(); } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/CompositeDeploymentStateResolver.java ================================================ /* * Copyright 2017 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.cloud.deployer.spi.kubernetes; import io.fabric8.kubernetes.api.model.ContainerStatus; import org.springframework.cloud.deployer.spi.app.DeploymentState; /** * @author David Turanski **/ class CompositeDeploymentStateResolver implements RunningPhaseDeploymentStateResolver { private final RunningPhaseDeploymentStateResolver[] delegates; CompositeDeploymentStateResolver(RunningPhaseDeploymentStateResolver... delegates) { this.delegates = delegates; } @Override public DeploymentState resolve(ContainerStatus containerStatus) { for (RunningPhaseDeploymentStateResolver resolver: delegates) { DeploymentState deploymentState = resolver.resolve(containerStatus); if (deploymentState != null) { return deploymentState; } } return null; } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/ContainerConfiguration.java ================================================ /* * Copyright 2018 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.cloud.deployer.spi.kubernetes; import io.fabric8.kubernetes.api.model.Secret; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; /** * Encapsulates parameters used to configure a container. * * @author Chris Schaefer */ public class ContainerConfiguration { private String appId; private Integer externalPort; private boolean isHostNetwork; private Secret probeCredentialsSecret; private AppDeploymentRequest appDeploymentRequest; public ContainerConfiguration(String appId, AppDeploymentRequest appDeploymentRequest) { this.appId = appId; this.appDeploymentRequest = appDeploymentRequest; } public AppDeploymentRequest getAppDeploymentRequest() { return appDeploymentRequest; } public String getAppId() { return appId; } public boolean isHostNetwork() { return isHostNetwork; } public ContainerConfiguration withHostNetwork(boolean isHostNetwork) { this.isHostNetwork = isHostNetwork; return this; } public ContainerConfiguration withExternalPort(Integer externalPort) { this.externalPort = externalPort; return this; } public Integer getExternalPort() { return externalPort; } public ContainerConfiguration withProbeCredentialsSecret(Secret probeCredentialsSecret) { this.probeCredentialsSecret = probeCredentialsSecret; return this; } public Secret getProbeCredentialsSecret() { return probeCredentialsSecret; } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/ContainerFactory.java ================================================ /* * Copyright 2015-2019 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.cloud.deployer.spi.kubernetes; import io.fabric8.kubernetes.api.model.Container; /** * Defines how a Kubernetes {@link Container} is created. * * @author Florian Rosenberg * @author Thomas Risberg * @author David Turanski * @author Chris Schaefer */ public interface ContainerFactory { /** * Creates a {@link Container} using configuration from the provided {@link ContainerConfiguration}. * * @param containerConfiguration the {@link ContainerConfiguration} * @return a {@link Container} */ Container create(ContainerConfiguration containerConfiguration); } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/DefaultContainerFactory.java ================================================ /* * Copyright 2015-2022 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.cloud.deployer.spi.kubernetes; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import io.fabric8.kubernetes.api.model.Container; import io.fabric8.kubernetes.api.model.ContainerBuilder; import io.fabric8.kubernetes.api.model.EnvFromSource; import io.fabric8.kubernetes.api.model.EnvVar; import io.fabric8.kubernetes.api.model.EnvVarSource; import io.fabric8.kubernetes.api.model.ObjectFieldSelector; import io.fabric8.kubernetes.api.model.Probe; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.cloud.deployer.spi.app.AppDeployer; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.scheduler.ScheduleRequest; import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** * Create a Kubernetes {@link Container} that will be started as part of a * Kubernetes Pod by launching the specified Docker image. * * @author Florian Rosenberg * @author Thomas Risberg * @author Donovan Muller * @author David Turanski * @author Chris Schaefer * @author Ilayaperumal Gopinathan * @author Glenn Renfro * @author Corneil du Plessis */ public class DefaultContainerFactory implements ContainerFactory { private static Log logger = LogFactory.getLog(DefaultContainerFactory.class); private static final String SPRING_APPLICATION_JSON = "SPRING_APPLICATION_JSON"; private static final String SPRING_CLOUD_APPLICATION_GUID = "SPRING_CLOUD_APPLICATION_GUID"; private final KubernetesDeployerProperties properties; public DefaultContainerFactory(KubernetesDeployerProperties properties) { this.properties = properties; } @Override public Container create(ContainerConfiguration containerConfiguration) { AppDeploymentRequest request = containerConfiguration.getAppDeploymentRequest(); Map deploymentProperties = getDeploymentProperties(request); DeploymentPropertiesResolver deploymentPropertiesResolver = getDeploymentPropertiesResolver(request); String image; try { image = request.getResource().getURI().getSchemeSpecificPart(); } catch (IOException e) { throw new IllegalArgumentException("Unable to get URI for " + request.getResource(), e); } logger.info("Using Docker image: " + image); EntryPointStyle entryPointStyle = deploymentPropertiesResolver.determineEntryPointStyle(deploymentProperties); logger.info("Using Docker entry point style: " + entryPointStyle); Map envVarsMap = new HashMap<>(); for (String envVar : this.properties.getEnvironmentVariables()) { String[] strings = envVar.split("=", 2); Assert.isTrue(strings.length == 2, "Invalid environment variable declared: " + envVar); envVarsMap.put(strings[0], strings[1]); } //Create EnvVar entries for additional variables set at the app level //For instance, this may be used to set JAVA_OPTS independently for each app if the base container //image supports it. envVarsMap.putAll(deploymentPropertiesResolver.getAppEnvironmentVariables(deploymentProperties)); List appArgs = new ArrayList<>(); Map appAdminCredentials = new HashMap<>(); properties.getAppAdmin().addCredentialsToAppEnvironmentAsProperties(appAdminCredentials); switch (entryPointStyle) { case exec: appArgs = createCommandArgs(request); List finalAppArgs = appArgs; appAdminCredentials.forEach((k, v) -> finalAppArgs.add(String.format("--%s=%s", k, v))); break; case boot: if (envVarsMap.containsKey(SPRING_APPLICATION_JSON)) { throw new IllegalStateException( "You can't use boot entry point style and also set SPRING_APPLICATION_JSON for the app"); } try { envVarsMap.put(SPRING_APPLICATION_JSON, new ObjectMapper().writeValueAsString(request.getDefinition().getProperties())); } catch (JsonProcessingException e) { throw new IllegalStateException("Unable to create SPRING_APPLICATION_JSON", e); } appArgs = request.getCommandlineArguments(); break; case shell: for (String key : request.getDefinition().getProperties().keySet()) { String envVar = key.replace('.', '_').toUpperCase(Locale.ROOT); envVarsMap.put(envVar, request.getDefinition().getProperties().get(key)); envVarsMap.putAll(appAdminCredentials); } // Push all the command line arguments as environment properties // The task app name(in case of Composed Task), platform_name and executionId are expected to be updated. // This will also override any of the existing app properties that match the provided cmdline args. for (String cmdLineArg : request.getCommandlineArguments()) { String cmdLineArgKey; if (cmdLineArg.startsWith("--")) { cmdLineArgKey = cmdLineArg.substring(2, cmdLineArg.indexOf("=")); } else { cmdLineArgKey = cmdLineArg.substring(0, cmdLineArg.indexOf("=")); } String cmdLineArgValue = cmdLineArg.substring(cmdLineArg.indexOf("=") + 1); envVarsMap.put(cmdLineArgKey.replace('.', '_').toUpperCase(Locale.ROOT), cmdLineArgValue); } break; } List envVars = new ArrayList<>(); for (Map.Entry e : envVarsMap.entrySet()) { envVars.add(new EnvVar(e.getKey(), e.getValue(), null)); } envVars.addAll(deploymentPropertiesResolver.getSecretKeyRefs(deploymentProperties)); envVars.addAll(deploymentPropertiesResolver.getConfigMapKeyRefs(deploymentProperties)); envVars.add(getGUIDEnvVar()); if (request.getDeploymentProperties().get(AppDeployer.GROUP_PROPERTY_KEY) != null) { envVars.add(new EnvVar("SPRING_CLOUD_APPLICATION_GROUP", request.getDeploymentProperties().get(AppDeployer.GROUP_PROPERTY_KEY), null)); } List envFromSources = new ArrayList<>(); envFromSources.addAll(deploymentPropertiesResolver.getConfigMapRefs(deploymentProperties)); envFromSources.addAll(deploymentPropertiesResolver.getSecretRefs(deploymentProperties)); ContainerBuilder container = new ContainerBuilder(); container.withName(containerConfiguration.getAppId()).withImage(image).withEnv(envVars).withEnvFrom(envFromSources) .withArgs(appArgs).withImagePullPolicy(deploymentPropertiesResolver.getImagePullPolicy(deploymentProperties)) .withVolumeMounts(deploymentPropertiesResolver.getVolumeMounts(deploymentProperties)); Set ports = new HashSet<>(); Integer defaultPort = containerConfiguration.getExternalPort(); if (defaultPort != null) { ports.add(defaultPort); } ports.addAll(deploymentPropertiesResolver.getContainerPorts(deploymentProperties)); configureStartupProbe(containerConfiguration, container, ports); configureReadinessProbe(containerConfiguration, container, ports); configureLivenessProbe(containerConfiguration, container, ports); if (!ports.isEmpty()) { for (Integer containerPort : ports) { if (containerConfiguration.isHostNetwork()) { container.addNewPort().withContainerPort(containerPort).withHostPort(containerPort).endPort(); } else { container.addNewPort().withContainerPort(containerPort).endPort(); } } } //Override the containers default entry point with one specified during the app deployment List containerCommand = deploymentPropertiesResolver.getContainerCommand(deploymentProperties); if (!containerCommand.isEmpty()) { container.withCommand(containerCommand); } return container.build(); } private EnvVar getGUIDEnvVar() { ObjectFieldSelector objectFieldSelector = new ObjectFieldSelector(); objectFieldSelector.setFieldPath("metadata.uid"); EnvVarSource envVarSource = new EnvVarSource(); envVarSource.setFieldRef(objectFieldSelector); EnvVar guidEnvVar = new EnvVar(); guidEnvVar.setValueFrom(envVarSource); guidEnvVar.setName(SPRING_CLOUD_APPLICATION_GUID); return guidEnvVar; } private void configureReadinessProbe(ContainerConfiguration containerConfiguration, ContainerBuilder containerBuilder, Set ports) { Probe readinessProbe = ProbeCreatorFactory.createReadinessProbe(containerConfiguration, properties, getProbeType(containerConfiguration)); Integer probePort = null; if (readinessProbe.getHttpGet() != null) { probePort = readinessProbe.getHttpGet().getPort().getIntVal(); } if (readinessProbe.getTcpSocket() != null) { probePort = readinessProbe.getTcpSocket().getPort().getIntVal(); } if (probePort != null || (containerConfiguration.getExternalPort() != null && readinessProbe.getExec() != null)) { containerBuilder.withReadinessProbe(readinessProbe); } if (probePort != null) { ports.add(probePort); } } private void configureStartupProbe(ContainerConfiguration containerConfiguration, ContainerBuilder containerBuilder, Set ports) { Probe startupProbe = ProbeCreatorFactory.createStartupProbe(containerConfiguration, properties, getProbeType(containerConfiguration)); Integer probePort = null; if (startupProbe.getHttpGet() != null) { probePort = startupProbe.getHttpGet().getPort().getIntVal(); } if (startupProbe.getTcpSocket() != null) { probePort = startupProbe.getTcpSocket().getPort().getIntVal(); } if (probePort != null || (containerConfiguration.getExternalPort() != null && startupProbe.getExec() != null)) { containerBuilder.withStartupProbe(startupProbe); } if (probePort != null) { ports.add(probePort); } } private void configureLivenessProbe(ContainerConfiguration containerConfiguration, ContainerBuilder containerBuilder, Set ports) { Probe livenessProbe = ProbeCreatorFactory.createLivenessProbe(containerConfiguration, properties, getProbeType(containerConfiguration)); Integer probePort = null; if (livenessProbe.getHttpGet() != null) { probePort = livenessProbe.getHttpGet().getPort().getIntVal(); } if (livenessProbe.getTcpSocket() != null) { probePort = livenessProbe.getTcpSocket().getPort().getIntVal(); } if (probePort != null || (containerConfiguration.getExternalPort() != null && livenessProbe.getExec() != null)) { containerBuilder.withLivenessProbe(livenessProbe); } if (probePort != null) { ports.add(probePort); } } private ProbeType getProbeType(ContainerConfiguration containerConfiguration) { AppDeploymentRequest appDeploymentRequest = containerConfiguration.getAppDeploymentRequest(); Map deploymentProperties = getDeploymentProperties(appDeploymentRequest); DeploymentPropertiesResolver deploymentPropertiesResolver = getDeploymentPropertiesResolver(appDeploymentRequest); return deploymentPropertiesResolver.determineProbeType(deploymentProperties); } /** * Create command arguments * * @param request the {@link AppDeploymentRequest} * @return the command line arguments to use */ List createCommandArgs(AppDeploymentRequest request) { List cmdArgs = new LinkedList<>(); List commandArgOptions = request.getCommandlineArguments().stream() .map(this::getArgOption) .collect(Collectors.toList()); // add properties from deployment request Map args = request.getDefinition().getProperties(); for (Map.Entry entry : args.entrySet()) { if (!StringUtils.hasText(entry.getValue())) { logger.warn( "Excluding request property with missing value from command args: " + entry.getKey()); } else if (commandArgOptions.contains(entry.getKey())) { logger.warn( String.format( "Excluding request property [--%s=%s] as a command arg. Existing command line argument takes precedence." , entry.getKey(), entry.getValue())); } else { cmdArgs.add(String.format("--%s=%s", entry.getKey(), entry.getValue())); } } // add provided command line args cmdArgs.addAll(request.getCommandlineArguments()); logger.debug("Using command args: " + cmdArgs); return cmdArgs; } private String getArgOption(String arg) { int indexOfAssignment = arg.indexOf("="); String argOption = (indexOfAssignment < 0) ? arg : arg.substring(0, indexOfAssignment); return argOption.trim().replaceAll("^--", ""); } private DeploymentPropertiesResolver getDeploymentPropertiesResolver(AppDeploymentRequest request) { String propertiesPrefix = (request instanceof ScheduleRequest && ((ScheduleRequest) request).getSchedulerProperties() != null && ((ScheduleRequest) request).getSchedulerProperties().size() > 0) ? KubernetesSchedulerProperties.KUBERNETES_SCHEDULER_PROPERTIES_PREFIX : KubernetesDeployerProperties.KUBERNETES_DEPLOYER_PROPERTIES_PREFIX; return new DeploymentPropertiesResolver(propertiesPrefix, this.properties); } private Map getDeploymentProperties(AppDeploymentRequest request) { return request.getDeploymentProperties(); } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/DefaultRunningPhaseDeploymentStateResolver.java ================================================ /* * Copyright 2017 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.cloud.deployer.spi.kubernetes; import org.springframework.cloud.deployer.spi.app.DeploymentState; /** * @author David Turanski **/ public class DefaultRunningPhaseDeploymentStateResolver extends CompositeDeploymentStateResolver { public DefaultRunningPhaseDeploymentStateResolver(KubernetesDeployerProperties properties) { super( new PredicateRunningPhaseDeploymentStateResolver.ContainerReady(properties), new PredicateRunningPhaseDeploymentStateResolver.ContainerCrashed(properties), new PredicateRunningPhaseDeploymentStateResolver.RestartsDueToTheSameError(properties), new PredicateRunningPhaseDeploymentStateResolver.CrashLoopBackOffRestarts(properties), new PredicateRunningPhaseDeploymentStateResolver.ContainerTerminated(properties), //default containerStatus -> DeploymentState.deploying); } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/DeploymentPropertiesResolver.java ================================================ /* * Copyright 2020-2023 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.cloud.deployer.spi.kubernetes; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.Properties; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import io.fabric8.kubernetes.api.model.Affinity; import io.fabric8.kubernetes.api.model.AffinityBuilder; import io.fabric8.kubernetes.api.model.CapabilitiesBuilder; import io.fabric8.kubernetes.api.model.ConfigMapEnvSource; import io.fabric8.kubernetes.api.model.ConfigMapKeySelector; import io.fabric8.kubernetes.api.model.Container; import io.fabric8.kubernetes.api.model.ContainerBuilder; import io.fabric8.kubernetes.api.model.EnvFromSource; import io.fabric8.kubernetes.api.model.EnvVar; import io.fabric8.kubernetes.api.model.EnvVarBuilder; import io.fabric8.kubernetes.api.model.EnvVarSource; import io.fabric8.kubernetes.api.model.EnvVarSourceBuilder; import io.fabric8.kubernetes.api.model.ObjectFieldSelector; import io.fabric8.kubernetes.api.model.ObjectFieldSelectorBuilder; import io.fabric8.kubernetes.api.model.PodSecurityContext; import io.fabric8.kubernetes.api.model.PodSecurityContextBuilder; import io.fabric8.kubernetes.api.model.Quantity; import io.fabric8.kubernetes.api.model.SecretEnvSource; import io.fabric8.kubernetes.api.model.SecretKeySelector; import io.fabric8.kubernetes.api.model.SecurityContext; import io.fabric8.kubernetes.api.model.SecurityContextBuilder; import io.fabric8.kubernetes.api.model.Sysctl; import io.fabric8.kubernetes.api.model.SysctlBuilder; import io.fabric8.kubernetes.api.model.Toleration; import io.fabric8.kubernetes.api.model.Volume; import io.fabric8.kubernetes.api.model.VolumeMount; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.config.YamlPropertiesFactoryBean; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.kubernetes.KubernetesDeployerProperties.ConfigMapKeyRef; import org.springframework.cloud.deployer.spi.kubernetes.KubernetesDeployerProperties.InitContainer; import org.springframework.cloud.deployer.spi.kubernetes.KubernetesDeployerProperties.SecretKeyRef; import org.springframework.cloud.deployer.spi.kubernetes.support.PropertyParserUtils; import org.springframework.cloud.deployer.spi.kubernetes.support.RelaxedNames; import org.springframework.cloud.deployer.spi.util.ByteSizeUtils; import org.springframework.cloud.deployer.spi.util.CommandLineTokenizer; import org.springframework.core.io.ByteArrayResource; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; /** * Class that resolves the appropriate deployment properties based on the property prefix being used. * Currently, both the Deployer/TaskLauncher and the Scheduler use this resolver to retrieve the * deployment properties for the given Kubernetes deployer properties. * * @author Chris Schaefer * @author Ilayaperumal Gopinathan * @author Chris Bono * @author Corneil du Plessis */ class DeploymentPropertiesResolver { static final String STATEFUL_SET_IMAGE_NAME = "busybox"; private final Log logger = LogFactory.getLog(getClass().getName()); private String propertyPrefix; private KubernetesDeployerProperties properties; DeploymentPropertiesResolver(String propertyPrefix, KubernetesDeployerProperties properties) { this.propertyPrefix = propertyPrefix; this.properties = properties; } String getPropertyPrefix() { return this.propertyPrefix; } List getTolerations(Map kubernetesDeployerProperties) { List tolerations = new ArrayList<>(); KubernetesDeployerProperties deployerProperties = bindProperties(kubernetesDeployerProperties, this.propertyPrefix + ".tolerations", "tolerations" ); deployerProperties.getTolerations().forEach(toleration -> tolerations.add( new Toleration(toleration.getEffect(), toleration.getKey(), toleration.getOperator(), toleration.getTolerationSeconds(), toleration.getValue()))); this.properties.getTolerations().stream() .filter(toleration -> tolerations.stream() .noneMatch(existing -> existing.getKey().equals(toleration.getKey()))) .collect(Collectors.toList()) .forEach(toleration -> tolerations.add(new Toleration(toleration.getEffect(), toleration.getKey(), toleration.getOperator(), toleration.getTolerationSeconds(), toleration.getValue()))); return tolerations; } /** * Volume deployment properties are specified in YAML format: * * * spring.cloud.deployer.kubernetes.volumes=[{name: testhostpath, hostPath: { path: '/test/override/hostPath' }}, * {name: 'testpvc', persistentVolumeClaim: { claimName: 'testClaim', readOnly: 'true' }}, * {name: 'testnfs', nfs: { server: '10.0.0.1:111', path: '/test/nfs' }}] * * * Volumes can be specified as deployer properties as well as app deployment properties. * Deployment properties override deployer properties. * * @param kubernetesDeployerProperties the kubernetes deployer properties map * @return the configured volumes */ List getVolumes(Map kubernetesDeployerProperties) { List volumes = new ArrayList<>(); KubernetesDeployerProperties deployerProperties = bindProperties(kubernetesDeployerProperties, this.propertyPrefix + ".volumes", "volumes"); volumes.addAll(deployerProperties.getVolumes()); // only add volumes that have not already been added, based on the volume's name // i.e. allow provided deployment volumes to override deployer defined volumes volumes.addAll(properties.getVolumes().stream() .filter(volume -> volumes.stream() .noneMatch(existingVolume -> existingVolume.getName().equals(volume.getName()))) .collect(Collectors.toList())); return volumes; } /** * Get the resource limits for the deployment request. A Pod can define its maximum needed resources by setting the * limits and Kubernetes can provide more resources if any are free. *

* Falls back to the server properties if not present in the deployment request. *

* * @param kubernetesDeployerProperties the kubernetes deployment properties map * @return the resource limits to use */ Map deduceResourceLimits(Map kubernetesDeployerProperties) { String memory = PropertyParserUtils.getDeploymentPropertyValue(kubernetesDeployerProperties, this.propertyPrefix + ".limits.memory", properties.getLimits().getMemory()); String cpu = PropertyParserUtils.getDeploymentPropertyValue(kubernetesDeployerProperties, this.propertyPrefix + ".limits.cpu", properties.getLimits().getCpu()); String ephemeralStorage = PropertyParserUtils.getDeploymentPropertyValue(kubernetesDeployerProperties, ".limits.ephemeral-storage", properties.getLimits().getEphemeralStorage()); String hugePages2Mi = PropertyParserUtils.getDeploymentPropertyValue(kubernetesDeployerProperties, ".limits.hugepages-2Mi", properties.getLimits().getHugepages2Mi()); String hugePages1Gi = PropertyParserUtils.getDeploymentPropertyValue(kubernetesDeployerProperties, ".limits.hugepages-1Gi", properties.getLimits().getHugepages1Gi()); String gpuVendor = PropertyParserUtils.getDeploymentPropertyValue(kubernetesDeployerProperties, this.propertyPrefix + ".limits.gpuVendor", properties.getLimits().getGpuVendor()); String gpuCount = PropertyParserUtils.getDeploymentPropertyValue(kubernetesDeployerProperties, this.propertyPrefix + ".limits.gpuCount", properties.getLimits().getGpuCount()); Map limits = new HashMap(); if (StringUtils.hasText(memory)) { limits.put("memory", new Quantity(memory)); } if (StringUtils.hasText(cpu)) { limits.put("cpu", new Quantity(cpu)); } if(StringUtils.hasText(ephemeralStorage)) { limits.put("ephemeral-storage", new Quantity(ephemeralStorage)); } if(StringUtils.hasText(hugePages2Mi)) { limits.put("hugepages-2Mi", new Quantity(hugePages2Mi)); } if(StringUtils.hasText(hugePages1Gi)) { limits.put("hugepages-1Gi", new Quantity(hugePages1Gi)); } if (StringUtils.hasText(gpuVendor) && StringUtils.hasText(gpuCount)) { limits.put(gpuVendor, new Quantity(gpuCount)); } return limits; } /** * Get the image pull policy for the deployment request. If it is not present use the server default. If an override * for the deployment is present but not parseable, fall back to a default value. * * @param kubernetesDeployerProperties the kubernetes deployment properties map * @return The image pull policy to use for the container in the request. */ ImagePullPolicy deduceImagePullPolicy(Map kubernetesDeployerProperties) { String pullPolicyOverride = PropertyParserUtils.getDeploymentPropertyValue(kubernetesDeployerProperties, this.propertyPrefix + ".imagePullPolicy"); ImagePullPolicy pullPolicy; if (pullPolicyOverride == null) { pullPolicy = properties.getImagePullPolicy(); } else { pullPolicy = ImagePullPolicy.relaxedValueOf(pullPolicyOverride); if (pullPolicy == null) { logger.warn("Parsing of pull policy " + pullPolicyOverride + " failed, using default \"IfNotPresent\"."); pullPolicy = ImagePullPolicy.IfNotPresent; } } logger.debug("Using imagePullPolicy " + pullPolicy); return pullPolicy; } /** * Get the resource requests for the deployment request. Resource requests are guaranteed by the Kubernetes * runtime. * Falls back to the server properties if not present in the deployment request. * * @param kubernetesDeployerProperties the kubernetes deployer properties map * @return the resource requests to use */ Map deduceResourceRequests(Map kubernetesDeployerProperties) { String memOverride = PropertyParserUtils.getDeploymentPropertyValue(kubernetesDeployerProperties, this.propertyPrefix + ".requests.memory", properties.getRequests().getMemory()); String cpuOverride = PropertyParserUtils.getDeploymentPropertyValue(kubernetesDeployerProperties, this.propertyPrefix + ".requests.cpu", properties.getRequests().getCpu()); logger.debug("Using requests - cpu: " + cpuOverride + " mem: " + memOverride); String ephemeralStorage = PropertyParserUtils.getDeploymentPropertyValue(kubernetesDeployerProperties, ".requests.ephemeral-storage", properties.getRequests().getEphemeralStorage()); String hugePages2Mi = PropertyParserUtils.getDeploymentPropertyValue(kubernetesDeployerProperties, ".requests.hugepages-2Mi", properties.getRequests().getHugepages2Mi()); String hugePages1Gi = PropertyParserUtils.getDeploymentPropertyValue(kubernetesDeployerProperties, ".requests.hugepages-1Gi", properties.getRequests().getHugepages1Gi()); Map requests = new HashMap(); if (StringUtils.hasText(memOverride)) { requests.put("memory", new Quantity(memOverride)); } if (StringUtils.hasText(cpuOverride)) { requests.put("cpu", new Quantity(cpuOverride)); } if(StringUtils.hasText(ephemeralStorage)) { requests.put("ephemeral-storage", new Quantity(ephemeralStorage)); } if(StringUtils.hasText(hugePages2Mi)) { requests.put("hugepages-2Mi", new Quantity(hugePages2Mi)); } if(StringUtils.hasText(hugePages1Gi)) { requests.put("hugepages-1Gi", new Quantity(hugePages1Gi)); } return requests; } /** * Get the VolumeClaim template name for Statefulset from the deployment properties. * * @param kubernetesDeployerProperties the kubernetes deployer properties * @return the volume claim template name */ String getStatefulSetVolumeClaimTemplateName(Map kubernetesDeployerProperties) { String name = PropertyParserUtils.getDeploymentPropertyValue(kubernetesDeployerProperties, this.propertyPrefix + ".statefulSet.volumeClaimTemplate.name"); if (name == null && properties.getStatefulSet() != null && properties.getStatefulSet().getVolumeClaimTemplate() != null) { name = properties.getStatefulSet().getVolumeClaimTemplate().getName(); } return name; } /** * Get the StatefulSet storage class name to be set in VolumeClaim template for the deployment properties. * * @param kubernetesDeployerProperties the kubernetes deployer properties * @return the storage class name */ String getStatefulSetStorageClassName(Map kubernetesDeployerProperties) { String storageClassName = PropertyParserUtils.getDeploymentPropertyValue(kubernetesDeployerProperties, this.propertyPrefix + ".statefulSet.volumeClaimTemplate.storageClassName"); if (storageClassName == null && properties.getStatefulSet() != null && properties.getStatefulSet().getVolumeClaimTemplate() != null) { storageClassName = properties.getStatefulSet().getVolumeClaimTemplate().getStorageClassName(); } return storageClassName; } /** * Get the StatefulSet storage value to be set in VolumeClaim template for the given deployment properties. * * @param kubernetesDeployerProperties the kubernetes deployer properties * @return the StatefulSet storage */ String getStatefulSetStorage(Map kubernetesDeployerProperties) { String storage = PropertyParserUtils.getDeploymentPropertyValue(kubernetesDeployerProperties, this.propertyPrefix + ".statefulSet.volumeClaimTemplate.storage"); if (storage == null && properties.getStatefulSet() != null && properties.getStatefulSet().getVolumeClaimTemplate() != null) { storage = properties.getStatefulSet().getVolumeClaimTemplate().getStorage(); } return ByteSizeUtils.parseToMebibytes(storage) + "Mi"; } /** * Get the hostNetwork setting for the deployment request. * * @param kubernetesDeployerProperties the kubernetes deployment properties map * @return Whether host networking is requested */ boolean getHostNetwork(Map kubernetesDeployerProperties) { String hostNetworkOverride = PropertyParserUtils.getDeploymentPropertyValue(kubernetesDeployerProperties, this.propertyPrefix + ".hostNetwork"); boolean hostNetwork; if (!StringUtils.hasText(hostNetworkOverride)) { hostNetwork = properties.isHostNetwork(); } else { hostNetwork = Boolean.valueOf(hostNetworkOverride); } logger.debug("Using hostNetwork " + hostNetwork); return hostNetwork; } /** * Get the nodeSelectors setting for the deployment request. * * @param deploymentProperties The deployment request deployment properties. * @return map of nodeSelectors */ Map getNodeSelectors(Map deploymentProperties) { Map nodeSelectors = new HashMap<>(); String nodeSelector = this.properties.getNodeSelector(); String nodeSelectorDeploymentProperty = deploymentProperties.getOrDefault(KubernetesDeployerProperties.KUBERNETES_DEPLOYMENT_NODE_SELECTOR, ""); for (String name : RelaxedNames.forCamelCase(KubernetesDeployerProperties.KUBERNETES_DEPLOYMENT_NODE_SELECTOR)) { String value = deploymentProperties.get(name); if (StringUtils.hasText(value)) { nodeSelectorDeploymentProperty = value; break; } } boolean hasDeployerPropertyNodeSelector = StringUtils.hasText(nodeSelectorDeploymentProperty); if (hasDeployerPropertyNodeSelector) { nodeSelector = nodeSelectorDeploymentProperty; } if (StringUtils.hasText(nodeSelector)) { String[] nodeSelectorPairs = nodeSelector.split(","); for (String nodeSelectorPair : nodeSelectorPairs) { String[] selector = nodeSelectorPair.split(":"); Assert.isTrue(selector.length == 2, String.format("Invalid nodeSelector value: '%s'", nodeSelectorPair)); nodeSelectors.put(selector[0].trim(), selector[1].trim()); } } return nodeSelectors; } String getImagePullPolicy(Map kubernetesDeployerProperties) { ImagePullPolicy imagePullPolicy = deduceImagePullPolicy(kubernetesDeployerProperties); return imagePullPolicy != null ? imagePullPolicy.name() : null; } String getImagePullSecret(Map kubernetesDeployerProperties) { String imagePullSecret = PropertyParserUtils.getDeploymentPropertyValue(kubernetesDeployerProperties, this.propertyPrefix + ".imagePullSecret", ""); if(!StringUtils.hasText(imagePullSecret)) { imagePullSecret = this.properties.getImagePullSecret(); } return imagePullSecret; } List getImagePullSecrets(Map kubernetesDeployerProperties) { KubernetesDeployerProperties deployerProperties = bindProperties(kubernetesDeployerProperties, this.propertyPrefix + ".imagePullSecrets", "imagePullSecrets"); if (deployerProperties.getImagePullSecrets() == null || deployerProperties.getImagePullSecrets().isEmpty()) { return properties.getImagePullSecrets(); } else { return deployerProperties.getImagePullSecrets(); } } String getDeploymentServiceAccountName(Map kubernetesDeployerProperties) { String deploymentServiceAccountName = PropertyParserUtils.getDeploymentPropertyValue(kubernetesDeployerProperties, this.propertyPrefix + ".deploymentServiceAccountName"); if (!StringUtils.hasText(deploymentServiceAccountName)) { deploymentServiceAccountName = properties.getDeploymentServiceAccountName(); } return deploymentServiceAccountName; } Boolean getShareProcessNamespace(Map kubernetesDeployerProperties) { KubernetesDeployerProperties deployerProperties = bindProperties(kubernetesDeployerProperties, this.propertyPrefix + ".shareProcessNamespace", "shareProcessNamespace"); return deployerProperties.getShareProcessNamespace(); } String getPriorityClassName(Map kubernetesDeployerProperties) { KubernetesDeployerProperties deployerProperties = bindProperties(kubernetesDeployerProperties, this.propertyPrefix + ".priorityClassName", "priorityClassName"); return deployerProperties.getPriorityClassName(); } PodSecurityContext getPodSecurityContext(Map kubernetesDeployerProperties) { PodSecurityContext podSecurityContext = null; KubernetesDeployerProperties deployerProperties = bindProperties(kubernetesDeployerProperties, this.propertyPrefix + ".podSecurityContext", "podSecurityContext"); if (deployerProperties.getPodSecurityContext() != null) { podSecurityContext = buildPodSecurityContext(deployerProperties); } else if (this.properties.getPodSecurityContext() != null ) { podSecurityContext = buildPodSecurityContext(this.properties); } return podSecurityContext; } private PodSecurityContext buildPodSecurityContext(KubernetesDeployerProperties deployerProperties) { PodSecurityContextBuilder podSecurityContextBuilder = new PodSecurityContextBuilder() .withRunAsUser(deployerProperties.getPodSecurityContext().getRunAsUser()) .withRunAsGroup(deployerProperties.getPodSecurityContext().getRunAsGroup()) .withRunAsNonRoot(deployerProperties.getPodSecurityContext().getRunAsNonRoot()) .withFsGroup(deployerProperties.getPodSecurityContext().getFsGroup()) .withFsGroupChangePolicy(deployerProperties.getPodSecurityContext().getFsGroupChangePolicy()) .withSupplementalGroups(deployerProperties.getPodSecurityContext().getSupplementalGroups()); if (deployerProperties.getPodSecurityContext().getSeccompProfile() != null) { podSecurityContextBuilder.withNewSeccompProfile( deployerProperties.getPodSecurityContext().getSeccompProfile().getLocalhostProfile(), deployerProperties.getPodSecurityContext().getSeccompProfile().getType()); } if (deployerProperties.getPodSecurityContext().getSeLinuxOptions() != null) { podSecurityContextBuilder.withNewSeLinuxOptions( deployerProperties.getPodSecurityContext().getSeLinuxOptions().getLevel(), deployerProperties.getPodSecurityContext().getSeLinuxOptions().getRole(), deployerProperties.getPodSecurityContext().getSeLinuxOptions().getType(), deployerProperties.getPodSecurityContext().getSeLinuxOptions().getUser()); } if (!CollectionUtils.isEmpty(deployerProperties.getPodSecurityContext().getSysctls())) { List sysctls = deployerProperties.getPodSecurityContext().getSysctls().stream() .map((sysctlInfo) -> new SysctlBuilder().withName(sysctlInfo.getName()) .withValue(sysctlInfo.getValue()).build()) .collect(Collectors.toList()); podSecurityContextBuilder.withSysctls(sysctls); } if (deployerProperties.getPodSecurityContext().getWindowsOptions() != null) { podSecurityContextBuilder.withNewWindowsOptions( deployerProperties.getPodSecurityContext().getWindowsOptions().getGmsaCredentialSpec(), deployerProperties.getPodSecurityContext().getWindowsOptions().getGmsaCredentialSpecName(), deployerProperties.getPodSecurityContext().getWindowsOptions().getHostProcess(), deployerProperties.getPodSecurityContext().getWindowsOptions().getRunAsUserName()); } return podSecurityContextBuilder.build(); } SecurityContext getContainerSecurityContext(Map kubernetesDeployerProperties) { SecurityContext securityContext = null; KubernetesDeployerProperties deployerProperties = bindProperties(kubernetesDeployerProperties, this.propertyPrefix + ".containerSecurityContext", "containerSecurityContext"); if (deployerProperties.getContainerSecurityContext() != null) { securityContext = buildContainerSecurityContext(deployerProperties); } else if (this.properties.getContainerSecurityContext() != null ) { securityContext = buildContainerSecurityContext(this.properties); } return securityContext; } private SecurityContext buildContainerSecurityContext(KubernetesDeployerProperties deployerProperties) { SecurityContextBuilder securityContextBuilder = new SecurityContextBuilder() .withAllowPrivilegeEscalation(deployerProperties.getContainerSecurityContext().getAllowPrivilegeEscalation()) .withPrivileged(deployerProperties.getContainerSecurityContext().getPrivileged()) .withProcMount(deployerProperties.getContainerSecurityContext().getProcMount()) .withReadOnlyRootFilesystem(deployerProperties.getContainerSecurityContext().getReadOnlyRootFilesystem()) .withRunAsUser(deployerProperties.getContainerSecurityContext().getRunAsUser()) .withRunAsGroup(deployerProperties.getContainerSecurityContext().getRunAsGroup()) .withRunAsNonRoot(deployerProperties.getContainerSecurityContext().getRunAsNonRoot()); if (deployerProperties.getContainerSecurityContext().getCapabilities() != null) { securityContextBuilder.withCapabilities( new CapabilitiesBuilder().withAdd(deployerProperties.getContainerSecurityContext().getCapabilities().getAdd()) .withDrop(deployerProperties.getContainerSecurityContext().getCapabilities().getDrop()) .build()); } if (deployerProperties.getContainerSecurityContext().getSeccompProfile() != null) { securityContextBuilder.withNewSeccompProfile( deployerProperties.getContainerSecurityContext().getSeccompProfile().getLocalhostProfile(), deployerProperties.getContainerSecurityContext().getSeccompProfile().getType()); } if (deployerProperties.getContainerSecurityContext().getSeLinuxOptions() != null) { securityContextBuilder.withNewSeLinuxOptions( deployerProperties.getContainerSecurityContext().getSeLinuxOptions().getLevel(), deployerProperties.getContainerSecurityContext().getSeLinuxOptions().getRole(), deployerProperties.getContainerSecurityContext().getSeLinuxOptions().getType(), deployerProperties.getContainerSecurityContext().getSeLinuxOptions().getUser()); } if (deployerProperties.getContainerSecurityContext().getWindowsOptions() != null) { securityContextBuilder.withNewWindowsOptions( deployerProperties.getContainerSecurityContext().getWindowsOptions().getGmsaCredentialSpec(), deployerProperties.getContainerSecurityContext().getWindowsOptions().getGmsaCredentialSpecName(), deployerProperties.getContainerSecurityContext().getWindowsOptions().getHostProcess(), deployerProperties.getContainerSecurityContext().getWindowsOptions().getRunAsUserName()); } return securityContextBuilder.build(); } Affinity getAffinityRules(Map kubernetesDeployerProperties) { Affinity affinity = new Affinity(); String nodeAffinityPropertyKey = this.propertyPrefix + ".affinity.nodeAffinity"; String podAffinityPropertyKey = this.propertyPrefix + ".affinity.podAffinity"; String podAntiAffinityPropertyKey = this.propertyPrefix + ".affinity.podAntiAffinity"; String nodeAffinityValue = PropertyParserUtils.getDeploymentPropertyValue(kubernetesDeployerProperties, nodeAffinityPropertyKey); String podAffinityValue = PropertyParserUtils.getDeploymentPropertyValue(kubernetesDeployerProperties, podAffinityPropertyKey); String podAntiAffinityValue = PropertyParserUtils.getDeploymentPropertyValue(kubernetesDeployerProperties, podAntiAffinityPropertyKey); if (properties.getNodeAffinity() != null && !StringUtils.hasText(nodeAffinityValue)) { affinity.setNodeAffinity(new AffinityBuilder() .withNodeAffinity(properties.getNodeAffinity()) .buildNodeAffinity()); } else if (StringUtils.hasText(nodeAffinityValue)) { KubernetesDeployerProperties nodeAffinityProperties = bindProperties(kubernetesDeployerProperties, nodeAffinityPropertyKey, "nodeAffinity"); affinity.setNodeAffinity(new AffinityBuilder() .withNodeAffinity(nodeAffinityProperties.getNodeAffinity()) .buildNodeAffinity()); } if (properties.getPodAffinity() != null && !StringUtils.hasText(podAffinityValue)) { affinity.setPodAffinity(new AffinityBuilder() .withPodAffinity(properties.getPodAffinity()) .buildPodAffinity()); } else if (StringUtils.hasText(podAffinityValue)) { KubernetesDeployerProperties podAffinityProperties = bindProperties(kubernetesDeployerProperties, podAffinityPropertyKey, "podAffinity"); affinity.setPodAffinity(new AffinityBuilder() .withPodAffinity(podAffinityProperties.getPodAffinity()) .buildPodAffinity()); } if (properties.getPodAntiAffinity() != null && !StringUtils.hasText(podAntiAffinityValue)) { affinity.setPodAntiAffinity(new AffinityBuilder() .withPodAntiAffinity(properties.getPodAntiAffinity()) .buildPodAntiAffinity()); } else if (StringUtils.hasText(podAntiAffinityValue)) { KubernetesDeployerProperties podAntiAffinityProperties = bindProperties(kubernetesDeployerProperties, podAntiAffinityPropertyKey, "podAntiAffinity"); affinity.setPodAntiAffinity(new AffinityBuilder() .withPodAntiAffinity(podAntiAffinityProperties.getPodAntiAffinity()) .buildPodAntiAffinity()); } return affinity; } Collection getInitContainers(Map kubernetesDeployerProperties) { Collection initContainers = new ArrayList<>(); KubernetesDeployerProperties deployerProperties = bindProperties(kubernetesDeployerProperties, this.propertyPrefix + ".initContainer", "initContainer"); // Deployment prop passed in for entire '.initContainer' InitContainer initContainerProps = deployerProperties.getInitContainer(); if (initContainerProps != null) { initContainers.add(containerFromProps(initContainerProps)); } else { String propertyKey = this.propertyPrefix + ".initContainer"; Container container = initContainerFromProperties(kubernetesDeployerProperties, propertyKey); if (container != null) { initContainers.add(container); } else { initContainerProps = this.properties.getInitContainer(); if (initContainerProps != null) { initContainers.add(containerFromProps(initContainerProps)); } } } KubernetesDeployerProperties initContainerDeployerProperties = bindProperties(kubernetesDeployerProperties, this.propertyPrefix + ".initContainers", "initContainers"); for (InitContainer initContainer : initContainerDeployerProperties.getInitContainers()) { initContainers.add(containerFromProps(initContainer)); } if(initContainerDeployerProperties.getInitContainers().isEmpty()) { for (int i = 0; ; i++) { String propertyKey = this.propertyPrefix + ".initContainers[" + i + "]"; // Get properties using binding KubernetesDeployerProperties kubeProps = bindProperties(kubernetesDeployerProperties, propertyKey, "initContainer"); if (kubeProps.getInitContainer() != null) { initContainers.add(containerFromProps(kubeProps.getInitContainer())); } else { // Get properties using FQN Container initContainer = initContainerFromProperties(kubernetesDeployerProperties, propertyKey); if (initContainer != null) { initContainers.add(initContainer); } else { // Use default is configured if (properties.getInitContainers().size() > i) { initContainers.add(containerFromProps(properties.getInitContainers().get(i))); } break; } } } } if (!properties.getInitContainers().isEmpty()) { // Add remaining defaults. for (int i = initContainers.size(); i < properties.getInitContainers().size(); i++) { initContainers.add(containerFromProps(properties.getInitContainers().get(i))); } } return initContainers; } private @Nullable Container initContainerFromProperties(Map kubeProps, String propertyKey) { String name = getFirstProperty(kubeProps, propertyKey, ".name", ".containerName"); String image = getFirstProperty(kubeProps, propertyKey, ".image", ".imageName"); if (StringUtils.hasText(name) && StringUtils.hasText(image)) { String commandStr = getFirstProperty(kubeProps, propertyKey, ".command", ".commands"); List commands = StringUtils.hasText(commandStr) ? Arrays.asList(commandStr.split(",")) : Collections.emptyList(); String envString = getFirstProperty(kubeProps, propertyKey, ".env", ".environmentVariables"); List vms = this.getInitContainerVolumeMounts(kubeProps, propertyKey); return new ContainerBuilder() .withName(name) .withImage(image) .withCommand(commands) .withEnv(toEnvironmentVariables((envString != null) ? envString.split(",") : new String[0])) .addAllToVolumeMounts(vms) .build(); } return null; } public static String getFirstProperty(Map kubeProps, String baseKey, String... suffixes) { for (String suffix : suffixes) { String value = PropertyParserUtils.getDeploymentPropertyValue(kubeProps, baseKey + suffix); if (StringUtils.hasText(value)) { return value; } } return null; } private Container containerFromProps(InitContainer initContainerProps) { List envVarList = new ArrayList<>(); envVarList.addAll(toEnvironmentVariables(initContainerProps.getEnvironmentVariables())); envVarList.addAll(toEnvironmentVariablesFromFieldRef(initContainerProps.getEnvironmentVariablesFromFieldRefs())); List envFromSourceList = new ArrayList<>(); envFromSourceList.addAll(Arrays.stream(initContainerProps.getConfigMapRefEnvVars()).map(this::buildConfigMapRefEnvVar).collect(Collectors.toList())); envFromSourceList.addAll(Arrays.stream(initContainerProps.getSecretRefEnvVars()).map(this::buildSecretRefEnvVar).collect(Collectors.toList())); return new ContainerBuilder() .withName(initContainerProps.getName()) .withImage(initContainerProps.getImage()) .withCommand(initContainerProps.getCommand()) .withArgs(initContainerProps.getArgs()) .withEnv(envVarList) .withEnvFrom(envFromSourceList) .addAllToVolumeMounts(Optional.ofNullable(initContainerProps.getVolumeMounts()).orElse(Collections.emptyList())) .build(); } private List toEnvironmentVariables(String[] environmentVariables) { Map envVarsMap = new HashMap<>(); if (environmentVariables != null) { for (String envVar : environmentVariables) { String[] strings = envVar.split("=", 2); Assert.isTrue(strings.length == 2, "Invalid environment variable declared: " + envVar); envVarsMap.put(strings[0], strings[1]); } } List envVars = new ArrayList<>(); for (Map.Entry e : envVarsMap.entrySet()) { envVars.add(new EnvVar(e.getKey(), e.getValue(), null)); } return envVars; } private List toEnvironmentVariablesFromFieldRef(String[] environmentVariablesFromFieldRef) { if (environmentVariablesFromFieldRef == null || environmentVariablesFromFieldRef.length == 0) { return Collections.emptyList(); } return Arrays.stream(environmentVariablesFromFieldRef) .map(entry -> { String[] tokens = entry.split("=", 2); Assert.isTrue(tokens.length == 2 && StringUtils.hasText(tokens[0]) && StringUtils.hasText(tokens[1]), "Invalid environment variable from field ref: " + entry); ObjectFieldSelector fieldSelector = new ObjectFieldSelectorBuilder() .withFieldPath(tokens[1]) .build(); return new EnvVar(tokens[0], null, new EnvVarSourceBuilder().withFieldRef(fieldSelector).build()); }).collect(Collectors.toList()); } List getAdditionalContainers(Map deploymentProperties) { List containers = new ArrayList<>(); KubernetesDeployerProperties deployerProperties = bindProperties(deploymentProperties, this.propertyPrefix + ".additionalContainers", "additionalContainers" ); if (deployerProperties.getAdditionalContainers() != null) { deployerProperties.getAdditionalContainers().forEach(container -> containers.add(container)); } // Add the containers from the original properties excluding the containers with the matching names from the // deployment properties if (this.properties.getAdditionalContainers() != null) { this.properties.getAdditionalContainers().stream() .filter(container -> containers.stream().noneMatch(existing -> existing.getName().equals(container.getName()))) .forEachOrdered(container -> containers.add(container)); } return containers; } Map getPodAnnotations(Map kubernetesDeployerProperties) { String annotationsValue = PropertyParserUtils.getDeploymentPropertyValue(kubernetesDeployerProperties, this.propertyPrefix + ".podAnnotations", ""); if (!StringUtils.hasText(annotationsValue)) { annotationsValue = properties.getPodAnnotations(); } return PropertyParserUtils.getStringPairsToMap(annotationsValue); } Map getServiceAnnotations(Map kubernetesDeployerProperties) { String annotationsProperty = PropertyParserUtils.getDeploymentPropertyValue(kubernetesDeployerProperties, this.propertyPrefix + ".serviceAnnotations", ""); if (!StringUtils.hasText(annotationsProperty)) { annotationsProperty = this.properties.getServiceAnnotations(); } return PropertyParserUtils.getStringPairsToMap(annotationsProperty); } Map getDeploymentLabels(Map kubernetesDeployerProperties) { Map labels = new HashMap<>(); String deploymentLabels = PropertyParserUtils.getDeploymentPropertyValue(kubernetesDeployerProperties, this.propertyPrefix + ".deploymentLabels", ""); // Add deployment labels set at the deployer level. String updatedLabels = StringUtils.hasText(this.properties.getDeploymentLabels()) ? new StringBuilder().append(deploymentLabels).append(StringUtils.hasText(deploymentLabels) ? ",": "") .append(this.properties.getDeploymentLabels()).toString() : deploymentLabels; if (StringUtils.hasText(updatedLabels)) { String[] deploymentLabel = updatedLabels.split(","); for (String label : deploymentLabel) { String[] labelPair = label.split(":"); Assert.isTrue(labelPair.length == 2, String.format("Invalid label format, expected 'labelKey:labelValue', got: '%s'", labelPair)); labels.put(labelPair[0].trim(), labelPair[1].trim()); } } return labels; } RestartPolicy getRestartPolicy(Map kubernetesDeployerProperties) { String restartPolicy = PropertyParserUtils.getDeploymentPropertyValue(kubernetesDeployerProperties, this.propertyPrefix + ".restartPolicy", ""); if (StringUtils.hasText(restartPolicy)) { return RestartPolicy.valueOf(restartPolicy); } return this.properties.getRestartPolicy(); } String getTaskServiceAccountName(Map kubernetesDeployerProperties) { String taskServiceAccountName = PropertyParserUtils.getDeploymentPropertyValue(kubernetesDeployerProperties, this.propertyPrefix + ".taskServiceAccountName", ""); if (StringUtils.hasText(taskServiceAccountName)) { return taskServiceAccountName; } return this.properties.getTaskServiceAccountName(); } /** * Binds the YAML formatted value of a deployment property to a {@link KubernetesDeployerProperties} instance. * * @param kubernetesDeployerProperties the map of Kubernetes deployer properties * @param propertyKey the property key to obtain the value to bind for * @param yamlLabel the label representing the field to bind to * @return a {@link KubernetesDeployerProperties} with the bound property data */ private static KubernetesDeployerProperties bindProperties(Map kubernetesDeployerProperties, String propertyKey, String yamlLabel) { String deploymentPropertyValue = PropertyParserUtils.getDeploymentPropertyValue(kubernetesDeployerProperties, propertyKey); KubernetesDeployerProperties deployerProperties = new KubernetesDeployerProperties(); if (StringUtils.hasText(deploymentPropertyValue)) { try { YamlPropertiesFactoryBean properties = new YamlPropertiesFactoryBean(); String tmpYaml = "{ " + yamlLabel + ": " + deploymentPropertyValue + " }"; properties.setResources(new ByteArrayResource(tmpYaml.getBytes())); Properties yaml = properties.getObject(); MapConfigurationPropertySource source = new MapConfigurationPropertySource(yaml); deployerProperties = new Binder(source) .bind("", Bindable.of(KubernetesDeployerProperties.class)).get(); } catch (Exception e) { throw new IllegalArgumentException( String.format("Invalid binding property '%s'", deploymentPropertyValue), e); } } return deployerProperties; } String getStatefulSetInitContainerImageName(Map kubernetesDeployerProperties) { String statefulSetInitContainerImageName = PropertyParserUtils.getDeploymentPropertyValue(kubernetesDeployerProperties, this.propertyPrefix + ".statefulSetInitContainerImageName", ""); if (StringUtils.hasText(statefulSetInitContainerImageName)) { return statefulSetInitContainerImageName; } statefulSetInitContainerImageName = this.properties.getStatefulSetInitContainerImageName(); if (StringUtils.hasText(statefulSetInitContainerImageName)) { return statefulSetInitContainerImageName; } return STATEFUL_SET_IMAGE_NAME; } Map getJobAnnotations(Map kubernetesDeployerProperties) { String annotationsProperty = PropertyParserUtils.getDeploymentPropertyValue(kubernetesDeployerProperties, this.propertyPrefix + ".jobAnnotations", ""); if (!StringUtils.hasText(annotationsProperty)) { annotationsProperty = this.properties.getJobAnnotations(); } return PropertyParserUtils.getStringPairsToMap(annotationsProperty); } /** * Volume mount deployment properties are specified in YAML format: *

* * spring.cloud.deployer.kubernetes.volumeMounts=[{name: 'testhostpath', mountPath: '/test/hostPath'}, * {name: 'testpvc', mountPath: '/test/pvc'}, {name: 'testnfs', mountPath: '/test/nfs'}] * *

* Volume mounts can be specified as deployer properties as well as app deployment properties. * Deployment properties override deployer properties. * * @param deploymentProperties the deployment properties from {@link AppDeploymentRequest} * @return the configured volume mounts */ List getVolumeMounts(Map deploymentProperties) { return this.getVolumeMounts(PropertyParserUtils.getDeploymentPropertyValue(deploymentProperties, this.propertyPrefix + ".volumeMounts")); } /** * Init Containers volume mount properties are specified in YAML format: *

* * spring.cloud.deployer.kubernetes.initContainer.volumeMounts=[{name: 'testhostpath', mountPath: '/test/hostPath'}, * {name: 'testpvc', mountPath: '/test/pvc'}, {name: 'testnfs', mountPath: '/test/nfs'}] * *

* They can be specified as deployer properties as well as app deployment properties. * The later overrides deployer properties. * * @param deploymentProperties the deployment properties from {@link AppDeploymentRequest} * @return the configured volume mounts */ private List getInitContainerVolumeMounts(Map deploymentProperties, String propertyKey) { return this.getVolumeMounts(PropertyParserUtils.getDeploymentPropertyValue( deploymentProperties, propertyKey + ".volumeMounts") ); } private List getVolumeMounts(String propertyValue) { List volumeMounts = new ArrayList<>(); if (StringUtils.hasText(propertyValue)) { try { YamlPropertiesFactoryBean properties = new YamlPropertiesFactoryBean(); String tmpYaml = "{ volume-mounts: " + propertyValue + " }"; properties.setResources(new ByteArrayResource(tmpYaml.getBytes())); Properties yaml = properties.getObject(); MapConfigurationPropertySource source = new MapConfigurationPropertySource(yaml); KubernetesDeployerProperties deployerProperties = new Binder(source) .bind("", Bindable.of(KubernetesDeployerProperties.class)).get(); volumeMounts.addAll(deployerProperties.getVolumeMounts()); } catch (Exception e) { throw new IllegalArgumentException( String.format("Invalid volume mount '%s'", propertyValue), e); } } // only add volume mounts that have not already been added, based on the volume mount's name // i.e. allow provided deployment volume mounts to override deployer defined volume mounts volumeMounts.addAll(this.properties.getVolumeMounts().stream().filter(volumeMount -> volumeMounts.stream() .noneMatch(existingVolumeMount -> existingVolumeMount.getName().equals(volumeMount.getName()))) .collect(Collectors.toList())); return volumeMounts; } /** * The list represents a single command with many arguments. * * @param deploymentProperties the kubernetes deployer properties map * @return a list of strings that represents the command and any arguments for that command */ List getContainerCommand(Map deploymentProperties) { String containerCommand = PropertyParserUtils.getDeploymentPropertyValue(deploymentProperties, this.propertyPrefix + ".containerCommand", ""); return new CommandLineTokenizer(containerCommand).getArgs(); } /** * Parse Lifecycle hooks. * @param deploymentProperties the kubernetes deployer properties map * @return Lifecycle spec */ KubernetesDeployerProperties.Lifecycle getLifeCycle(Map deploymentProperties) { KubernetesDeployerProperties.Lifecycle lifecycle = properties.getLifecycle(); if (deploymentProperties.keySet().stream() .noneMatch(s -> s.startsWith(propertyPrefix + ".lifecycle"))) { return lifecycle; } String postStart = PropertyParserUtils.getDeploymentPropertyValue(deploymentProperties, this.propertyPrefix + ".lifecycle.postStart.exec.command"); if (StringUtils.hasText(postStart)) { lifecycle.setPostStart(lifecycleHook(postStart)); } String preStop = PropertyParserUtils.getDeploymentPropertyValue(deploymentProperties, this.propertyPrefix + ".lifecycle.preStop.exec.command"); if (StringUtils.hasText(preStop)) { lifecycle.setPreStop(lifecycleHook(preStop)); } return lifecycle; } private KubernetesDeployerProperties.Lifecycle.Hook lifecycleHook(String command) { KubernetesDeployerProperties.Lifecycle.Hook hook = new KubernetesDeployerProperties.Lifecycle.Hook(); KubernetesDeployerProperties.Lifecycle.Exec exec = new KubernetesDeployerProperties.Lifecycle.Exec(); exec.setCommand(Arrays.asList(command.split(","))); hook.setExec(exec); return hook; } /** * Determine the pod-level termination grace period seconds. * @param deploymentProperties the deployer properties * @return the termination grace period seconds to use for the pod's containers or null to use the default */ Long determineTerminationGracePeriodSeconds(Map deploymentProperties) { String gracePeriodStr = PropertyParserUtils.getDeploymentPropertyValue(deploymentProperties, this.propertyPrefix + ".terminationGracePeriodSeconds", null); if (gracePeriodStr != null) { return Long.parseLong(gracePeriodStr); } return this.properties.getTerminationGracePeriodSeconds(); } /** * @param deploymentProperties the kubernetes deployer properties map * @return a list of Integers to add to the container */ List getContainerPorts(Map deploymentProperties) { List containerPortList = new ArrayList<>(); String containerPorts = PropertyParserUtils.getDeploymentPropertyValue(deploymentProperties, this.propertyPrefix + ".containerPorts", null); if (containerPorts != null) { String[] containerPortSplit = containerPorts.split(","); for (String containerPort : containerPortSplit) { logger.trace("Adding container ports from AppDeploymentRequest: " + containerPort); Integer port = Integer.parseInt(containerPort.trim()); containerPortList.add(port); } } return containerPortList; } /** * @param deploymentProperties the kubernetes deployer properties map * @return a List of EnvVar objects for app specific environment settings */ Map getAppEnvironmentVariables(Map deploymentProperties) { Map appEnvVarMap = new HashMap<>(); String appEnvVar = PropertyParserUtils.getDeploymentPropertyValue(deploymentProperties, this.propertyPrefix + ".environmentVariables", null); if (appEnvVar != null) { String[] appEnvVars = new NestedCommaDelimitedVariableParser().parse(appEnvVar); for (String envVar : appEnvVars) { logger.trace("Adding environment variable from AppDeploymentRequest: " + envVar); String[] strings = envVar.split("=", 2); Assert.isTrue(strings.length == 2, "Invalid environment variable declared: " + envVar); appEnvVarMap.put(strings[0], strings[1]); } } return appEnvVarMap; } static class NestedCommaDelimitedVariableParser { static final String REGEX = "(\\w+='.+?'),?"; static final Pattern pattern = Pattern.compile(REGEX); String[] parse(String value) { List vars = new ArrayList<>(); Matcher m = pattern.matcher(value); while (m.find()) { String replacedVar = m.group(1).replaceAll("'",""); if (StringUtils.hasText(replacedVar)) { vars.add(replacedVar); } } String nonQuotedVars = value.replaceAll(pattern.pattern(), ""); if (StringUtils.hasText(nonQuotedVars)) { vars.addAll(Arrays.asList(nonQuotedVars.split(","))); } return vars.toArray(new String[0]); } } EntryPointStyle determineEntryPointStyle(Map deploymentProperties) { EntryPointStyle entryPointStyle = null; String deployerPropertyValue = PropertyParserUtils.getDeploymentPropertyValue(deploymentProperties, this.propertyPrefix + ".entryPointStyle", null); if (deployerPropertyValue != null) { try { entryPointStyle = EntryPointStyle.valueOf(deployerPropertyValue.toLowerCase(Locale.ROOT)); } catch (IllegalArgumentException ignore) { } } if (entryPointStyle == null) { entryPointStyle = this.properties.getEntryPointStyle(); } return entryPointStyle; } ProbeType determineProbeType(Map deploymentProperties) { ProbeType probeType = this.properties.getProbeType(); String deployerPropertyValue = PropertyParserUtils.getDeploymentPropertyValue(deploymentProperties, this.propertyPrefix + ".probeType", null); if (StringUtils.hasText(deployerPropertyValue)) { probeType = ProbeType.valueOf(deployerPropertyValue.toUpperCase(Locale.ROOT)); } return probeType; } List getConfigMapKeyRefs(Map deploymentProperties) { List configMapKeyRefs = new ArrayList<>(); KubernetesDeployerProperties deployerProperties = bindProperties(deploymentProperties, this.propertyPrefix + ".configMapKeyRefs", "configMapKeyRefs"); deployerProperties.getConfigMapKeyRefs().forEach(configMapKeyRef -> configMapKeyRefs.add(buildConfigMapKeyRefEnvVar(configMapKeyRef))); properties.getConfigMapKeyRefs().stream() .filter(configMapKeyRef -> configMapKeyRefs.stream() .noneMatch(existing -> existing.getName().equals(configMapKeyRef.getEnvVarName()))) .collect(Collectors.toList()) .forEach(configMapKeyRef -> configMapKeyRefs.add(buildConfigMapKeyRefEnvVar(configMapKeyRef))); return configMapKeyRefs; } private EnvVar buildConfigMapKeyRefEnvVar(ConfigMapKeyRef configMapKeyRef) { ConfigMapKeySelector configMapKeySelector = new ConfigMapKeySelector(); EnvVarSource envVarSource = new EnvVarSource(); envVarSource.setConfigMapKeyRef(configMapKeySelector); EnvVar configMapKeyEnvRefVar = new EnvVar(); configMapKeyEnvRefVar.setValueFrom(envVarSource); configMapKeySelector.setName(configMapKeyRef.getConfigMapName()); configMapKeySelector.setKey(configMapKeyRef.getDataKey()); configMapKeyEnvRefVar.setName(configMapKeyRef.getEnvVarName()); return configMapKeyEnvRefVar; } List getSecretKeyRefs(Map deploymentProperties) { List secretKeyRefs = new ArrayList<>(); KubernetesDeployerProperties deployerProperties = bindProperties(deploymentProperties, this.propertyPrefix + ".secretKeyRefs", "secretKeyRefs" ); deployerProperties.getSecretKeyRefs().forEach(secretKeyRef -> secretKeyRefs.add(buildSecretKeyRefEnvVar(secretKeyRef))); properties.getSecretKeyRefs().stream() .filter(secretKeyRef -> secretKeyRefs.stream() .noneMatch(existing -> existing.getName().equals(secretKeyRef.getEnvVarName()))) .collect(Collectors.toList()) .forEach(secretKeyRef -> secretKeyRefs.add(buildSecretKeyRefEnvVar(secretKeyRef))); return secretKeyRefs; } private EnvVar buildSecretKeyRefEnvVar(SecretKeyRef secretKeyRef) { SecretKeySelector secretKeySelector = new SecretKeySelector(); EnvVarSource envVarSource = new EnvVarSource(); envVarSource.setSecretKeyRef(secretKeySelector); EnvVar secretKeyEnvRefVar = new EnvVar(); secretKeyEnvRefVar.setValueFrom(envVarSource); secretKeySelector.setName(secretKeyRef.getSecretName()); secretKeySelector.setKey(secretKeyRef.getDataKey()); secretKeyEnvRefVar.setName(secretKeyRef.getEnvVarName()); return secretKeyEnvRefVar; } List getConfigMapRefs(Map deploymentProperties) { List configMapRefs = new ArrayList<>(); KubernetesDeployerProperties deployerProperties = bindProperties(deploymentProperties, this.propertyPrefix + ".configMapRefs", "configMapRefs"); deployerProperties.getConfigMapRefs().forEach(configMapRef -> configMapRefs.add(buildConfigMapRefEnvVar(configMapRef))); if (deployerProperties.getConfigMapRefs().isEmpty()) { properties.getConfigMapRefs().stream() .filter(configMapRef -> configMapRefs.stream() .noneMatch(existing -> existing.getConfigMapRef().getName().equals(configMapRef))) .collect(Collectors.toList()) .forEach(configMapRef -> configMapRefs.add(buildConfigMapRefEnvVar(configMapRef))); } return configMapRefs; } private EnvFromSource buildConfigMapRefEnvVar(String configMapRefName) { ConfigMapEnvSource configMapEnvSource = new ConfigMapEnvSource(); configMapEnvSource.setName(configMapRefName); EnvFromSource envFromSource = new EnvFromSource(); envFromSource.setConfigMapRef(configMapEnvSource); return envFromSource; } List getSecretRefs(Map deploymentProperties) { List secretRefs = new ArrayList<>(); KubernetesDeployerProperties deployerProperties = bindProperties(deploymentProperties, this.propertyPrefix + ".secretRefs", "secretRefs"); deployerProperties.getSecretRefs().forEach(secretRef -> secretRefs.add(buildSecretRefEnvVar(secretRef))); if (deployerProperties.getSecretRefs().isEmpty()) { properties.getSecretRefs().stream() .filter(secretRef -> secretRefs.stream() .noneMatch(existing -> existing.getSecretRef().getName().equals(secretRef))) .collect(Collectors.toList()) .forEach(secretRef -> secretRefs.add(buildSecretRefEnvVar(secretRef))); } return secretRefs; } private EnvFromSource buildSecretRefEnvVar(String secretRefName) { SecretEnvSource secretEnvSource = new SecretEnvSource(); secretEnvSource.setName(secretRefName); EnvFromSource envFromSource = new EnvFromSource(); envFromSource.setSecretRef(secretEnvSource); return envFromSource; } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/EntryPointStyle.java ================================================ /* * Copyright 2018-2019 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.cloud.deployer.spi.kubernetes; import java.util.HashMap; import java.util.Map; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; /** * Defines container entry point styles that are available. The selected entry point style * will determine how application properties are made available to the container. * * @author Chris Schaefer */ public enum EntryPointStyle { /** * Application properties will be provided to the container as command line arguments. */ exec, /** * Application properties will be provided to the container as environment variables. */ shell, /** * Application properties will be provided to the container as JSON in the * SPRING_APPLICATION_JSON environment variable. Command line arguments will be passed * as-is. */ boot; /** * Converts the string of the provided entry point style to the appropriate enum value. * Defaults to {@link EntryPointStyle#exec} if no matching * entry point style is found. * * @param entryPointStyle the entry point style to use * @return the converted {@link EntryPointStyle} */ public static EntryPointStyle relaxedValueOf(String entryPointStyle) { // 'value' is just a dummy key as you can't bind a single value to an enum Map props = new HashMap<>(); props.put("value", entryPointStyle); MapConfigurationPropertySource source = new MapConfigurationPropertySource(props); Binder binder = new Binder(source); try { return binder.bind("value", Bindable.of(EntryPointStyle.class)).get(); } catch (Exception e) { // error means we couldn't bind, caller seem to handle null } return exec; } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/HttpProbeCreator.java ================================================ /* * Copyright 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.cloud.deployer.spi.kubernetes; import java.util.ArrayList; import java.util.List; import io.fabric8.kubernetes.api.model.HTTPGetActionBuilder; import io.fabric8.kubernetes.api.model.HTTPHeader; import io.fabric8.kubernetes.api.model.Probe; import io.fabric8.kubernetes.api.model.ProbeBuilder; import io.fabric8.kubernetes.api.model.Secret; import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** * Base class for HTTP based probe creators * * @author Chris Schaefer * @author Corneil du Plessis * @since 2.5 */ public abstract class HttpProbeCreator extends ProbeCreator { private static final int BOOT_1_MAJOR_VERSION = 1; static final String AUTHORIZATION_HEADER_NAME = "Authorization"; static final String PROBE_CREDENTIALS_SECRET_KEY_NAME = "credentials"; static final String BOOT_1_READINESS_PROBE_PATH = "/info"; static final String BOOT_1_LIVENESS_PROBE_PATH = "/health"; static final String BOOT_2_READINESS_PROBE_PATH = "/actuator" + BOOT_1_READINESS_PROBE_PATH; static final String BOOT_2_LIVENESS_PROBE_PATH = "/actuator" + BOOT_1_LIVENESS_PROBE_PATH; static final String DEFAULT_PROBE_SCHEME = "HTTP"; HttpProbeCreator(KubernetesDeployerProperties kubernetesDeployerProperties, ContainerConfiguration containerConfiguration) { super(kubernetesDeployerProperties, containerConfiguration); } protected abstract String getProbePath(); protected abstract Integer getPort(); protected abstract String getScheme(); protected abstract int getTimeout(); protected Probe create() { HTTPGetActionBuilder httpGetActionBuilder = new HTTPGetActionBuilder() .withPath(getProbePath()) .withNewPort(getPort()) .withScheme(getScheme()); List httpHeaders = getHttpHeaders(); if (!httpHeaders.isEmpty()) { httpGetActionBuilder.withHttpHeaders(httpHeaders); } return new ProbeBuilder() .withHttpGet(httpGetActionBuilder.build()) .withTimeoutSeconds(getTimeout()) .withInitialDelaySeconds(getInitialDelay()) .withPeriodSeconds(getPeriod()) .withFailureThreshold(getFailure()) .withSuccessThreshold(getSuccess()) .build(); } private List getHttpHeaders() { List httpHeaders = new ArrayList<>(); HTTPHeader authenticationHeader = getAuthorizationHeader(); if (authenticationHeader != null) { httpHeaders.add(authenticationHeader); } return httpHeaders; } private HTTPHeader getAuthorizationHeader() { HTTPHeader httpHeader = null; Secret probeCredentialsSecret = getContainerConfiguration().getProbeCredentialsSecret(); if (probeCredentialsSecret != null) { Assert.isTrue(probeCredentialsSecret.getData().containsKey(PROBE_CREDENTIALS_SECRET_KEY_NAME), "Secret does not contain a key by the name of " + PROBE_CREDENTIALS_SECRET_KEY_NAME); httpHeader = new HTTPHeader(); httpHeader.setName(AUTHORIZATION_HEADER_NAME); // only Basic auth is supported currently httpHeader.setValue(ProbeAuthenticationType.Basic.name() + " " + probeCredentialsSecret.getData().get(PROBE_CREDENTIALS_SECRET_KEY_NAME)); } return httpHeader; } Integer getDefaultPort() { return getContainerConfiguration().getExternalPort(); } boolean useBoot1ProbePath() { String bootMajorVersionProperty = KUBERNETES_DEPLOYER_PREFIX + ".bootMajorVersion"; String bootMajorVersionValue = getDeploymentPropertyValue(bootMajorVersionProperty); if (StringUtils.hasText(bootMajorVersionValue)) { return Integer.parseInt(bootMajorVersionValue) == BOOT_1_MAJOR_VERSION; } return false; } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/ImagePullPolicy.java ================================================ /* * Copyright 2015-2016 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.cloud.deployer.spi.kubernetes; import java.util.HashMap; import java.util.Map; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; /** * ImagePullPolicy for containers inside a Kubernetes Pod, cf. https://kubernetes.io/docs/user-guide/images/ * * @author Moritz Schulze */ public enum ImagePullPolicy { Always, IfNotPresent, Never; /** * Tries to convert {@code name} to an {@link ImagePullPolicy} by ignoring case, dashes, underscores * and so on in a relaxed fashion. * * @param name The name to convert to an {@link ImagePullPolicy}. * @return The {@link ImagePullPolicy} for {@code name} or {@code null} if the conversion was not possible. */ public static ImagePullPolicy relaxedValueOf(String name) { // 'value' is just a dummy key as you can't bind a single value to an enum Map props = new HashMap<>(); props.put("value", name); MapConfigurationPropertySource source = new MapConfigurationPropertySource(props); Binder binder = new Binder(source); try { return binder.bind("value", Bindable.of(ImagePullPolicy.class)).get(); } catch (Exception e) { // error means we couldn't bind, caller seem to handle null } return null; } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/KubernetesActuatorTemplate.java ================================================ /* * Copyright 2022 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.cloud.deployer.spi.kubernetes; import org.springframework.cloud.deployer.spi.app.AbstractActuatorTemplate; import org.springframework.cloud.deployer.spi.app.AppAdmin; import org.springframework.cloud.deployer.spi.app.AppDeployer; import org.springframework.cloud.deployer.spi.app.AppInstanceStatus; import org.springframework.web.client.RestTemplate; /** * @author David Turanski */ public class KubernetesActuatorTemplate extends AbstractActuatorTemplate { public KubernetesActuatorTemplate(RestTemplate restTemplate, AppDeployer appDeployer, AppAdmin appAdmin) { super(restTemplate, appDeployer, appAdmin); } protected String actuatorUrlForInstance(AppInstanceStatus appInstanceStatus) { return String.format("http://%s:%d/%s", appInstanceStatus.getAttributes().get("pod.ip"), Integer.valueOf(appInstanceStatus.getAttributes().get("actuator.port")), appInstanceStatus.getAttributes().get("actuator.path")); } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/KubernetesAppDeployer.java ================================================ /* * Copyright 2015-2023 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.cloud.deployer.spi.kubernetes; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.function.Function; import io.fabric8.kubernetes.api.model.Container; import io.fabric8.kubernetes.api.model.ContainerBuilder; import io.fabric8.kubernetes.api.model.PersistentVolumeClaim; import io.fabric8.kubernetes.api.model.PersistentVolumeClaimBuilder; import io.fabric8.kubernetes.api.model.PersistentVolumeClaimList; import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.api.model.PodList; import io.fabric8.kubernetes.api.model.PodSpec; import io.fabric8.kubernetes.api.model.Quantity; import io.fabric8.kubernetes.api.model.SecurityContext; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.ServiceBuilder; import io.fabric8.kubernetes.api.model.ServiceList; import io.fabric8.kubernetes.api.model.ServicePort; import io.fabric8.kubernetes.api.model.ServiceSpecBuilder; import io.fabric8.kubernetes.api.model.StatusDetails; import io.fabric8.kubernetes.api.model.VolumeBuilder; import io.fabric8.kubernetes.api.model.VolumeMountBuilder; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; import io.fabric8.kubernetes.api.model.apps.DeploymentList; import io.fabric8.kubernetes.api.model.apps.StatefulSet; import io.fabric8.kubernetes.api.model.apps.StatefulSetBuilder; import io.fabric8.kubernetes.api.model.apps.StatefulSetList; import io.fabric8.kubernetes.api.model.apps.StatefulSetSpec; import io.fabric8.kubernetes.api.model.apps.StatefulSetSpecBuilder; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable; import io.fabric8.kubernetes.client.dsl.NonDeletingOperation; import io.fabric8.kubernetes.client.dsl.PodResource; import io.fabric8.kubernetes.client.dsl.Resource; import io.fabric8.kubernetes.client.dsl.RollableScalableResource; import io.fabric8.kubernetes.client.dsl.ScalableResource; import io.fabric8.kubernetes.client.dsl.ServiceResource; import io.fabric8.kubernetes.client.dsl.TimeoutableScalable; import io.fabric8.kubernetes.client.dsl.internal.apps.v1.DeploymentOperationsImpl; import io.fabric8.kubernetes.client.dsl.internal.apps.v1.StatefulSetOperationsImpl; import io.fabric8.kubernetes.client.dsl.internal.batch.v1.JobOperationsImpl; import io.fabric8.kubernetes.client.dsl.internal.core.v1.ServiceOperationsImpl; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.cloud.deployer.spi.app.AppDeployer; import org.springframework.cloud.deployer.spi.app.AppInstanceStatus; import org.springframework.cloud.deployer.spi.app.AppScaleRequest; import org.springframework.cloud.deployer.spi.app.AppStatus; import org.springframework.cloud.deployer.spi.app.DeploymentState; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; import org.springframework.cloud.deployer.spi.kubernetes.support.ArgumentSanitizer; import org.springframework.cloud.deployer.spi.kubernetes.support.PropertyParserUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; /** * A deployer that targets Kubernetes. * * @author Florian Rosenberg * @author Thomas Risberg * @author Mark Fisher * @author Donovan Muller * @author David Turanski * @author Ilayaperumal Gopinathan * @author Chris Schaefer * @author Christian Tzolov * @author Omar Gonzalez * @author Chris Bono * @author Corneil du Plessis */ public class KubernetesAppDeployer extends AbstractKubernetesDeployer implements AppDeployer { protected final Log logger = LogFactory.getLog(getClass().getName()); public KubernetesAppDeployer(KubernetesDeployerProperties properties, KubernetesClient client) { this(properties, client, new DefaultContainerFactory(properties)); } public KubernetesAppDeployer(KubernetesDeployerProperties properties, KubernetesClient client, ContainerFactory containerFactory) { this.properties = properties; this.client = client; this.containerFactory = containerFactory; this.deploymentPropertiesResolver = new DeploymentPropertiesResolver( KubernetesDeployerProperties.KUBERNETES_DEPLOYER_PROPERTIES_PREFIX, properties); } @Override public String deploy(AppDeploymentRequest request) { String appId = createDeploymentId(request); if (logger.isDebugEnabled()) { ArgumentSanitizer sanitizer = new ArgumentSanitizer(); Map sanitized = sanitizer.sanitizeProperties(request.getDeploymentProperties()); List sanitizedCommandlineArguments = sanitizer.sanitizeArguments(request.getCommandlineArguments()); logger.debug(String.format("Deploying app: %s, request: commandlineArguments=%s, deploymentProperties=%s, definition=%s, resource=%s", appId, sanitizedCommandlineArguments, sanitized, request.getDefinition(), request.getResource())); } try { AppStatus status = status(appId); if (!status.getState().equals(DeploymentState.unknown)) { throw new IllegalStateException(String.format("App '%s' is already deployed", appId)); } String indexedProperty = request.getDeploymentProperties().get(INDEXED_PROPERTY_KEY); boolean indexed = Boolean.parseBoolean(indexedProperty); logPossibleDownloadResourceMessage(request.getResource()); createService(request); if (indexed) { createStatefulSet(request); } else { createDeployment(request); } return appId; } catch (RuntimeException e) { logger.error(e.getMessage(), e); throw e; } } @Override public void undeploy(String appId) { if (logger.isDebugEnabled()) { logger.debug(String.format("Undeploying app: %s", appId)); } AppStatus status = status(appId); if (status.getState().equals(DeploymentState.unknown)) { // ensure objects for this appId are deleted in the event a previous deployment failed. // allows for log inspection prior to making an undeploy request. deleteAllObjects(appId); throw new IllegalStateException(String.format("App '%s' is not deployed", appId)); } try { deleteAllObjects(appId); } catch (RuntimeException e) { logger.error(e.getMessage(), e); throw e; } } @Override public AppStatus status(String appId) { Map selector = new HashMap<>(); ServiceList services = client.services().withLabel(SPRING_APP_KEY, appId).list(); selector.put(SPRING_APP_KEY, appId); PodList podList = client.pods().withLabels(selector).list(); if (logger.isDebugEnabled()) { logger.debug(String.format("Building AppStatus for app: %s", appId)); if (podList != null && podList.getItems() != null) { logger.debug(String.format("Pods for appId %s: %d", appId, podList.getItems().size())); for (Pod pod : podList.getItems()) { logger.debug(String.format("Pod: %s", pod.getMetadata().getName())); } } } AppStatus status = buildAppStatus(appId, podList, services); if (logger.isDebugEnabled()) { logger.debug(String.format("status:%s = %s", appId, status.getDeploymentId())); for (AppInstanceStatus instanceStatus : status.getInstances().values()) { logger.debug(String.format("status:%s:%s:%s", instanceStatus.getId(), instanceStatus.getState(), instanceStatus.getAttributes())); } } return status; } @Override public String getLog(String appId) { Map selector = new HashMap<>(); selector.put(SPRING_APP_KEY, appId); PodList podList = client.pods().withLabels(selector).list(); StringBuilder logAppender = new StringBuilder(); for (Pod pod : podList.getItems()) { if (pod.getSpec().getContainers().size() > 1) { for (Container container : pod.getSpec().getContainers()) { if (container.getEnv().stream().anyMatch(envVar -> "SPRING_CLOUD_APPLICATION_GUID".equals(envVar.getName()))) { //find log for this container logAppender.append(this.client.pods() .withName(pod.getMetadata().getName()) .inContainer(container.getName()) .tailingLines(500).getLog()); break; } } } else { logAppender.append(this.client.pods().withName(pod.getMetadata().getName()).tailingLines(500).getLog()); } } return logAppender.toString(); } @Override public void scale(AppScaleRequest appScaleRequest) { String deploymentId = appScaleRequest.getDeploymentId(); if (logger.isDebugEnabled()) { logger.debug(String.format("Scale app: %s to: %s", deploymentId, appScaleRequest.getCount())); } try { ScalableResource scalableResource = this.client.apps().deployments().withName(deploymentId); if (scalableResource.get() == null) { scalableResource = this.client.apps().statefulSets().withName(deploymentId); } if (scalableResource.get() == null) { throw new IllegalStateException(String.format("App '%s' is not deployed", deploymentId)); } JobOperationsImpl jobOperations = new JobOperationsImpl(this.client); TimeoutableScalable timeoutableScalable = scalableResource.withTimeoutInMillis(jobOperations.getRequestConfig().getScaleTimeout()); timeoutableScalable.scale(appScaleRequest.getCount()); } catch (KubernetesClientException x) { logger.debug("scale:exception:" + x, x); throw new IllegalStateException(x); } } @Override public RuntimeEnvironmentInfo environmentInfo() { return super.createRuntimeEnvironmentInfo(AppDeployer.class, this.getClass()); } private Deployment createDeployment(AppDeploymentRequest request) { String appId = createDeploymentId(request); if (logger.isDebugEnabled()) { logger.debug(String.format("Creating Deployment: %s", appId)); } int replicas = getCountFromRequest(request); Map idMap = createIdMap(appId, request); Map kubernetesDeployerProperties = request.getDeploymentProperties(); Map annotations = this.deploymentPropertiesResolver.getPodAnnotations(kubernetesDeployerProperties); Map deploymentLabels = this.deploymentPropertiesResolver.getDeploymentLabels(kubernetesDeployerProperties); PodSpec podSpec = createPodSpec(request); Deployment d = new DeploymentBuilder().withNewMetadata().withName(appId).withLabels(idMap) .addToLabels(SPRING_MARKER_KEY, SPRING_MARKER_VALUE).addToLabels(deploymentLabels).endMetadata() .withNewSpec().withNewSelector().addToMatchLabels(idMap).endSelector().withReplicas(replicas) .withNewTemplate().withNewMetadata().withLabels(idMap).addToLabels(SPRING_MARKER_KEY, SPRING_MARKER_VALUE) .addToLabels(deploymentLabels).withAnnotations(annotations).endMetadata().withSpec(podSpec).endTemplate() .endSpec().build(); DeploymentOperationsImpl deploymentOperations = new DeploymentOperationsImpl(this.client); d = client.apps().deployments().inNamespace(deploymentOperations.getNamespace()).resource(d).create(); if (logger.isDebugEnabled()) { logger.debug("created:" + d.getFullResourceName() + ":" + d.getStatus()); } return d; } private int getCountFromRequest(AppDeploymentRequest request) { String countProperty = request.getDeploymentProperties().get(COUNT_PROPERTY_KEY); return (countProperty != null) ? Integer.parseInt(countProperty) : 1; } /** * Create a StatefulSet * * @param request the {@link AppDeploymentRequest} */ protected void createStatefulSet(AppDeploymentRequest request) { String appId = createDeploymentId(request); int externalPort = getExternalPort(request); Map idMap = createIdMap(appId, request); int replicas = getCountFromRequest(request); Map kubernetesDeployerProperties = request.getDeploymentProperties(); if (logger.isDebugEnabled()) { logger.debug(String.format("Creating StatefulSet: %s on %d with %d replicas", appId, externalPort, replicas)); } Map storageResource = Collections.singletonMap("storage", new Quantity(this.deploymentPropertiesResolver.getStatefulSetStorage(kubernetesDeployerProperties))); String storageClassName = this.deploymentPropertiesResolver.getStatefulSetStorageClassName(kubernetesDeployerProperties); String volumeClaimTemplateName = this.deploymentPropertiesResolver.getStatefulSetVolumeClaimTemplateName(kubernetesDeployerProperties); volumeClaimTemplateName = StringUtils.hasText(volumeClaimTemplateName) ? volumeClaimTemplateName : appId; PersistentVolumeClaimBuilder persistentVolumeClaimBuilder = new PersistentVolumeClaimBuilder().withNewSpec(). withStorageClassName(storageClassName).withAccessModes(Collections.singletonList("ReadWriteOnce")) .withNewResources().addToLimits(storageResource).addToRequests(storageResource).endResources() .endSpec().withNewMetadata().withName(volumeClaimTemplateName).withLabels(idMap) .addToLabels(SPRING_MARKER_KEY, SPRING_MARKER_VALUE).endMetadata(); PodSpec podSpec = createPodSpec(request); podSpec.getVolumes().add(new VolumeBuilder().withName("config").withNewEmptyDir().endEmptyDir().build()); podSpec.getContainers().get(0).getVolumeMounts() .add(new VolumeMountBuilder().withName("config").withMountPath("/config").build()); String statefulSetInitContainerImageName = this.deploymentPropertiesResolver.getStatefulSetInitContainerImageName(kubernetesDeployerProperties); podSpec.getInitContainers().add(createStatefulSetInitContainer(podSpec, statefulSetInitContainerImageName)); Map deploymentLabels = this.deploymentPropertiesResolver.getDeploymentLabels(request.getDeploymentProperties()); Map annotations = this.deploymentPropertiesResolver.getPodAnnotations(kubernetesDeployerProperties); StatefulSetSpec spec = new StatefulSetSpecBuilder().withNewSelector().addToMatchLabels(idMap) .addToMatchLabels(SPRING_MARKER_KEY, SPRING_MARKER_VALUE).endSelector() .withVolumeClaimTemplates(persistentVolumeClaimBuilder.build()).withServiceName(appId) .withPodManagementPolicy("Parallel").withReplicas(replicas).withNewTemplate().withNewMetadata() .withLabels(idMap).addToLabels(SPRING_MARKER_KEY, SPRING_MARKER_VALUE).addToLabels(deploymentLabels) .addToAnnotations(annotations).endMetadata().withSpec(podSpec).endTemplate().build(); StatefulSet statefulSet = new StatefulSetBuilder().withNewMetadata().withName(appId).withLabels(idMap) .addToLabels(SPRING_MARKER_KEY, SPRING_MARKER_VALUE).addToLabels(deploymentLabels).endMetadata().withSpec(spec).build(); StatefulSetOperationsImpl statefulSetOperations = new StatefulSetOperationsImpl(this.client); statefulSet = client.apps().statefulSets().inNamespace(statefulSetOperations.getNamespace()).resource(statefulSet).create(); if (logger.isDebugEnabled()) { logger.debug("created:" + statefulSet.getFullResourceName() + ":" + statefulSet.getStatus()); } } protected void createService(AppDeploymentRequest request) { String appId = createDeploymentId(request); int externalPort = getExternalPort(request); if (logger.isDebugEnabled()) { ArgumentSanitizer sanitizer = new ArgumentSanitizer(); Map sanitized = sanitizer.sanitizeProperties(request.getDeploymentProperties()); logger.debug(String.format("Creating Service: %s on %d using %s", appId, externalPort, sanitized)); } Map idMap = createIdMap(appId, request); ServiceSpecBuilder spec = new ServiceSpecBuilder(); boolean isCreateLoadBalancer = false; String createLoadBalancer = PropertyParserUtils.getDeploymentPropertyValue(request.getDeploymentProperties(), "spring.cloud.deployer.kubernetes.createLoadBalancer"); String createNodePort = PropertyParserUtils.getDeploymentPropertyValue(request.getDeploymentProperties(), "spring.cloud.deployer.kubernetes.createNodePort"); String additionalServicePorts = PropertyParserUtils.getDeploymentPropertyValue(request.getDeploymentProperties(), "spring.cloud.deployer.kubernetes.servicePorts"); if (createLoadBalancer != null && createNodePort != null) { throw new IllegalArgumentException("Cannot create NodePort and LoadBalancer at the same time."); } if (createLoadBalancer == null) { isCreateLoadBalancer = properties.isCreateLoadBalancer(); } else { if ("true".equalsIgnoreCase(createLoadBalancer)) { isCreateLoadBalancer = true; } } if (isCreateLoadBalancer) { spec.withType("LoadBalancer"); } ServicePort servicePort = new ServicePort(); servicePort.setPort(externalPort); servicePort.setName("port-" + externalPort); if (createNodePort != null) { spec.withType("NodePort"); if (!"true".equalsIgnoreCase(createNodePort)) { try { Integer nodePort = Integer.valueOf(createNodePort); servicePort.setNodePort(nodePort); } catch (NumberFormatException e) { throw new IllegalArgumentException( String.format("Invalid value: %s: provided port is not valid.", createNodePort)); } } } Set servicePorts = new HashSet<>(); servicePorts.add(servicePort); if (StringUtils.hasText(additionalServicePorts)) { servicePorts.addAll(addAdditionalServicePorts(additionalServicePorts)); } spec.addAllToPorts(servicePorts); Map annotations = this.deploymentPropertiesResolver.getServiceAnnotations(request.getDeploymentProperties()); String serviceName = getServiceName(request, appId); // if called from skipper, use unique selectors to maintain connectivity // between service and pods that are being brought up/down if (request.getDeploymentProperties().containsKey(APP_NAME_PROPERTY_KEY)) { spec.withSelector(Collections.singletonMap(APP_NAME_KEY, request.getDeploymentProperties().get(APP_NAME_PROPERTY_KEY))); String groupId = request.getDeploymentProperties().get(AppDeployer.GROUP_PROPERTY_KEY); if (groupId != null) { spec.addToSelector(SPRING_GROUP_KEY, groupId); } } else { spec.withSelector(idMap); } Service service = new ServiceBuilder().withNewMetadata().withName(serviceName) .withLabels(idMap).withAnnotations(annotations).addToLabels(SPRING_MARKER_KEY, SPRING_MARKER_VALUE) .endMetadata().withSpec(spec.build()).build(); ServiceOperationsImpl serviceOperations = new ServiceOperationsImpl(client); service = client.services().inNamespace(serviceOperations.getNamespace()).resource(service).createOr( new Function, Service>() { @Override public Service apply(NonDeletingOperation serviceNonDeletingOperation) { return serviceNonDeletingOperation.update(); } }); if (logger.isDebugEnabled()) { logger.debug("created:" + service.getFullResourceName() + ":" + service.getStatus()); } } // logic to support using un-versioned service names when called from skipper private String getServiceName(AppDeploymentRequest request, String appId) { String appName = request.getDeploymentProperties().get(APP_NAME_PROPERTY_KEY); // if we have an un-versioned app name from skipper if (StringUtils.hasText(appName)) { String serviceName = formatServiceName(request, appName); // need to check if a versioned service exists to maintain backwards compat.. // version number itself isn't checked on as it could be different if create or upgrade // which we don't know at runtime.... List services = client.services().withLabel(SPRING_DEPLOYMENT_KEY).list().getItems(); for (Service service : services) { String serviceMetadataName = service.getMetadata().getName(); if (serviceMetadataName.startsWith(serviceName + "-v")) { return appId; } } return serviceName; } // no un-versioned app name provided, maybe not called from skipper, return whatever is in appId return appId; } private String formatServiceName(AppDeploymentRequest request, String appName) { String groupId = request.getDeploymentProperties().get(AppDeployer.GROUP_PROPERTY_KEY); String serviceName = groupId == null ? String.format("%s", appName) : String.format("%s-%s", groupId, appName); return serviceName.replace('.', '-').toLowerCase(Locale.ROOT); } private Set addAdditionalServicePorts(String additionalServicePorts) { Set ports = new HashSet<>(); String[] servicePorts = additionalServicePorts.split(","); for (String servicePort : servicePorts) { Integer port = Integer.parseInt(servicePort.trim()); ServicePort extraServicePort = new ServicePort(); extraServicePort.setPort(port); extraServicePort.setName("port-" + port); ports.add(extraServicePort); } return ports; } /** * For StatefulSets, create an init container to parse ${HOSTNAME} to get the `instance.index` and write it to * config/application.properties on a shared volume so the main container has it. Using the legacy annotation * configuration since the current client version does not directly support InitContainers. *

* Since 1.8 the annotation method has been removed, and the initContainer API is supported since 1.6 * * @param podSpec the current pod spec the container is being added to * @param imageName the image name to use in the init container * @return a container definition with the aforementioned configuration */ private Container createStatefulSetInitContainer(PodSpec podSpec, String imageName) { List command = new LinkedList<>(); String commandString = String .format("%s && %s", setIndexProperty("INSTANCE_INDEX"), setIndexProperty("spring.cloud.stream.instanceIndex")); command.add("sh"); command.add("-c"); command.add(commandString); ContainerBuilder containerBuilder = new ContainerBuilder().withName("index-provider") .withImage(imageName) .withImagePullPolicy("IfNotPresent") .withCommand(command) .withVolumeMounts(new VolumeMountBuilder().withName("config").withMountPath("/config").build()); if (!CollectionUtils.isEmpty(podSpec.getContainers())) { SecurityContext securityContext = podSpec.getContainers().get(0).getSecurityContext(); if (securityContext != null) { containerBuilder.withSecurityContext(securityContext); } } return containerBuilder.build(); } private String setIndexProperty(String name) { return String .format("echo %s=\"$(expr $HOSTNAME | grep -o \"[[:digit:]]*$\")\" >> " + "/config/application.properties", name); } private void deleteAllObjects(String appIdToDelete) { Map labels = Collections.singletonMap(SPRING_APP_KEY, appIdToDelete); if (logger.isDebugEnabled()) { logger.debug(String.format("deleteAllObjects:%s:%s", appIdToDelete, labels)); } // waitForLoadBalancerReady(labels); // Not negative effect in not waiting for loadbalancer. deleteService(labels); deleteDeployment(labels); deleteStatefulSet(labels); deletePod(labels); deletePvc(labels); } private void deleteService(Map labels) { FilterWatchListDeletable> servicesToDelete = client.services().withLabels(labels); if (servicesToDelete != null && servicesToDelete.list().getItems() != null) { List servicesDeleted = servicesToDelete.delete(); if (logger.isDebugEnabled()) { logger.debug(String.format("Service deleted for: %s - %s", labels, servicesDeleted)); } } } private void deleteDeployment(Map labels) { FilterWatchListDeletable> deploymentsToDelete = client.apps().deployments().withLabels(labels); if (deploymentsToDelete != null && deploymentsToDelete.list().getItems() != null) { List deploymentsDeleted = deploymentsToDelete.delete(); if (logger.isDebugEnabled()) { logger.debug(String.format("Deployment deleted for: %s - %s", labels, deploymentsDeleted)); } } } private void deleteStatefulSet(Map labels) { FilterWatchListDeletable> ssToDelete = client.apps().statefulSets().withLabels(labels); if (ssToDelete != null && ssToDelete.list().getItems() != null) { List ssDeleted = ssToDelete.delete(); if (logger.isDebugEnabled()) { logger.debug(String.format("StatefulSet deleted for: %s - %s", labels, ssDeleted)); } } } private void deletePod(Map labels) { FilterWatchListDeletable podsToDelete = client.pods() .withLabels(labels); if (podsToDelete != null && podsToDelete.list().getItems() != null) { List podsDeleted = podsToDelete.delete(); if (logger.isDebugEnabled()) { logger.debug(String.format("Pod deleted for: %s - %s", labels, podsDeleted)); } } } private void deletePvc(Map labels) { FilterWatchListDeletable> pvcsToDelete = client.persistentVolumeClaims() .withLabels(labels); if (pvcsToDelete != null && pvcsToDelete.list().getItems() != null) { List pvcsDeleted = pvcsToDelete.delete(); if (logger.isDebugEnabled()) { logger.debug(String.format("PVC deleted for: %s - %s", labels, pvcsDeleted)); } } } private void waitForLoadBalancerReady(Map labels) { List services = client.services().withLabels(labels).list().getItems(); if (!services.isEmpty()) { Service svc = services.get(0); if (svc != null && "LoadBalancer".equals(svc.getSpec().getType())) { int tries = 0; int maxWait = properties.getMinutesToWaitForLoadBalancer() * 6; // we check 6 times per minute while (tries++ < maxWait) { if (svc.getStatus() != null && svc.getStatus().getLoadBalancer() != null && svc.getStatus().getLoadBalancer().getIngress() != null && svc.getStatus() .getLoadBalancer().getIngress().isEmpty()) { if (tries % 6 == 0) { logger.warn("Waiting for LoadBalancer to complete before deleting it ..."); } if (logger.isDebugEnabled()) { logger.debug(String.format("Waiting for LoadBalancer, try %d", tries)); } try { Thread.sleep(10000L); } catch (InterruptedException e) { } svc = client.services().withLabels(labels).list().getItems().get(0); } else { break; } } if (logger.isDebugEnabled()) { logger.debug(String.format("LoadBalancer Ingress: %s", svc.getStatus().getLoadBalancer().getIngress().toString())); } } } } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/KubernetesAppInstanceStatus.java ================================================ /* * Copyright 2015-2022 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.cloud.deployer.spi.kubernetes; import java.nio.file.Paths; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import io.fabric8.kubernetes.api.model.Container; import io.fabric8.kubernetes.api.model.ContainerStatus; import io.fabric8.kubernetes.api.model.IntOrString; import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.ServicePort; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.cloud.deployer.spi.app.AppInstanceStatus; import org.springframework.cloud.deployer.spi.app.DeploymentState; /** * Represents the status of a module. * * @author Florian Rosenberg * @author Thomas Risberg * @author David Turanski * @author Chris Schaefer */ public class KubernetesAppInstanceStatus implements AppInstanceStatus { private static Log logger = LogFactory.getLog(KubernetesAppInstanceStatus.class); private final Pod pod; private final Service service; private final KubernetesDeployerProperties properties; private ContainerStatus containerStatus; private RunningPhaseDeploymentStateResolver runningPhaseDeploymentStateResolver; @Deprecated public KubernetesAppInstanceStatus(Pod pod, Service service, KubernetesDeployerProperties properties) { this.pod = pod; this.service = service; this.properties = properties; // we assume one container per pod if (pod != null && pod.getStatus().getContainerStatuses().size() == 1) { this.containerStatus = pod.getStatus().getContainerStatuses().get(0); } else { this.containerStatus = null; } this.runningPhaseDeploymentStateResolver = new DefaultRunningPhaseDeploymentStateResolver(properties); } public KubernetesAppInstanceStatus(Pod pod, Service service, KubernetesDeployerProperties properties, ContainerStatus containerStatus) { this.pod = pod; this.service = service; this.properties = properties; this.containerStatus = containerStatus; this.runningPhaseDeploymentStateResolver = new DefaultRunningPhaseDeploymentStateResolver(properties); } /** * Override the default {@link RunningPhaseDeploymentStateResolver} implementation. * * @param runningPhaseDeploymentStateResolver the * {@link RunningPhaseDeploymentStateResolver} to use */ public void setRunningPhaseDeploymentStateResolver( RunningPhaseDeploymentStateResolver runningPhaseDeploymentStateResolver) { this.runningPhaseDeploymentStateResolver = runningPhaseDeploymentStateResolver; } @Override public String getId() { return pod == null ? "N/A" : pod.getMetadata().getName(); } @Override public DeploymentState getState() { return pod != null && containerStatus != null ? mapState() : DeploymentState.unknown; } /** * Maps Kubernetes phases/states onto Spring Cloud Deployer states */ private DeploymentState mapState() { logger.debug(String.format("%s - Phase [ %s ]", pod.getMetadata().getName(), pod.getStatus().getPhase())); logger.debug(String.format("%s - ContainerStatus [ %s ]", pod.getMetadata().getName(), containerStatus)); switch (pod.getStatus().getPhase()) { case "Pending": return DeploymentState.deploying; // We only report a module as running if the container is also ready to service requests. // We also implement the Readiness check as part of the container to ensure ready means // that the module is up and running and not only that the JVM has been created and the // Spring module is still starting up case "Running": // we assume we only have one container return runningPhaseDeploymentStateResolver.resolve(containerStatus); case "Failed": return DeploymentState.failed; case "Unknown": return DeploymentState.unknown; default: return DeploymentState.unknown; } } @Override public Map getAttributes() { ConcurrentHashMap result = new ConcurrentHashMap<>(); if (pod != null) { result.put("pod.name", pod.getMetadata().getName()); result.put("pod.startTime", nullSafe(pod.getStatus().getStartTime())); result.put("pod.ip", nullSafe(pod.getStatus().getPodIP())); result.put("actuator.path", determineActuatorPathFromLivenessProbe(pod)); result.put("actuator.port", determineActuatorPortFromLivenessProbe(pod, result.get("actuator.path"))); result.put("host.ip", nullSafe(pod.getStatus().getHostIP())); result.put("phase", nullSafe(pod.getStatus().getPhase())); result.put(AbstractKubernetesDeployer.SPRING_APP_KEY.replace('-', '.'), pod.getMetadata().getLabels().get(AbstractKubernetesDeployer.SPRING_APP_KEY)); result.put(AbstractKubernetesDeployer.SPRING_DEPLOYMENT_KEY.replace('-', '.'), pod.getMetadata().getLabels().get(AbstractKubernetesDeployer.SPRING_DEPLOYMENT_KEY)); result.put("guid", pod.getMetadata().getUid()); } else { logger.debug("getAttributes:no pod"); } if (service != null) { result.put("service.name", service.getMetadata().getName()); if ("LoadBalancer".equals(service.getSpec().getType())) { if (service.getStatus() != null && service.getStatus().getLoadBalancer() != null && service.getStatus().getLoadBalancer().getIngress() != null && !service.getStatus() .getLoadBalancer().getIngress().isEmpty()) { String externalIp = service.getStatus().getLoadBalancer().getIngress().get(0).getIp(); if (externalIp == null) { externalIp = service.getStatus().getLoadBalancer().getIngress().get(0).getHostname(); } result.put("service.external.ip", externalIp); List ports = service.getSpec().getPorts(); int port = 0; if (ports != null && ports.size() > 0) { port = ports.get(0).getPort(); result.put("service.external.port", String.valueOf(port)); } if (externalIp != null) { result.put("url", "http://" + externalIp + (port > 0 && port != 80 ? ":" + port : "")); } } } } else { logger.debug("getAttributes:no service"); } if (containerStatus != null) { result.put("container.restartCount", "" + containerStatus.getRestartCount()); if (containerStatus.getLastState() != null && containerStatus.getLastState().getTerminated() != null) { result.put("container.lastState.terminated.exitCode", "" + containerStatus.getLastState().getTerminated().getExitCode()); result.put("container.lastState.terminated.reason", containerStatus.getLastState().getTerminated().getReason()); } if (containerStatus.getState() != null && containerStatus.getState().getTerminated() != null) { result.put("container.state.terminated.exitCode", "" + containerStatus.getState().getTerminated().getExitCode()); result.put("container.state.terminated.reason", containerStatus.getState().getTerminated().getReason()); } } else { logger.debug("getAttributes:no containerStatus"); } if (logger.isDebugEnabled()) { logger.debug("getAttributes:" + result); } return result; } private String nullSafe(String value) { return value == null ? "" : value; } private String determineActuatorPathFromLivenessProbe(Pod pod) { return pod.getSpec().getContainers().stream() .filter((Container container) -> container.getLivenessProbe() != null && container.getLivenessProbe().getHttpGet() != null) .findFirst() .map(container -> Paths.get(container.getLivenessProbe().getHttpGet().getPath()).getParent().toString()) .orElse("/actuator"); } private String determineActuatorPortFromLivenessProbe(Pod pod, String path) { IntOrString intOrString = pod.getSpec().getContainers().stream() .filter(container -> container.getLivenessProbe() != null && container.getLivenessProbe().getHttpGet() != null && container.getLivenessProbe().getHttpGet().getPath().equals(path)) .findFirst() .map(container -> container.getLivenessProbe().getHttpGet().getPort()) .orElse(new IntOrString(8080)); return intOrString.getIntVal() != null ? String.valueOf(intOrString.getIntVal()) : intOrString.getStrVal(); } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/KubernetesAutoConfiguration.java ================================================ /* * Copyright 2015-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.cloud.deployer.spi.kubernetes; import io.fabric8.kubernetes.client.KubernetesClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.AutoConfigureOrder; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.deployer.spi.app.ActuatorOperations; import org.springframework.cloud.deployer.spi.app.AppDeployer; import org.springframework.cloud.deployer.spi.task.TaskLauncher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.web.client.RestTemplate; /** * Spring Bean configuration for the {@link KubernetesAppDeployer}. * * @author Florian Rosenberg * @author Thomas Risberg * @author Ilayaperumal Gopinathan * @author Chris Schaefer */ @Configuration @EnableConfigurationProperties({KubernetesDeployerProperties.class, KubernetesTaskLauncherProperties.class}) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) public class KubernetesAutoConfiguration { @Autowired private KubernetesDeployerProperties deployerProperties; @Autowired private KubernetesTaskLauncherProperties taskLauncherProperties; @Bean @ConditionalOnMissingBean(AppDeployer.class) public AppDeployer appDeployer(KubernetesClient kubernetesClient, ContainerFactory containerFactory) { return new KubernetesAppDeployer(deployerProperties, kubernetesClient, containerFactory); } @Bean @ConditionalOnMissingBean(TaskLauncher.class) public TaskLauncher taskDeployer(KubernetesClient kubernetesClient, ContainerFactory containerFactory) { return new KubernetesTaskLauncher(deployerProperties, taskLauncherProperties, kubernetesClient, containerFactory); } @Bean @ConditionalOnMissingBean(KubernetesClient.class) public KubernetesClient kubernetesClient() { return KubernetesClientFactory.getKubernetesClient(this.deployerProperties); } @Bean public ContainerFactory containerFactory() { return new DefaultContainerFactory(deployerProperties); } @Bean @ConditionalOnMissingBean(ActuatorOperations.class) ActuatorOperations actuatorOperations(RestTemplate actuatorRestTemplate, AppDeployer appDeployer, KubernetesDeployerProperties properties) { return new KubernetesActuatorTemplate(actuatorRestTemplate, appDeployer, properties.getAppAdmin()); } @Bean @ConditionalOnMissingBean RestTemplate actuatorRestTemplate() { //TODO: Configure security return new RestTemplate(); } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/KubernetesClientFactory.java ================================================ /* * Copyright 2018-2019 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.cloud.deployer.spi.kubernetes; import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientBuilder; /** * The class responsible for creating Kubernetes Client based on the deployer properties. * * @author Ilayaperumal Gopinathan * @author Chris Schaefer */ public class KubernetesClientFactory { public static KubernetesClient getKubernetesClient(KubernetesDeployerProperties kubernetesDeployerProperties) { Config config = kubernetesDeployerProperties.getFabric8(); // use any fabric8 auto-detected properties, only set namespace from deployer properties if not null if (kubernetesDeployerProperties.getNamespace() != null) { config.setNamespace(kubernetesDeployerProperties.getNamespace()); } KubernetesClientBuilder builder = new KubernetesClientBuilder(); return builder.withConfig(config).build(); } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/KubernetesDeployerProperties.java ================================================ /* * Copyright 2015-2023 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.cloud.deployer.spi.kubernetes; import java.util.ArrayList; import java.util.List; import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonProperty; import io.fabric8.kubernetes.api.model.NodeAffinity; import io.fabric8.kubernetes.api.model.PodAffinity; import io.fabric8.kubernetes.api.model.PodAntiAffinity; import io.fabric8.kubernetes.api.model.Volume; import io.fabric8.kubernetes.api.model.VolumeMount; import io.fabric8.kubernetes.client.Config; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.NestedConfigurationProperty; import org.springframework.cloud.deployer.spi.app.AppAdmin; /** * @author Florian Rosenberg * @author Thomas Risberg * @author Donovan Muller * @author Ilayaperumal Gopinathan * @author Leonardo Diniz * @author Chris Schaefer * @author David Turanski * @author Enrique Medina Montenegro * @author Chris Bono * @author Corneil du Plessis */ @ConfigurationProperties(prefix = KubernetesDeployerProperties.KUBERNETES_DEPLOYER_PROPERTIES_PREFIX) public class KubernetesDeployerProperties { static final String KUBERNETES_DEPLOYER_PROPERTIES_PREFIX = "spring.cloud.deployer.kubernetes"; /** * Constants for app deployment properties that don't have a deployer level default * property. */ static final String KUBERNETES_DEPLOYMENT_NODE_SELECTOR = KUBERNETES_DEPLOYER_PROPERTIES_PREFIX + ".nodeSelector"; /** * The maximum concurrent tasks allowed for this platform instance. */ private int maximumConcurrentTasks = 20; /** * The number of seconds to wait before terminating the container. */ private Long terminationGracePeriodSeconds; @NestedConfigurationProperty private Config fabric8 = Config.autoConfigure(null); public Config getFabric8() { return this.fabric8; } public void setFabric8(Config fabric8) { this.fabric8 = fabric8; } /** * Encapsulates resources for Kubernetes Container resource limits */ public static class LimitsResources { /** * Container resource cpu limit. */ private String cpu; /** * Container resource memory limit. */ private String memory; /** * Container resource ephemeral-storage limit. */ private String ephemeralStorage; /** * Container resource hugepages-2Mi limit. */ private String hugepages2Mi; /** * Container resource hugepages-1Gi limit. */ private String hugepages1Gi; /** * Container GPU vendor name for limit * If gpuVendor=nvidia.com/gpu and gpuCount=2 then the following will be used * {@code * limits: * nvidia.com/gpu: 2 * } */ private String gpuVendor; /** * Container GPU count for limit. */ private String gpuCount; public LimitsResources() { } /** * New all args constructor * @param cpu Container limit for cpu resource * @param memory Container limit for memory resource * @param ephemeralStorage Container limit for ephemetal storage * @param hugepages2Mi Container limit for 2M huge pages * @param hugepages1Gi Container limit for 1G huge pages * @param gpuVendor The complete limit entry name for gpu vendor. * @param gpuCount */ public LimitsResources(String cpu, String memory, String ephemeralStorage, String hugepages2Mi, String hugepages1Gi, String gpuVendor, String gpuCount) { this.cpu = cpu; this.memory = memory; this.ephemeralStorage = ephemeralStorage; this.hugepages2Mi = hugepages2Mi; this.hugepages1Gi = hugepages1Gi; this.gpuVendor = gpuVendor; this.gpuCount = gpuCount; } public String getCpu() { return cpu; } public void setCpu(String cpu) { this.cpu = cpu; } public String getMemory() { return memory; } public void setMemory(String memory) { this.memory = memory; } public String getEphemeralStorage() { return ephemeralStorage; } public void setEphemeralStorage(String ephemeralStorage) { this.ephemeralStorage = ephemeralStorage; } public String getHugepages2Mi() { return hugepages2Mi; } public void setHugepages2Mi(String hugepages2Mi) { this.hugepages2Mi = hugepages2Mi; } public String getHugepages1Gi() { return hugepages1Gi; } public void setHugepages1Gi(String hugepages1Gi) { this.hugepages1Gi = hugepages1Gi; } public String getGpuVendor() { return gpuVendor; } public void setGpuVendor(String gpuVendor) { this.gpuVendor = gpuVendor; } public String getGpuCount() { return gpuCount; } public void setGpuCount(String gpuCount) { this.gpuCount = gpuCount; } } /** * Encapsulates resources for Kubernetes Container resource requests */ public static class RequestsResources { /** * Container request limit. */ private String cpu; /** * Container memory limit. */ private String memory; /** * Container resource ephemeral-storage request. */ private String ephemeralStorage; /** * Container resource hugepages-2Mi request. */ private String hugepages2Mi; /** * Container resource hugepages-1Gi request. */ private String hugepages1Gi; public RequestsResources() { } @Deprecated public RequestsResources(String cpu, String memory, String ephemeralStorage, String hugepages2Mi, String hugepages1Gi) { this.cpu = cpu; this.memory = memory; this.ephemeralStorage = ephemeralStorage; this.hugepages2Mi = hugepages2Mi; this.hugepages1Gi = hugepages1Gi; } public String getCpu() { return cpu; } public void setCpu(String cpu) { this.cpu = cpu; } public String getMemory() { return memory; } public void setMemory(String memory) { this.memory = memory; } public String getEphemeralStorage() { return ephemeralStorage; } public void setEphemeralStorage(String ephemeralStorage) { this.ephemeralStorage = ephemeralStorage; } public String getHugepages2Mi() { return hugepages2Mi; } public void setHugepages2Mi(String hugepages2Mi) { this.hugepages2Mi = hugepages2Mi; } public String getHugepages1Gi() { return hugepages1Gi; } public void setHugepages1Gi(String hugepages1Gi) { this.hugepages1Gi = hugepages1Gi; } } public static class StatefulSet { private VolumeClaimTemplate volumeClaimTemplate = new VolumeClaimTemplate(); public VolumeClaimTemplate getVolumeClaimTemplate() { return volumeClaimTemplate; } public void setVolumeClaimTemplate(VolumeClaimTemplate volumeClaimTemplate) { this.volumeClaimTemplate = volumeClaimTemplate; } public static class VolumeClaimTemplate { /** * VolumeClaimTemplate name */ private String name; /** * VolumeClaimTemplate storage. */ private String storage = "10m"; /** * VolumeClaimTemplate storage class name. */ private String storageClassName; public String getName() { return this.name; } public void setName(String name) { this.name = name; } public String getStorage() { return storage; } public void setStorage(String storage) { this.storage = storage; } public String getStorageClassName() { return storageClassName; } public void setStorageClassName(String storageClassName) { this.storageClassName = storageClassName; } } } public static class Toleration { private String effect; private String key; private String operator; private Long tolerationSeconds; private String value; public String getEffect() { return effect; } public void setEffect(String effect) { this.effect = effect; } public String getKey() { return key; } public void setKey(String key) { this.key = key; } public String getOperator() { return operator; } public void setOperator(String operator) { this.operator = operator; } public Long getTolerationSeconds() { return tolerationSeconds; } public void setTolerationSeconds(Long tolerationSeconds) { this.tolerationSeconds = tolerationSeconds; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } } static class KeyRef { private String envVarName; private String dataKey; public void setEnvVarName(String envVarName) { this.envVarName = envVarName; } public String getEnvVarName() { return envVarName; } public void setDataKey(String dataKey) { this.dataKey = dataKey; } public String getDataKey() { return dataKey; } } public static class SecretKeyRef extends KeyRef { private String secretName; public void setSecretName(String secretName) { this.secretName = secretName; } public String getSecretName() { return secretName; } } public static class ConfigMapKeyRef extends KeyRef { private String configMapName; public void setConfigMapName(String configMapName) { this.configMapName = configMapName; } public String getConfigMapName() { return configMapName; } } public static class PodSecurityContext { /** * The numeric user ID to run pod container processes under */ private Long runAsUser; /** * The numeric group id to run the entrypoint of the container process */ private Long runAsGroup; /** * Indicates that the container must run as a non-root user */ private Boolean runAsNonRoot; /** * The numeric group ID for the volumes of the pod */ private Long fsGroup; /** * Defines behavior of changing ownership and permission of the volume before being * exposed inside pod (only applies to volume types which support fsGroup based * ownership and permissions) - possible values are "OnRootMismatch", "Always" */ private String fsGroupChangePolicy; /** * The numeric group IDs applied to the pod container processes, in addition to the container's primary group ID */ private Long[] supplementalGroups; /** * The seccomp options to use for the pod containers */ private SeccompProfile seccompProfile; /** * The SELinux context to be applied to the pod containers */ private SELinuxOptions seLinuxOptions; /** * List of namespaced sysctls used for the pod (not used when spec.os.name is windows). */ private List sysctls; /** * The Windows specific settings applied to all containers. */ private WindowsSecurityContextOptions windowsOptions; public void setRunAsUser(Long runAsUser) { this.runAsUser = runAsUser; } public Long getRunAsUser() { return this.runAsUser; } public Long getRunAsGroup() { return runAsGroup; } public void setRunAsGroup(Long runAsGroup) { this.runAsGroup = runAsGroup; } public Boolean getRunAsNonRoot() { return runAsNonRoot; } public void setRunAsNonRoot(Boolean runAsNonRoot) { this.runAsNonRoot = runAsNonRoot; } public void setFsGroup(Long fsGroup) { this.fsGroup = fsGroup; } public Long getFsGroup() { return fsGroup; } public String getFsGroupChangePolicy() { return fsGroupChangePolicy; } public void setFsGroupChangePolicy(String fsGroupChangePolicy) { this.fsGroupChangePolicy = fsGroupChangePolicy; } public void setSupplementalGroups(Long[] supplementalGroups) { this.supplementalGroups = supplementalGroups; } public Long[] getSupplementalGroups() { return supplementalGroups; } public SeccompProfile getSeccompProfile() { return seccompProfile; } public void setSeccompProfile(SeccompProfile seccompProfile) { this.seccompProfile = seccompProfile; } public SELinuxOptions getSeLinuxOptions() { return seLinuxOptions; } public void setSeLinuxOptions(SELinuxOptions seLinuxOptions) { this.seLinuxOptions = seLinuxOptions; } public List getSysctls() { return sysctls; } public void setSysctls(List sysctls) { this.sysctls = sysctls; } public WindowsSecurityContextOptions getWindowsOptions() { return windowsOptions; } public void setWindowsOptions(WindowsSecurityContextOptions windowsOptions) { this.windowsOptions = windowsOptions; } } public static class ContainerSecurityContext { /** * Whether a process can gain more privileges than its parent process */ private Boolean allowPrivilegeEscalation; /** * The capabilities to add/drop when running the container (cannot be set when spec.os.name is windows) */ private Capabilities capabilities; /** * Run container in privileged mode. */ private Boolean privileged; /** * The type of proc mount to use for the container (cannot be set when spec.os.name is windows) */ private String procMount; /** * Mounts the container's root filesystem as read-only */ private Boolean readOnlyRootFilesystem; /** * The numeric user ID to run pod container processes under */ private Long runAsUser; /** * The numeric group id to run the entrypoint of the container process */ private Long runAsGroup; /** * Indicates that the container must run as a non-root user */ private Boolean runAsNonRoot; /** * The seccomp options to use for the container */ private SeccompProfile seccompProfile; /** * The SELinux context to be applied to the container. */ private SELinuxOptions seLinuxOptions; /** * The Windows specific settings applied to the container. */ private WindowsSecurityContextOptions windowsOptions; public Boolean getAllowPrivilegeEscalation() { return allowPrivilegeEscalation; } public void setAllowPrivilegeEscalation(Boolean allowPrivilegeEscalation) { this.allowPrivilegeEscalation = allowPrivilegeEscalation; } public Capabilities getCapabilities() { return capabilities; } public void setCapabilities(Capabilities capabilities) { this.capabilities = capabilities; } public Boolean getPrivileged() { return privileged; } public void setPrivileged(Boolean privileged) { this.privileged = privileged; } public String getProcMount() { return procMount; } public void setProcMount(String procMount) { this.procMount = procMount; } public Boolean getReadOnlyRootFilesystem() { return readOnlyRootFilesystem; } public void setReadOnlyRootFilesystem(Boolean readOnlyRootFilesystem) { this.readOnlyRootFilesystem = readOnlyRootFilesystem; } public Long getRunAsUser() { return runAsUser; } public void setRunAsUser(Long runAsUser) { this.runAsUser = runAsUser; } public Long getRunAsGroup() { return runAsGroup; } public void setRunAsGroup(Long runAsGroup) { this.runAsGroup = runAsGroup; } public Boolean getRunAsNonRoot() { return runAsNonRoot; } public void setRunAsNonRoot(Boolean runAsNonRoot) { this.runAsNonRoot = runAsNonRoot; } public SeccompProfile getSeccompProfile() { return seccompProfile; } public void setSeccompProfile(SeccompProfile seccompProfile) { this.seccompProfile = seccompProfile; } public SELinuxOptions getSeLinuxOptions() { return seLinuxOptions; } public void setSeLinuxOptions(SELinuxOptions seLinuxOptions) { this.seLinuxOptions = seLinuxOptions; } public WindowsSecurityContextOptions getWindowsOptions() { return windowsOptions; } public void setWindowsOptions(WindowsSecurityContextOptions windowsOptions) { this.windowsOptions = windowsOptions; } } /** * Adds and removes POSIX capabilities from running containers. */ public static class Capabilities { /** * Added capabilities. */ private List add; /** * Removed capabilities. */ private List drop; public List getAdd() { return add; } public void setAdd(List add) { this.add = add; } public List getDrop() { return drop; } public void setDrop(List drop) { this.drop = drop; } } /** * Defines a pod seccomp profile settings. */ public static class SeccompProfile { /** * Type of seccomp profile. */ private String type; /** * Path of the pre-configured profile on the node, relative to the kubelet's configured Seccomp profile location, only valid when type is "Localhost". */ private String localhostProfile; public String getType() { return type; } public void setType(String type) { this.type = type; } public String getLocalhostProfile() { return localhostProfile; } public void setLocalhostProfile(String localhostProfile) { this.localhostProfile = localhostProfile; } } /** * The labels to be applied to the container. */ public static class SELinuxOptions { /** * Level label applied to the container */ private String level; /** * Role Level label applied to the container */ private String role; /** * Type label applied to the container */ private String type; /** * User label applied to the container */ private String user; public String getLevel() { return level; } public void setLevel(String level) { this.level = level; } public String getRole() { return role; } public void setRole(String role) { this.role = role; } public String getType() { return type; } public void setType(String type) { this.type = type; } public String getUser() { return user; } public void setUser(String user) { this.user = user; } } /** * Sysctl defines a kernel parameter to be set on the pod. */ public static class SysctlInfo { /** * Name of the property */ private String name; /** * Value of the property */ private String value; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } } /** * Contains Windows-specific options and credentials. */ public static class WindowsSecurityContextOptions { /** * Where the GMSA admission webhook inlines the contents of the GMSA credential spec * named by the GMSACredentialSpecName field. */ private String gmsaCredentialSpec; /** * The name of the GMSA credential spec to use. */ private String gmsaCredentialSpecName; /** * Whether a container should be run as a 'Host Process' container. */ private Boolean hostProcess; /** * The username in Windows to run the entrypoint of the container process. */ private String runAsUserName; public String getGmsaCredentialSpec() { return gmsaCredentialSpec; } public void setGmsaCredentialSpec(String gmsaCredentialSpec) { this.gmsaCredentialSpec = gmsaCredentialSpec; } public String getGmsaCredentialSpecName() { return gmsaCredentialSpecName; } public void setGmsaCredentialSpecName(String gmsaCredentialSpecName) { this.gmsaCredentialSpecName = gmsaCredentialSpecName; } public Boolean getHostProcess() { return hostProcess; } public void setHostProcess(Boolean hostProcess) { this.hostProcess = hostProcess; } public String getRunAsUserName() { return runAsUserName; } public void setRunAsUserName(String runAsUserName) { this.runAsUserName = runAsUserName; } } public static class Lifecycle { private Hook postStart; private Hook preStop; Hook getPreStop() { return preStop; } Hook getPostStart() { return postStart; } void setPostStart(Hook postStart) { this.postStart = postStart; } void setPreStop(Hook preStop) { this.preStop = preStop; } public static class Hook { private Exec exec; Exec getExec() { return exec; } void setExec(Exec exec) { this.exec = exec; } } public static class Exec { private List command; List getCommand() { return command; } void setCommand(List command) { this.command = command; } } } public static class InitContainer extends ContainerProperties { } static class Container extends io.fabric8.kubernetes.api.model.Container { } public static class ContainerProperties { @JsonAlias("imageName") @JsonProperty("image") private String image; @JsonAlias("containerName") @JsonProperty("name") private String name; @JsonProperty("command") @JsonAlias("commands") private List command; private List args; private List volumeMounts; /** * Environment variables to set for any deployed init container. */ private String[] environmentVariables = new String[]{}; private String[] environmentVariablesFromFieldRefs = new String[]{}; private String[] configMapRefEnvVars = new String[]{}; private String[] secretRefEnvVars = new String[]{}; public String getName() { return name; } public void setName(String name) { this.name = name; } @Deprecated public String getImageName() { return getImage(); } @Deprecated public void setImageName(String image) { setImage(image); } public String getImage() { return image; } public void setImage(String image) { this.image = image; } @Deprecated public String getContainerName() { return getName(); } @Deprecated public void setContainerName(String containerName) { setName(containerName); } public List getCommand() { return command; } public void setCommand(List command) { this.command = command; } @Deprecated public List getCommands() { return getCommand(); } @Deprecated public void setCommands(List commands) { setCommand(commands); } public List getArgs() { return args; } public void setArgs(List args) { this.args = args; } public List getVolumeMounts() { return volumeMounts; } public void setVolumeMounts(List volumeMounts) { this.volumeMounts = volumeMounts; } public String[] getEnvironmentVariables() { return environmentVariables; } public void setEnvironmentVariables(String[] environmentVariables) { this.environmentVariables = environmentVariables; } public String[] getEnvironmentVariablesFromFieldRefs() { return environmentVariablesFromFieldRefs; } public void setEnvironmentVariablesFromFieldRefs(String[] environmentVariablesFromFieldRefs) { this.environmentVariablesFromFieldRefs = environmentVariablesFromFieldRefs; } public String[] getConfigMapRefEnvVars() { return configMapRefEnvVars; } public void setConfigMapRefEnvVars(String[] configMapRefEnvVars) { this.configMapRefEnvVars = configMapRefEnvVars; } public String[] getSecretRefEnvVars() { return secretRefEnvVars; } public void setSecretRefEnvVars(String[] secretRefEnvVars) { this.secretRefEnvVars = secretRefEnvVars; } } /** * The {@link RestartPolicy} to use. Defaults to {@link RestartPolicy#Always}. */ private RestartPolicy restartPolicy = RestartPolicy.Always; /** * The default service account name to use for tasks. */ protected static final String DEFAULT_TASK_SERVICE_ACCOUNT_NAME = "default"; /** * Service account name to use for tasks, defaults to: * {@link #DEFAULT_TASK_SERVICE_ACCOUNT_NAME} */ private String taskServiceAccountName = DEFAULT_TASK_SERVICE_ACCOUNT_NAME; /** * Obtains the {@link RestartPolicy} to use. Defaults to * {@link #restartPolicy}. * * @return the {@link RestartPolicy} to use */ public RestartPolicy getRestartPolicy() { return restartPolicy; } /** * Sets the {@link RestartPolicy} to use. * * @param restartPolicy the {@link RestartPolicy} to use */ public void setRestartPolicy(RestartPolicy restartPolicy) { this.restartPolicy = restartPolicy; } /** * Obtains the service account name to use for tasks. * * @return the service account name */ public String getTaskServiceAccountName() { return taskServiceAccountName; } /** * Sets the service account name to use for tasks. * * @param taskServiceAccountName the service account name */ public void setTaskServiceAccountName(String taskServiceAccountName) { this.taskServiceAccountName = taskServiceAccountName; } /** * Name of the environment variable that can define the Kubernetes namespace to use. */ public static final String ENV_KEY_KUBERNETES_NAMESPACE = "KUBERNETES_NAMESPACE"; private static String KUBERNETES_NAMESPACE = System.getenv("KUBERNETES_NAMESPACE"); /** * Namespace to use. */ private String namespace = KUBERNETES_NAMESPACE; /** * Secrets for a access a private registry to pull images. */ private String imagePullSecret; /** * List of Secrets for a access a private registry to pull images. */ private List imagePullSecrets; /** * Delay in seconds when the Kubernetes liveness check of the app container should start * checking its health status. */ private int livenessHttpProbeDelay = 1; /** * When the probe fails more times than the failure value the pod is restarted. */ private int livenessHttpProbeFailure = 3; /** * When the probe passes success times it is considered live. */ private int livenessHttpProbeSuccess = 1; /** * Period in seconds for performing the Kubernetes liveness check of the app container. */ private int livenessHttpProbePeriod = 60; /** * Timeout in seconds for the check to wait for a response after which it is considered a failed check. */ private int livenessHttpProbeTimeout = 5; /** * Timeout in seconds for the liveness check to wait for a response before the check is considered a failure. */ private int livenessTcpProbeTimeout = 5; /** * Path that app container has to respond to for liveness check. */ private String livenessHttpProbePath; /** * Port that app container has to respond on for liveness check. */ private Integer livenessHttpProbePort = null; /** * Schema that app container has to respond on for liveness check. */ private String livenessHttpProbeScheme = "HTTP"; /** * Schema that app container has to respond to for readiness check. */ private String readinessHttpProbeScheme = "HTTP"; /** * Schema that app container has to respond to for startup check. */ private String startupProbeScheme = "HTTP"; /** * If present will assign to spec.shareProcessNamespace of the Pod. */ private Boolean shareProcessNamespace; /** * Delay in seconds when the readiness check of the app container should start checking if * the module is fully up and running. */ private int readinessHttpProbeDelay = 1; private int readinessHttpProbeSuccess = 1; private int readinessHttpProbeFailure = 3; private int readinessTcpProbeTimeout = 3; private int readinessTcpProbeFailure = 3; private int readinessTcpProbeSuccess = 1; /** * Delay in seconds when the startup check of the app container should start checking if * the module is fully up and running. */ private int startupHttpProbeDelay = 30; /** * The number of time the http startup probe will be allowed to fail before restarting the pod. */ private int startupHttpProbeFailure = 20; /** * The number of time the http startup probe will be required to pass before considering the application started. */ private int startupHttpProbeSuccess = 1; /** * The number of time the tcp startup probe will be allowed to fail before restarting the pod. */ private int startupTcpProbeFailure = 20; /** * The number of time the tcp startup probe will be required to pass before considering the application started. */ private int startupTcpProbeSuccess = 1; /** * The number of time the command startup probe will be allowed to fail before restarting the pod. */ private int startupCommandProbeFailure = 10; /** * The number of time the command startup probe will be required to pass before considering the application started. */ private int startupCommandProbeSuccess = 1; /** * Period in seconds to perform the readiness check of the app container. */ private int readinessHttpProbePeriod = 10; /** * Period in seconds to perform the startup check of the app container. */ private int startupHttpProbePeriod = 3; /** * Timeout in seconds that the app container has to respond to its status during * the startup check. */ private int startupHttpProbeTimeout = 5; /** * Timeout in seconds that the app container has to respond to its health status during * the readiness check. */ private int readinessHttpProbeTimeout = 5; private int readinessCommandProbeFailure = 3; private int readinessCommandProbeSuccess = 1; /** * Path that app container has to respond to for readiness check. */ private String readinessHttpProbePath; /** * Path that app container has to respond to for startup check. */ private String startupHttpProbePath; /** * Port that app container has to respond on for readiness check. */ private Integer readinessHttpProbePort = null; /** * Port that app container has to respond on for startup check. */ private Integer startupHttpProbePort = null; /** * Delay in seconds when the liveness TCP check should start checking */ private int livenessTcpProbeDelay = 10; private int livenessTcpProbeSuccess = 1; private int livenessTcpProbeFailure = 3; /** * Period in seconds to perform the liveness TCP check */ private int livenessTcpProbePeriod = 60; /** * The TCP port the liveness probe should check */ private Integer livenessTcpProbePort = null; /** * Delay in seconds when the readiness TCP check should start checking */ private int readinessTcpProbeDelay = 1; /** * Period in seconds to perform the readiness TCP check */ private int readinessTcpProbePeriod = 10; /** * The TCP port the readiness probe should check */ private Integer readinessTcpProbePort = null; /** * Delay in seconds when the readiness command check should start checking */ private int readinessCommandProbeDelay = 1; /** * Period in seconds to perform the readiness command check */ private int readinessCommandProbePeriod = 10; /** * The command the readiness probe should use to check */ private String readinessCommandProbeCommand = null; /** * Delay in seconds when the readiness TCP check should start checking */ private int startupTcpProbeDelay = 30; private int startupTcpProbeTimeout = 5; /** * Period in seconds to perform the readiness TCP check */ private int startupTcpProbePeriod = 3; /** * The TCP port the readiness probe should check */ private Integer startupTcpProbePort = null; /** * Delay in seconds when the readiness command check should start checking */ private int startupCommandProbeDelay = 30; /** * Period in seconds to perform the readiness command check */ private int startupCommandProbePeriod = 10; /** * The command the readiness probe should use to check */ private String startupCommandProbeCommand = null; /** * Delay in seconds when the liveness command check should start checking */ private int livenessCommandProbeDelay = 10; private int livenessCommandProbeFailure = 3; private int livenessCommandProbeSuccess = 1; /** * Period in seconds to perform the liveness command check */ private int livenessCommandProbePeriod = 10; /** * The command the liveness probe should use to check */ private String livenessCommandProbeCommand = null; /** * The secret name containing the credentials to use when accessing secured probe * endpoints. */ private String probeCredentialsSecret; /** * The probe type to use when doing health checks. Defaults to HTTP. */ private ProbeType probeType = ProbeType.HTTP; /** * Memory and CPU limits (i.e. maximum needed values) to allocate for a Pod. */ private LimitsResources limits = new LimitsResources(); /** * Memory and CPU requests (i.e. guaranteed needed values) to allocate for a Pod. */ private RequestsResources requests = new RequestsResources(); /** * Tolerations to allocate for a Pod. */ private List tolerations = new ArrayList<>(); /** * Secret key references to be added to the Pod environment. */ private List secretKeyRefs = new ArrayList<>(); /** * ConfigMap key references to be added to the Pod environment. */ private List configMapKeyRefs = new ArrayList<>(); /** * ConfigMap references to be added to the Pod environment. */ private List configMapRefs = new ArrayList<>(); /** * Secret references to be added to the Pod environment. */ private List secretRefs = new ArrayList<>(); /** * Resources to assign for VolumeClaimTemplates (identified by metadata name) inside * StatefulSet. */ private StatefulSet statefulSet = new StatefulSet(); /** * Environment variables to set for any deployed app container. To be used for service * binding. */ private String[] environmentVariables = new String[]{}; /** * Entry point style used for the Docker image. To be used to determine how to pass in * properties. */ private EntryPointStyle entryPointStyle = EntryPointStyle.exec; /** * Create a "LoadBalancer" for the service created for each app. This facilitates * assignment of external IP to app. */ private boolean createLoadBalancer = false; /** * Service annotations to set for the service created for each app. */ private String serviceAnnotations = null; /** * Pod annotations to set for the pod created for each deployment. */ private String podAnnotations; /** * Job annotations to set for the pod or job created for a job. */ private String jobAnnotations; /** * Time to wait for load balancer to be available before attempting delete of service (in * minutes). */ private int minutesToWaitForLoadBalancer = 5; /** * Maximum allowed restarts for app that fails due to an error or excessive resource use. */ private int maxTerminatedErrorRestarts = 2; /** * Maximum allowed restarts for app that is in a CrashLoopBackOff. */ private int maxCrashLoopBackOffRestarts = 4; /** * The image pull policy to use for Pod deployments in Kubernetes. */ private ImagePullPolicy imagePullPolicy = ImagePullPolicy.IfNotPresent; /** * Volume mounts that a container is requesting. This can be specified as a deployer * property or as an app deployment property. Deployment properties will override deployer * properties. */ private List volumeMounts = new ArrayList<>(); /** * The volumes that a Kubernetes instance supports. See * https://kubernetes.io/docs/user-guide/volumes/#types-of-volumes This can be specified * as a deployer property or as an app deployment property. Deployment properties will * override deployer properties. */ private List volumes = new ArrayList<>(); /** * The hostNetwork setting for the deployments. See * https://kubernetes.io/docs/api-reference/v1/definitions/#_v1_podspec This can be * specified as a deployer property or as an app deployment property. Deployment * properties will override deployer properties. */ private boolean hostNetwork = false; /** * Create a "Job" instead of just a "Pod" when launching tasks. See * https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/ */ private boolean createJob = false; /** * The node selector to use in key:value format, comma separated */ private String nodeSelector; /** * Service account name to use for app deployments */ private String deploymentServiceAccountName; /** * The security context to apply to created pod's. */ private PodSecurityContext podSecurityContext; /** * The security context to apply to created pod's main container. */ private ContainerSecurityContext containerSecurityContext; /** * The node affinity rules to apply. */ private NodeAffinity nodeAffinity; /** * The pod affinity rules to apply */ private PodAffinity podAffinity; /** * The pod anti-affinity rules to apply */ private PodAntiAffinity podAntiAffinity; /** * A custom init container image name to use when creating a StatefulSet */ private String statefulSetInitContainerImageName; /** * A custom init container to apply. */ private InitContainer initContainer; /** * Apply multiple custom init containers. * Will add initContainer first if it exists and add to list. */ private List initContainers = new ArrayList<>(); /** * Lifecycle spec to apply. */ private Lifecycle lifecycle = new Lifecycle(); /** * The additional containers one can add to the main application container. */ private List additionalContainers; /** * Deployment label to be applied to Deployment, StatefulSet, JobSpec etc., */ private String deploymentLabels; /** * Pod priorityClassName. The user should create a PriorityClass before setting this property. */ private String priorityClassName; private AppAdmin appAdmin = new AppAdmin(); public String getNamespace() { return namespace; } public void setNamespace(String namespace) { this.namespace = namespace; } public String getImagePullSecret() { return imagePullSecret; } public void setImagePullSecret(String imagePullSecret) { this.imagePullSecret = imagePullSecret; } public List getImagePullSecrets() { return imagePullSecrets; } public void setImagePullSecrets(List imagePullSecrets) { this.imagePullSecrets = imagePullSecrets; } public static class CronConfig { private String concurrencyPolicy; private Integer ttlSecondsAfterFinished; private Integer backoffLimit; public String getConcurrencyPolicy() { return concurrencyPolicy; } public void setConcurrencyPolicy(String concurrencyPolicy) { this.concurrencyPolicy = concurrencyPolicy; } public Integer getTtlSecondsAfterFinished() { return ttlSecondsAfterFinished; } public void setTtlSecondsAfterFinished(Integer ttlSecondsAfterFinished) { this.ttlSecondsAfterFinished = ttlSecondsAfterFinished; } public Integer getBackoffLimit() { return backoffLimit; } public void setBackoffLimit(Integer backoffLimit) { this.backoffLimit = backoffLimit; } } public Boolean getShareProcessNamespace() { return shareProcessNamespace; } public void setShareProcessNamespace(Boolean shareProcessNamespace) { this.shareProcessNamespace = shareProcessNamespace; } public String getPriorityClassName() { return priorityClassName; } public void setPriorityClassName(String priorityClassName) { this.priorityClassName = priorityClassName; } public int getLivenessTcpProbeSuccess() { return livenessTcpProbeSuccess; } public void setLivenessTcpProbeSuccess(int livenessTcpProbeSuccess) { this.livenessTcpProbeSuccess = livenessTcpProbeSuccess; } public int getLivenessTcpProbeFailure() { return livenessTcpProbeFailure; } public void setLivenessTcpProbeFailure(int livenessTcpProbeFailure) { this.livenessTcpProbeFailure = livenessTcpProbeFailure; } public int getLivenessHttpProbeDelay() { return livenessHttpProbeDelay; } public void setLivenessHttpProbeDelay(int livenessHttpProbeDelay) { this.livenessHttpProbeDelay = livenessHttpProbeDelay; } public int getLivenessTcpProbeTimeout() { return livenessTcpProbeTimeout; } public void setLivenessTcpProbeTimeout(int livenessTcpProbeTimeout) { this.livenessTcpProbeTimeout = livenessTcpProbeTimeout; } public int getLivenessHttpProbePeriod() { return livenessHttpProbePeriod; } public void setLivenessHttpProbePeriod(int livenessHttpProbePeriod) { this.livenessHttpProbePeriod = livenessHttpProbePeriod; } public int getLivenessHttpProbeTimeout() { return livenessHttpProbeTimeout; } public void setLivenessHttpProbeTimeout(int livenessHttpProbeTimeout) { this.livenessHttpProbeTimeout = livenessHttpProbeTimeout; } public String getLivenessHttpProbePath() { return livenessHttpProbePath; } public void setLivenessHttpProbePath(String livenessHttpProbePath) { this.livenessHttpProbePath = livenessHttpProbePath; } public Integer getLivenessHttpProbePort() { return livenessHttpProbePort; } public void setLivenessHttpProbePort(Integer livenessHttpProbePort) { this.livenessHttpProbePort = livenessHttpProbePort; } public int getStartupHttpProbeTimeout() { return startupHttpProbeTimeout; } public int getStartupHttpProbeFailure() { return startupHttpProbeFailure; } public void setStartupHttpProbeFailure(int startupHttpProbeFailure) { this.startupHttpProbeFailure = startupHttpProbeFailure; } public void setStartupProbeFailure(int startupHttpProbeFailure) { this.startupHttpProbeFailure = startupHttpProbeFailure; } public int getLivenessHttpProbeFailure() { return livenessHttpProbeFailure; } public void setLivenessHttpProbeFailure(int livenessHttpProbeFailure) { this.livenessHttpProbeFailure = livenessHttpProbeFailure; } public void setLivenessProbeFailure(int livenessHttpProbeFailure) { this.livenessHttpProbeFailure = livenessHttpProbeFailure; } public int getLivenessHttpProbeSuccess() { return livenessHttpProbeSuccess; } public void setLivenessHttpProbeSuccess(int livenessHttpProbeSuccess) { this.livenessHttpProbeSuccess = livenessHttpProbeSuccess; } public void setLivenessProbeSuccess(int livenessHttpProbeSuccess) { this.livenessHttpProbeSuccess = livenessHttpProbeSuccess; } public int getStartupHttpProbeSuccess() { return startupHttpProbeSuccess; } public void setStartupHttpProbeSuccess(int startupHttpProbeSuccess) { this.startupHttpProbeSuccess = startupHttpProbeSuccess; } public void setStartupProbeSuccess(int startupHttpProbeSuccess) { this.startupHttpProbeSuccess = startupHttpProbeSuccess; } public int getStartupTcpProbeFailure() { return startupTcpProbeFailure; } public void setStartupTcpProbeFailure(int startupTcpProbeFailure) { this.startupTcpProbeFailure = startupTcpProbeFailure; } public int getStartupTcpProbeSuccess() { return startupTcpProbeSuccess; } public void setStartupTcpProbeSuccess(int startupTcpProbeSuccess) { this.startupTcpProbeSuccess = startupTcpProbeSuccess; } public int getStartupCommandProbeFailure() { return startupCommandProbeFailure; } public void setStartupCommandProbeFailure(int startupCommandProbeFailure) { this.startupCommandProbeFailure = startupCommandProbeFailure; } public int getStartupCommandProbeSuccess() { return startupCommandProbeSuccess; } public void setStartupCommandProbeSuccess(int startupCommandProbeSuccess) { this.startupCommandProbeSuccess = startupCommandProbeSuccess; } public int getLivenessCommandProbeFailure() { return livenessCommandProbeFailure; } public void setLivenessCommandProbeFailure(int livenessCommandProbeFailure) { this.livenessCommandProbeFailure = livenessCommandProbeFailure; } public int getLivenessCommandProbeSuccess() { return livenessCommandProbeSuccess; } public void setLivenessCommandProbeSuccess(int livenessCommandProbeSuccess) { this.livenessCommandProbeSuccess = livenessCommandProbeSuccess; } public int getStartupHttpProbeDelay() { return startupHttpProbeDelay; } public void setStartupHttpProbeTimeout(int startupHttpProbeTimeout) { this.startupHttpProbeTimeout = startupHttpProbeTimeout; } public void setStartupProbeTimeout(int startupHttpProbeTimeout) { this.startupHttpProbeTimeout = startupHttpProbeTimeout; } public void setStartupHttpProbeDelay(int startupHttpProbeDelay) { this.startupHttpProbeDelay = startupHttpProbeDelay; } public void setStartupProbeDelay(int startupHttpProbeDelay) { this.startupHttpProbeDelay = startupHttpProbeDelay; } public String getStartupHttpProbePath() { return startupHttpProbePath; } public void setStartupHttpProbePath(String startupHttpProbePath) { this.startupHttpProbePath = startupHttpProbePath; } public Integer getStartupHttpProbePort() { return startupHttpProbePort; } public void setStartupHttpProbePort(Integer startupHttpProbePort) { this.startupHttpProbePort = startupHttpProbePort; } public void setStartupProbePort(Integer startupHttpProbePort) { this.startupHttpProbePort = startupHttpProbePort; } public String getStartupProbeScheme() { return startupProbeScheme; } public void setStartupProbeScheme(String startupProbeScheme) { this.startupProbeScheme = startupProbeScheme; } public int getStartupHttpProbePeriod() { return startupHttpProbePeriod; } public void setStartupHttpProbePeriod(int startupHttpProbePeriod) { this.startupHttpProbePeriod = startupHttpProbePeriod; } public int getStartupProbePeriod() { return startupHttpProbePeriod; } public void setStartupProbePeriod(int startupHttpProbePeriod) { this.startupHttpProbePeriod = startupHttpProbePeriod; } public int getStartupTcpProbeDelay() { return startupTcpProbeDelay; } public void setStartupTcpProbeDelay(int startupTcpProbeDelay) { this.startupTcpProbeDelay = startupTcpProbeDelay; } public int getStartupTcpProbeTimeout() { return startupTcpProbeTimeout; } public void setStartupTcpProbeTimeout(int startupTcpProbeTimeout) { this.startupTcpProbeTimeout = startupTcpProbeTimeout; } /** * Cron configuration for job scheduling */ private CronConfig cron = new CronConfig(); public int getStartupTcpProbePeriod() { return startupTcpProbePeriod; } public void setStartupTcpProbePeriod(int startupTcpProbePeriod) { this.startupTcpProbePeriod = startupTcpProbePeriod; } public Integer getStartupTcpProbePort() { return startupTcpProbePort; } public void setStartupTcpProbePort(Integer startupTcpProbePort) { this.startupTcpProbePort = startupTcpProbePort; } public int getStartupCommandProbeDelay() { return startupCommandProbeDelay; } public void setStartupCommandProbeDelay(int startupCommandProbeDelay) { this.startupCommandProbeDelay = startupCommandProbeDelay; } public int getStartupCommandProbePeriod() { return startupCommandProbePeriod; } public void setStartupCommandProbePeriod(int startupCommandProbePeriod) { this.startupCommandProbePeriod = startupCommandProbePeriod; } public String getStartupCommandProbeCommand() { return startupCommandProbeCommand; } public void setStartupCommandProbeCommand(String startupCommandProbeCommand) { this.startupCommandProbeCommand = startupCommandProbeCommand; } public int getReadinessTcpProbeFailure() { return readinessTcpProbeFailure; } public void setReadinessTcpProbeFailure(int readinessTcpProbeFailure) { this.readinessTcpProbeFailure = readinessTcpProbeFailure; } public int getReadinessTcpProbeSuccess() { return readinessTcpProbeSuccess; } public void setReadinessTcpProbeSuccess(int readinessTcpProbeSuccess) { this.readinessTcpProbeSuccess = readinessTcpProbeSuccess; } public int getReadinessHttpProbeSuccess() { return readinessHttpProbeSuccess; } public void setReadinessHttpProbeSuccess(int readinessHttpProbeSuccess) { this.readinessHttpProbeSuccess = readinessHttpProbeSuccess; } public int getReadinessHttpProbeFailure() { return readinessHttpProbeFailure; } public void setReadinessHttpProbeFailure(int readinessHttpProbeFailure) { this.readinessHttpProbeFailure = readinessHttpProbeFailure; } public void setReadinessProbeFailure(int readinessHttpProbeFailure) { this.readinessHttpProbeFailure = readinessHttpProbeFailure; } public int getReadinessCommandProbeFailure() { return readinessCommandProbeFailure; } public void setReadinessCommandProbeFailure(int readinessCommandProbeFailure) { this.readinessCommandProbeFailure = readinessCommandProbeFailure; } public int getReadinessCommandProbeSuccess() { return readinessCommandProbeSuccess; } public void setReadinessCommandProbeSuccess(int readinessCommandProbeSuccess) { this.readinessCommandProbeSuccess = readinessCommandProbeSuccess; } public int getReadinessHttpProbeDelay() { return readinessHttpProbeDelay; } public void setReadinessHttpProbeDelay(int readinessHttpProbeDelay) { this.readinessHttpProbeDelay = readinessHttpProbeDelay; } public int getReadinessHttpProbePeriod() { return readinessHttpProbePeriod; } public void setReadinessHttpProbePeriod(int readinessHttpProbePeriod) { this.readinessHttpProbePeriod = readinessHttpProbePeriod; } public int getReadinessTcpProbeTimeout() { return readinessTcpProbeTimeout; } public void setReadinessTcpProbeTimeout(int readinessTcpProbeTimeout) { this.readinessTcpProbeTimeout = readinessTcpProbeTimeout; } public int getReadinessHttpProbeTimeout() { return readinessHttpProbeTimeout; } public void setReadinessHttpProbeTimeout(int readinessHttpProbeTimeout) { this.readinessHttpProbeTimeout = readinessHttpProbeTimeout; } public String getReadinessHttpProbePath() { return readinessHttpProbePath; } public void setReadinessHttpProbePath(String readinessHttpProbePath) { this.readinessHttpProbePath = readinessHttpProbePath; } public Integer getReadinessHttpProbePort() { return readinessHttpProbePort; } public void setReadinessHttpProbePort(Integer readinessHttpProbePort) { this.readinessHttpProbePort = readinessHttpProbePort; } public int getLivenessTcpProbeDelay() { return livenessTcpProbeDelay; } public void setLivenessTcpProbeDelay(int livenessTcpProbeDelay) { this.livenessTcpProbeDelay = livenessTcpProbeDelay; } public int getLivenessTcpProbePeriod() { return livenessTcpProbePeriod; } public void setLivenessTcpProbePeriod(int livenessTcpProbePeriod) { this.livenessTcpProbePeriod = livenessTcpProbePeriod; } public Integer getLivenessTcpProbePort() { return livenessTcpProbePort; } public void setLivenessTcpProbePort(Integer livenessTcpProbePort) { this.livenessTcpProbePort = livenessTcpProbePort; } public int getReadinessTcpProbeDelay() { return readinessTcpProbeDelay; } public void setReadinessTcpProbeDelay(int readinessTcpProbeDelay) { this.readinessTcpProbeDelay = readinessTcpProbeDelay; } public int getReadinessTcpProbePeriod() { return readinessTcpProbePeriod; } public void setReadinessTcpProbePeriod(int readinessTcpProbePeriod) { this.readinessTcpProbePeriod = readinessTcpProbePeriod; } public Integer getReadinessTcpProbePort() { return readinessTcpProbePort; } public void setReadinessTcpProbePort(Integer readinessTcpProbePort) { this.readinessTcpProbePort = readinessTcpProbePort; } public int getReadinessCommandProbeDelay() { return readinessCommandProbeDelay; } public void setReadinessCommandProbeDelay(int readinessCommandProbeDelay) { this.readinessCommandProbeDelay = readinessCommandProbeDelay; } public int getReadinessCommandProbePeriod() { return readinessCommandProbePeriod; } public void setReadinessCommandProbePeriod(int readinessCommandProbePeriod) { this.readinessCommandProbePeriod = readinessCommandProbePeriod; } public String getReadinessCommandProbeCommand() { return readinessCommandProbeCommand; } public void setReadinessCommandProbeCommand(String readinessCommandProbeCommand) { this.readinessCommandProbeCommand = readinessCommandProbeCommand; } public int getLivenessCommandProbeDelay() { return livenessCommandProbeDelay; } public void setLivenessCommandProbeDelay(int livenessCommandProbeDelay) { this.livenessCommandProbeDelay = livenessCommandProbeDelay; } public int getLivenessCommandProbePeriod() { return livenessCommandProbePeriod; } public void setLivenessCommandProbePeriod(int livenessCommandProbePeriod) { this.livenessCommandProbePeriod = livenessCommandProbePeriod; } public String getLivenessCommandProbeCommand() { return livenessCommandProbeCommand; } public void setLivenessCommandProbeCommand(String livenessCommandProbeCommand) { this.livenessCommandProbeCommand = livenessCommandProbeCommand; } public String getProbeCredentialsSecret() { return probeCredentialsSecret; } public void setProbeCredentialsSecret(String probeCredentialsSecret) { this.probeCredentialsSecret = probeCredentialsSecret; } public ProbeType getProbeType() { return probeType; } public void setProbeType(ProbeType probeType) { this.probeType = probeType; } public StatefulSet getStatefulSet() { return statefulSet; } public void setStatefulSet( StatefulSet statefulSet) { this.statefulSet = statefulSet; } public List getTolerations() { return tolerations; } public void setTolerations(List tolerations) { this.tolerations = tolerations; } public List getSecretKeyRefs() { return secretKeyRefs; } public void setSecretKeyRefs(List secretKeyRefs) { this.secretKeyRefs = secretKeyRefs; } public List getConfigMapKeyRefs() { return configMapKeyRefs; } public void setConfigMapKeyRefs(List configMapKeyRefs) { this.configMapKeyRefs = configMapKeyRefs; } public List getConfigMapRefs() { return configMapRefs; } public void setConfigMapRefs(List configMapRefs) { this.configMapRefs = configMapRefs; } public List getSecretRefs() { return secretRefs; } public void setSecretRefs(List secretRefs) { this.secretRefs = secretRefs; } public String[] getEnvironmentVariables() { return environmentVariables; } public void setEnvironmentVariables(String[] environmentVariables) { this.environmentVariables = environmentVariables; } public EntryPointStyle getEntryPointStyle() { return entryPointStyle; } public void setEntryPointStyle(EntryPointStyle entryPointStyle) { this.entryPointStyle = entryPointStyle; } public boolean isCreateLoadBalancer() { return createLoadBalancer; } public void setCreateLoadBalancer(boolean createLoadBalancer) { this.createLoadBalancer = createLoadBalancer; } public String getServiceAnnotations() { return serviceAnnotations; } public void setServiceAnnotations(String serviceAnnotations) { this.serviceAnnotations = serviceAnnotations; } public String getPodAnnotations() { return podAnnotations; } public void setPodAnnotations(String podAnnotations) { this.podAnnotations = podAnnotations; } public String getJobAnnotations() { return jobAnnotations; } public void setJobAnnotations(String jobAnnotations) { this.jobAnnotations = jobAnnotations; } public int getMinutesToWaitForLoadBalancer() { return minutesToWaitForLoadBalancer; } public void setMinutesToWaitForLoadBalancer(int minutesToWaitForLoadBalancer) { this.minutesToWaitForLoadBalancer = minutesToWaitForLoadBalancer; } public int getMaxTerminatedErrorRestarts() { return maxTerminatedErrorRestarts; } public void setMaxTerminatedErrorRestarts(int maxTerminatedErrorRestarts) { this.maxTerminatedErrorRestarts = maxTerminatedErrorRestarts; } public int getMaxCrashLoopBackOffRestarts() { return maxCrashLoopBackOffRestarts; } public void setMaxCrashLoopBackOffRestarts(int maxCrashLoopBackOffRestarts) { this.maxCrashLoopBackOffRestarts = maxCrashLoopBackOffRestarts; } public ImagePullPolicy getImagePullPolicy() { return imagePullPolicy; } public void setImagePullPolicy(ImagePullPolicy imagePullPolicy) { this.imagePullPolicy = imagePullPolicy; } public LimitsResources getLimits() { return limits; } public void setLimits(LimitsResources limits) { this.limits = limits; } public RequestsResources getRequests() { return requests; } public void setRequests(RequestsResources requests) { this.requests = requests; } public List getVolumeMounts() { return volumeMounts; } public void setVolumeMounts(List volumeMounts) { this.volumeMounts = volumeMounts; } public List getVolumes() { return volumes; } public void setVolumes(List volumes) { this.volumes = volumes; } public boolean isHostNetwork() { return hostNetwork; } public void setHostNetwork(boolean hostNetwork) { this.hostNetwork = hostNetwork; } public boolean isCreateJob() { return createJob; } public void setCreateJob(boolean createJob) { this.createJob = createJob; } public String getDeploymentServiceAccountName() { return deploymentServiceAccountName; } public void setDeploymentServiceAccountName(String deploymentServiceAccountName) { this.deploymentServiceAccountName = deploymentServiceAccountName; } public int getMaximumConcurrentTasks() { return maximumConcurrentTasks; } public void setMaximumConcurrentTasks(int maximumConcurrentTasks) { this.maximumConcurrentTasks = maximumConcurrentTasks; } public Long getTerminationGracePeriodSeconds() { return terminationGracePeriodSeconds; } public void setTerminationGracePeriodSeconds(Long terminationGracePeriodSeconds) { this.terminationGracePeriodSeconds = terminationGracePeriodSeconds; } public void setNodeSelector(String nodeSelector) { this.nodeSelector = nodeSelector; } public String getNodeSelector() { return nodeSelector; } public void setPodSecurityContext(PodSecurityContext podSecurityContext) { this.podSecurityContext = podSecurityContext; } public PodSecurityContext getPodSecurityContext() { return podSecurityContext; } public void setContainerSecurityContext(ContainerSecurityContext containerSecurityContext) { this.containerSecurityContext = containerSecurityContext; } public ContainerSecurityContext getContainerSecurityContext() { return containerSecurityContext; } public NodeAffinity getNodeAffinity() { return nodeAffinity; } public void setNodeAffinity(NodeAffinity nodeAffinity) { this.nodeAffinity = nodeAffinity; } public PodAffinity getPodAffinity() { return podAffinity; } public void setPodAffinity(PodAffinity podAffinity) { this.podAffinity = podAffinity; } public PodAntiAffinity getPodAntiAffinity() { return podAntiAffinity; } public void setPodAntiAffinity(PodAntiAffinity podAntiAffinity) { this.podAntiAffinity = podAntiAffinity; } public String getStatefulSetInitContainerImageName() { return statefulSetInitContainerImageName; } public void setStatefulSetInitContainerImageName(String statefulSetInitContainerImageName) { this.statefulSetInitContainerImageName = statefulSetInitContainerImageName; } public InitContainer getInitContainer() { return initContainer; } public void setInitContainer(InitContainer initContainer) { this.initContainer = initContainer; } public List getInitContainers() { return initContainers; } public void setInitContainers(List initContainers) { this.initContainers = initContainers; } public List getAdditionalContainers() { return this.additionalContainers; } public void setAdditionalContainers(List additionalContainers) { this.additionalContainers = additionalContainers; } public String getLivenessHttpProbeScheme() { return livenessHttpProbeScheme; } public void setLivenessHttpProbeScheme(String livenessHttpProbeScheme) { this.livenessHttpProbeScheme = livenessHttpProbeScheme; } public String getReadinessHttpProbeScheme() { return readinessHttpProbeScheme; } public void setReadinessHttpProbeScheme(String readinessHttpProbeScheme) { this.readinessHttpProbeScheme = readinessHttpProbeScheme; } Lifecycle getLifecycle() { return lifecycle; } void setLifecycle(Lifecycle lifecycle) { this.lifecycle = lifecycle; } public String getDeploymentLabels() { return deploymentLabels; } public void setDeploymentLabels(String deploymentLabels) { this.deploymentLabels = deploymentLabels; } public AppAdmin getAppAdmin() { return appAdmin; } public void setAppAdmin(AppAdmin appAdmin) { this.appAdmin = appAdmin; } public CronConfig getCron() { return cron; } public void setCron(CronConfig cron) { this.cron = cron; } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/KubernetesScheduler.java ================================================ /* * Copyright 2018-2021 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.cloud.deployer.spi.kubernetes; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Pattern; import java.util.stream.Collectors; import io.fabric8.kubernetes.api.model.LocalObjectReference; import io.fabric8.kubernetes.api.model.PodSpec; import io.fabric8.kubernetes.api.model.StatusCause; import io.fabric8.kubernetes.api.model.StatusDetails; import io.fabric8.kubernetes.api.model.batch.v1.CronJob; import io.fabric8.kubernetes.api.model.batch.v1.CronJobBuilder; import io.fabric8.kubernetes.api.model.batch.v1.CronJobList; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.dsl.internal.batch.v1.JobOperationsImpl; import org.springframework.cloud.deployer.spi.scheduler.CreateScheduleException; import org.springframework.cloud.deployer.spi.scheduler.ScheduleInfo; import org.springframework.cloud.deployer.spi.scheduler.ScheduleRequest; import org.springframework.cloud.deployer.spi.scheduler.Scheduler; import org.springframework.cloud.deployer.spi.scheduler.SchedulerException; import org.springframework.cloud.deployer.spi.scheduler.SchedulerPropertyKeys; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; /** * Kubernetes implementation of the {@link Scheduler} SPI. * * @author Chris Schaefer * @author Ilayaperumal Gopinathan * @author Corneil du Plessis */ public class KubernetesScheduler extends AbstractKubernetesDeployer implements Scheduler { protected static final String SPRING_CRONJOB_ID_KEY = "spring-cronjob-id"; private static final String SCHEDULE_EXPRESSION_FIELD_NAME = "spec.schedule"; static final String KUBERNETES_DEPLOYER_CRON_CONCURRENCY_POLICY = KubernetesDeployerProperties.KUBERNETES_DEPLOYER_PROPERTIES_PREFIX + ".cron.concurrencyPolicy"; static final String KUBERNETES_DEPLOYER_CRON_TTL_SECONDS_AFTER_FINISHED = KubernetesDeployerProperties.KUBERNETES_DEPLOYER_PROPERTIES_PREFIX + ".cron.ttlSecondsAfterFinished"; static final String KUBERNETES_DEPLOYER_CRON_BACKOFF_LIMIT = KubernetesDeployerProperties.KUBERNETES_DEPLOYER_PROPERTIES_PREFIX + ".cron.backoffLimit"; public KubernetesScheduler(KubernetesClient client, KubernetesDeployerProperties properties) { Assert.notNull(client, "KubernetesClient must not be null"); Assert.notNull(properties, "KubernetesSchedulerProperties must not be null"); this.client = client; this.properties = properties; this.containerFactory = new DefaultContainerFactory(properties); this.deploymentPropertiesResolver = new DeploymentPropertiesResolver( (properties instanceof KubernetesSchedulerProperties) ? KubernetesSchedulerProperties.KUBERNETES_SCHEDULER_PROPERTIES_PREFIX : KubernetesDeployerProperties.KUBERNETES_DEPLOYER_PROPERTIES_PREFIX, properties); } @Override public void schedule(ScheduleRequest scheduleRequest) { validateScheduleName(scheduleRequest); try { createCronJob(scheduleRequest); } catch (KubernetesClientException e) { String invalidCronExceptionMessage = getExceptionMessageForField(e, SCHEDULE_EXPRESSION_FIELD_NAME); if (StringUtils.hasText(invalidCronExceptionMessage)) { throw new CreateScheduleException(invalidCronExceptionMessage, e); } throw new CreateScheduleException("Failed to create schedule " + scheduleRequest.getScheduleName(), e); } } /** * Merge the Deployment properties into Scheduler properties. * This way, the CronJob's scheduler properties are updated with the deployer properties if set any. * @param scheduleRequest the {@link ScheduleRequest} * @return the merged schedule properties */ static Map mergeSchedulerProperties(ScheduleRequest scheduleRequest) { Map deploymentProperties = new HashMap<>(); Map schedulerProperties = scheduleRequest.getSchedulerProperties(); if(scheduleRequest.getDeploymentProperties() != null) { deploymentProperties.putAll(scheduleRequest.getDeploymentProperties()); } if (schedulerProperties != null) { for (Map.Entry schedulerProperty : schedulerProperties.entrySet()) { String schedulerPropertyKey = schedulerProperty.getKey(); if (StringUtils.hasText(schedulerPropertyKey) && schedulerPropertyKey.startsWith(KubernetesSchedulerProperties.KUBERNETES_SCHEDULER_PROPERTIES_PREFIX)) { String deployerPropertyKey = KubernetesDeployerProperties.KUBERNETES_DEPLOYER_PROPERTIES_PREFIX + schedulerPropertyKey.substring(KubernetesSchedulerProperties.KUBERNETES_SCHEDULER_PROPERTIES_PREFIX.length()); deploymentProperties.put(deployerPropertyKey, schedulerProperty.getValue()); } else if(StringUtils.hasText(schedulerPropertyKey) && schedulerPropertyKey.startsWith(SchedulerPropertyKeys.PREFIX)) { if (!deploymentProperties.containsKey(schedulerPropertyKey)) { deploymentProperties.put(schedulerPropertyKey, schedulerProperty.getValue()); } } } } if(!deploymentProperties.containsKey(KubernetesDeployerProperties.KUBERNETES_DEPLOYER_PROPERTIES_PREFIX + ".restartPolicy")) { deploymentProperties.put(KubernetesDeployerProperties.KUBERNETES_DEPLOYER_PROPERTIES_PREFIX + ".restartPolicy", RestartPolicy.Never.name()); } if(deploymentProperties.containsKey("spring.cloud.deployer.cron.expression")) { deploymentProperties.put(KubernetesDeployerProperties.KUBERNETES_DEPLOYER_PROPERTIES_PREFIX + ".cron.expression", deploymentProperties.get("spring.cloud.deployer.cron.expression")); } Map updatedDeploymentProperties = new HashMap<>(); Map updatedSchedulerProperties = new HashMap<>(); for (Map.Entry schedulerProperty : deploymentProperties.entrySet()) { String schedulerPropertyKey = schedulerProperty.getKey(); if (StringUtils.hasText(schedulerPropertyKey) && schedulerPropertyKey.startsWith(KubernetesSchedulerProperties.KUBERNETES_SCHEDULER_PROPERTIES_PREFIX)) { String deployerPropertyKey = KubernetesDeployerProperties.KUBERNETES_DEPLOYER_PROPERTIES_PREFIX + schedulerPropertyKey.substring(KubernetesSchedulerProperties.KUBERNETES_SCHEDULER_PROPERTIES_PREFIX.length()); updatedSchedulerProperties.put(deployerPropertyKey, schedulerProperty.getValue()); } else { updatedDeploymentProperties.put(schedulerProperty.getKey(), schedulerProperty.getValue()); } } deploymentProperties.clear(); deploymentProperties.putAll(updatedDeploymentProperties); deploymentProperties.putAll(updatedSchedulerProperties); return deploymentProperties; } public void validateScheduleName(ScheduleRequest request) { if(request.getScheduleName() == null) { throw new CreateScheduleException("The name for the schedule request is null", null); } if(request.getScheduleName().length() > 52) { throw new CreateScheduleException(String.format("because Schedule Name: '%s' has too many characters. Schedule name length must be 52 characters or less", request.getScheduleName()), null); } if(!Pattern.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", request.getScheduleName())) { throw new CreateScheduleException("Invalid Format for Schedule Name. Schedule name can only contain lowercase letters, numbers 0-9 and hyphens.", null); } } @Override public void unschedule(String scheduleName) { List unscheduled = this.client.batch().v1().cronjobs().withName(scheduleName).delete(); if (unscheduled == null || unscheduled.isEmpty()) { throw new SchedulerException("Failed to unschedule " + scheduleName); } } @Override public List list(String taskDefinitionName) { return list() .stream() .filter(scheduleInfo -> taskDefinitionName.equals(scheduleInfo.getTaskDefinitionName())) .collect(Collectors.toList()); } @Override public List list() { CronJobList cronJobList = this.client.batch().v1().cronjobs().list(); List cronJobs = cronJobList.getItems(); List scheduleInfos = new ArrayList<>(); for (CronJob cronJob : cronJobs) { if (cronJob.getMetadata() != null && cronJob.getMetadata().getLabels() != null && StringUtils.hasText(cronJob.getMetadata().getLabels().get(SPRING_CRONJOB_ID_KEY))) { Map properties = new HashMap<>(); properties.put(SchedulerPropertyKeys.CRON_EXPRESSION, cronJob.getSpec().getSchedule()); ScheduleInfo scheduleInfo = new ScheduleInfo(); scheduleInfo.setScheduleName(cronJob.getMetadata().getName()); scheduleInfo.setTaskDefinitionName(cronJob.getMetadata().getLabels().get(SPRING_CRONJOB_ID_KEY)); scheduleInfo.setScheduleProperties(properties); scheduleInfos.add(scheduleInfo); } } return scheduleInfos; } protected CronJob createCronJob(ScheduleRequest scheduleRequest) { Map labels = new HashMap<>(); labels.put(SPRING_CRONJOB_ID_KEY, scheduleRequest.getDefinition().getName()); Map schedulerProperties = mergeSchedulerProperties(scheduleRequest); String schedule = schedulerProperties.get("spring.cloud.deployer.kubernetes.cron.expression") != null ? schedulerProperties.get("spring.cloud.deployer.kubernetes.cron.expression") : schedulerProperties.get(SchedulerPropertyKeys.CRON_EXPRESSION); Assert.hasText(schedule, "The property spring.cloud.deployer.cron.expression must be defined"); String concurrencyPolicy = schedulerProperties.get(KUBERNETES_DEPLOYER_CRON_CONCURRENCY_POLICY); // check default server properties if (!StringUtils.hasText(concurrencyPolicy)) { concurrencyPolicy = this.properties.getCron().getConcurrencyPolicy(); } if (concurrencyPolicy == null) { concurrencyPolicy = "Allow"; } final Integer ttlSecondsAfterFinished; String ttlSecondsAfterFinishedString = schedulerProperties.get(KUBERNETES_DEPLOYER_CRON_TTL_SECONDS_AFTER_FINISHED); if (StringUtils.hasText(ttlSecondsAfterFinishedString)) { ttlSecondsAfterFinished = Integer.parseInt(ttlSecondsAfterFinishedString); } else { ttlSecondsAfterFinished = this.properties.getCron().getTtlSecondsAfterFinished(); } final Integer backoffLimit; String backoffLimitString = schedulerProperties.get(KUBERNETES_DEPLOYER_CRON_BACKOFF_LIMIT); if (StringUtils.hasText(backoffLimitString)) { backoffLimit = Integer.parseInt(backoffLimitString); } else { backoffLimit = this.properties.getCron().getBackoffLimit(); } PodSpec podSpec = createPodSpec(new ScheduleRequest(scheduleRequest.getDefinition(),schedulerProperties, scheduleRequest.getCommandlineArguments(), scheduleRequest.getScheduleName(),scheduleRequest.getResource())); String taskServiceAccountName = this.deploymentPropertiesResolver.getTaskServiceAccountName(schedulerProperties); taskServiceAccountName = taskServiceAccountName != null ? taskServiceAccountName : KubernetesDeployerProperties.DEFAULT_TASK_SERVICE_ACCOUNT_NAME; if (StringUtils.hasText(taskServiceAccountName)) { podSpec.setServiceAccountName(taskServiceAccountName); } Map annotations = this.deploymentPropertiesResolver.getPodAnnotations(schedulerProperties); labels.putAll(this.deploymentPropertiesResolver.getDeploymentLabels(schedulerProperties)); CronJob cronJob = new CronJobBuilder() .withNewMetadata() .withName(scheduleRequest.getScheduleName()) .withLabels(labels) .withAnnotations(this.deploymentPropertiesResolver.getJobAnnotations(schedulerProperties)) .endMetadata() .withNewSpec() .withSchedule(schedule) .withConcurrencyPolicy(concurrencyPolicy) .withNewJobTemplate() .withNewSpec() .withBackoffLimit(backoffLimit) .withTtlSecondsAfterFinished(ttlSecondsAfterFinished) .withNewTemplate() .withNewMetadata() .addToAnnotations(annotations).addToLabels(labels) .endMetadata() .withSpec(podSpec) .endTemplate() .endSpec() .endJobTemplate() .endSpec() .build(); setImagePullSecret(scheduleRequest, cronJob); JobOperationsImpl jobOperations = new JobOperationsImpl(this.client); return this.client.batch().v1().cronjobs().inNamespace(jobOperations.getNamespace()).resource(cronJob).create(); } protected String getExceptionMessageForField(KubernetesClientException clientException, String fieldName) { if (clientException.getStatus() == null || clientException.getStatus().getDetails() == null) { return null; } List statusCauses = clientException.getStatus().getDetails().getCauses(); if (!CollectionUtils.isEmpty(statusCauses)) { for (StatusCause statusCause : statusCauses) { if (fieldName.equals(statusCause.getField())) { return clientException.getStatus().getMessage(); } } } return null; } private void setImagePullSecret(ScheduleRequest scheduleRequest, CronJob cronJob) { String imagePullSecret = this.deploymentPropertiesResolver.getImagePullSecret(scheduleRequest.getDeploymentProperties()); if (StringUtils.hasText(imagePullSecret)) { LocalObjectReference localObjectReference = new LocalObjectReference(); localObjectReference.setName(imagePullSecret); cronJob.getSpec().getJobTemplate().getSpec().getTemplate().getSpec().getImagePullSecrets() .add(localObjectReference); } } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/KubernetesSchedulerProperties.java ================================================ /* * Copyright 2018-2021 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.cloud.deployer.spi.kubernetes; import org.springframework.boot.context.properties.ConfigurationProperties; /** * Configuration properties for the Kubernetes Scheduler. * * @author Chris Schaefer */ @Deprecated @ConfigurationProperties(prefix = KubernetesSchedulerProperties.KUBERNETES_SCHEDULER_PROPERTIES_PREFIX) public class KubernetesSchedulerProperties extends KubernetesDeployerProperties { /** * Namespace to use for Kubernetes Scheduler properties. */ public static final String KUBERNETES_SCHEDULER_PROPERTIES_PREFIX = "spring.cloud.scheduler.kubernetes"; /** * The {@link RestartPolicy} to use. Defaults to {@link RestartPolicy#Never}. */ private RestartPolicy restartPolicy = RestartPolicy.Never; /** * The default service account name to use for tasks. */ protected static final String DEFAULT_TASK_SERVICE_ACCOUNT_NAME = "default"; /** * Service account name to use for tasks, defaults to: * {@link KubernetesSchedulerProperties#DEFAULT_TASK_SERVICE_ACCOUNT_NAME} */ private String taskServiceAccountName = DEFAULT_TASK_SERVICE_ACCOUNT_NAME; /** * Obtains the {@link RestartPolicy} to use. Defaults to * {@link KubernetesSchedulerProperties#restartPolicy}. * * @return the {@link RestartPolicy} to use */ public RestartPolicy getRestartPolicy() { return restartPolicy; } /** * Sets the {@link RestartPolicy} to use. * * @param restartPolicy the {@link RestartPolicy} to use */ public void setRestartPolicy(RestartPolicy restartPolicy) { this.restartPolicy = restartPolicy; } /** * Obtains the service account name to use for tasks. * * @return the service account name */ public String getTaskServiceAccountName() { return taskServiceAccountName; } /** * Sets the service account name to use for tasks. * * @param taskServiceAccountName the service account name */ public void setTaskServiceAccountName(String taskServiceAccountName) { this.taskServiceAccountName = taskServiceAccountName; } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/KubernetesTaskLauncher.java ================================================ /* * Copyright 2016-2022 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.cloud.deployer.spi.kubernetes; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.atomic.AtomicInteger; import io.fabric8.kubernetes.api.model.Container; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.KubernetesResourceList; import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.api.model.PodBuilder; import io.fabric8.kubernetes.api.model.PodList; import io.fabric8.kubernetes.api.model.PodSpec; import io.fabric8.kubernetes.api.model.PodStatus; import io.fabric8.kubernetes.api.model.PodTemplateSpec; import io.fabric8.kubernetes.api.model.StatusDetails; import io.fabric8.kubernetes.api.model.batch.v1.Job; import io.fabric8.kubernetes.api.model.batch.v1.JobBuilder; import io.fabric8.kubernetes.api.model.batch.v1.JobList; import io.fabric8.kubernetes.api.model.batch.v1.JobSpec; import io.fabric8.kubernetes.api.model.batch.v1.JobSpecBuilder; import io.fabric8.kubernetes.api.model.batch.v1.JobStatus; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable; import io.fabric8.kubernetes.client.dsl.PodResource; import io.fabric8.kubernetes.client.dsl.ScalableResource; import io.fabric8.kubernetes.client.dsl.internal.batch.v1.JobOperationsImpl; import io.fabric8.kubernetes.client.dsl.internal.core.v1.PodOperationsImpl; import org.hashids.Hashids; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; import org.springframework.cloud.deployer.spi.kubernetes.support.PropertyParserUtils; import org.springframework.cloud.deployer.spi.task.LaunchState; import org.springframework.cloud.deployer.spi.task.TaskLauncher; import org.springframework.cloud.deployer.spi.task.TaskStatus; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** * A task launcher that targets Kubernetes. * * @author Thomas Risberg * @author David Turanski * @author Leonardo Diniz * @author Chris Schaefer * @author Ilayaperumal Gopinathan * @author Glenn Renfro * @author Corneil du Plessis */ public class KubernetesTaskLauncher extends AbstractKubernetesDeployer implements TaskLauncher { private final KubernetesTaskLauncherProperties taskLauncherProperties; public KubernetesTaskLauncher(KubernetesDeployerProperties properties, KubernetesClient client) { this(properties, new KubernetesTaskLauncherProperties(), client, new DefaultContainerFactory(properties)); } public KubernetesTaskLauncher(KubernetesDeployerProperties properties, KubernetesClient client, ContainerFactory containerFactory) { this(properties, new KubernetesTaskLauncherProperties(), client, containerFactory); } public KubernetesTaskLauncher(KubernetesDeployerProperties deployerProperties, KubernetesTaskLauncherProperties taskLauncherProperties, KubernetesClient client) { this(deployerProperties, taskLauncherProperties, client, new DefaultContainerFactory(deployerProperties)); } public KubernetesTaskLauncher(KubernetesDeployerProperties kubernetesDeployerProperties, KubernetesTaskLauncherProperties taskLauncherProperties, KubernetesClient client, ContainerFactory containerFactory) { this.properties = kubernetesDeployerProperties; this.taskLauncherProperties = taskLauncherProperties; this.client = client; this.containerFactory = containerFactory; this.deploymentPropertiesResolver = new DeploymentPropertiesResolver( KubernetesDeployerProperties.KUBERNETES_DEPLOYER_PROPERTIES_PREFIX, properties); } @Override public String launch(AppDeploymentRequest request) { String appId = createDeploymentId(request); TaskStatus status = status(appId); if (!status.getState().equals(LaunchState.unknown)) { throw new IllegalStateException("Task " + appId + " already exists with a state of " + status); } if (this.maxConcurrentExecutionsReached()) { throw new IllegalStateException( String.format("Cannot launch task %s. The maximum concurrent task executions is at its limit [%d].", request.getDefinition().getName(), this.getMaximumConcurrentTasks()) ); } logPossibleDownloadResourceMessage(request.getResource()); try { launch(appId, request); return appId; } catch (RuntimeException e) { logger.error(e.getMessage(), e); throw e; } } @Override public void cancel(String id) { logger.debug(String.format("Cancelling task: %s", id)); //ToDo: what does cancel mean? Kubernetes doesn't have stop - just cleanup cleanup(id); } @Override public void cleanup(String id) { try { if (properties.isCreateJob()) { deleteJob(id); } else { deletePod(id); } } catch (RuntimeException e) { logger.error(e.getMessage(), e); throw e; } } @Override public void destroy(String appName) { for (String id : getIdsForTasks(Optional.of(appName), properties.isCreateJob())) { cleanup(id); } } @Override public RuntimeEnvironmentInfo environmentInfo() { return super.createRuntimeEnvironmentInfo(TaskLauncher.class, this.getClass()); } @Override public TaskStatus status(String id) { TaskStatus status = buildTaskStatus(id); logger.debug(String.format("Status for task: %s is %s", id, status)); return status; } @Override public int getMaximumConcurrentTasks() { return this.properties.getMaximumConcurrentTasks(); } @Override public int getRunningTaskExecutionCount() { List taskIds = getIdsForTasks(Optional.empty(), false); AtomicInteger executionCount = new AtomicInteger(); taskIds.forEach(id -> { if (buildPodStatus(id).getState() == LaunchState.running) { executionCount.incrementAndGet(); } }); return executionCount.get(); } @Override public String getLog(String id) { if (properties.isCreateJob()) { Job job = getJob(id); Assert.notNull(job, "Expected job"); Map selector = new HashMap<>(); selector.put(SPRING_APP_KEY, id); selector.put("job-name", job.getMetadata().getName()); return getLogFromSelector(selector); } else { Map selector = new HashMap<>(); selector.put(SPRING_APP_KEY, id); return getLogFromSelector(selector); } } private String getLogFromSelector(Map selector) { PodList podList = client.pods().withLabels(selector).list(); StringBuilder logAppender = new StringBuilder(); for (Pod pod : podList.getItems()) { for (Container container : pod.getSpec().getContainers()) { logAppender.append(this.client.pods().withName(pod.getMetadata().getName()) .inContainer(container.getName()).tailingLines(500).getLog()); } } return logAppender.toString(); } private boolean maxConcurrentExecutionsReached() { return this.getRunningTaskExecutionCount() >= this.getMaximumConcurrentTasks(); } protected String createDeploymentId(AppDeploymentRequest request) { String name = request.getDefinition().getName(); Hashids hashids = new Hashids(name, 0, "abcdefghijklmnopqrstuvwxyz1234567890"); long idToEncode = System.currentTimeMillis() + ThreadLocalRandom.current().nextInt(1000); String hashid = hashids.encode(idToEncode); String deploymentId = name + "-" + hashid; // Kubernetes does not allow . in the name and does not allow uppercase in the name return deploymentId.replace('.', '-').toLowerCase(Locale.ROOT); } private synchronized void launch(String appId, AppDeploymentRequest request) { Map idMap = createIdMap(appId, request); Map podLabelMap = new HashMap<>(); podLabelMap.put("task-name", request.getDefinition().getName()); podLabelMap.put(SPRING_MARKER_KEY, SPRING_MARKER_VALUE); Map deploymentProperties = request.getDeploymentProperties(); Map deploymentLabels = this.deploymentPropertiesResolver.getDeploymentLabels(deploymentProperties); if (!CollectionUtils.isEmpty(deploymentLabels)) { logger.debug(String.format("Adding deploymentLabels: %s", deploymentLabels)); } PodSpec podSpec = createPodSpec(request); podSpec.setRestartPolicy(getRestartPolicy(request).name()); if (this.properties.isCreateJob()) { logger.debug(String.format("Launching Job for task: %s", appId)); ObjectMeta objectMeta = new ObjectMetaBuilder() .withLabels(podLabelMap) .addToLabels(idMap) .addToLabels(deploymentLabels) .withAnnotations(this.deploymentPropertiesResolver.getJobAnnotations(deploymentProperties)) .addToAnnotations(this.deploymentPropertiesResolver.getPodAnnotations(deploymentProperties)) .build(); PodTemplateSpec podTemplateSpec = new PodTemplateSpec(objectMeta, podSpec); JobSpec jobSpec = new JobSpecBuilder() .withTemplate(podTemplateSpec) .withBackoffLimit(getBackoffLimit(request)) .withTtlSecondsAfterFinished(getTtlSecondsAfterFinished(request)) .build(); JobOperationsImpl jobOperations = new JobOperationsImpl(this.client); this.client.batch().v1().jobs().inNamespace(jobOperations.getNamespace()).resource(new JobBuilder() .withNewMetadata() .withName(appId) .withLabels(Collections.singletonMap("task-name", podLabelMap.get("task-name"))) .addToLabels(idMap) .withAnnotations(this.deploymentPropertiesResolver.getJobAnnotations(deploymentProperties)) .endMetadata() .withSpec(jobSpec) .build() ).create(); ; } else { logger.debug(String.format("Launching Pod for task: %s", appId)); PodOperationsImpl podOperations = new PodOperationsImpl(this.client); this.client.pods().inNamespace(podOperations.getNamespace()).resource( new PodBuilder() .withNewMetadata() .withName(appId) .withLabels(podLabelMap) .addToLabels(deploymentLabels) .withAnnotations(this.deploymentPropertiesResolver.getJobAnnotations(deploymentProperties)) .addToAnnotations(this.deploymentPropertiesResolver.getPodAnnotations(deploymentProperties)) .addToLabels(idMap) .endMetadata() .withSpec(podSpec) .build() ).create(); } } private List getIdsForTasks(Optional taskName, boolean isCreateJob) { List ids = new ArrayList<>(); try { KubernetesResourceList resourceList = getTaskResources(taskName, isCreateJob); for (HasMetadata hasMetadata : resourceList.getItems()) { ids.add(hasMetadata.getMetadata().getName()); } } catch (KubernetesClientException kce) { logger.warn(String.format("Failed to retrieve pods for task: %s", taskName), kce); } return ids; } private KubernetesResourceList getTaskResources(Optional taskName, boolean isCreateJob) { KubernetesResourceList resourceList; if (taskName.isPresent()) { if (isCreateJob) { resourceList = client.batch().v1().jobs().withLabel("task-name", taskName.get()).list(); } else { resourceList = client.pods().withLabel("task-name", taskName.get()).list(); } } else { if (isCreateJob) { resourceList = client.batch().v1().jobs().withLabel("task-name").list(); } else { resourceList = client.pods().withLabel("task-name").list(); } } return resourceList; } TaskStatus buildTaskStatus(String id) { if (properties.isCreateJob()) { Job job = getJob(id); if (job == null) { return new TaskStatus(id, LaunchState.unknown, new HashMap<>()); } JobStatus jobStatus = job.getStatus(); if (jobStatus == null) { return new TaskStatus(id, LaunchState.unknown, new HashMap<>()); } boolean failed = jobStatus.getFailed() != null && jobStatus.getFailed() > 0; boolean succeeded = jobStatus.getSucceeded() != null && jobStatus.getSucceeded() > 0; if (failed) { return new TaskStatus(id, LaunchState.failed, new HashMap<>()); } if (succeeded) { return new TaskStatus(id, LaunchState.complete, new HashMap<>()); } return new TaskStatus(id, LaunchState.launching, new HashMap<>()); } else { return buildPodStatus(id); } } private TaskStatus buildPodStatus(String id) { Pod pod = getPodByName(id); if (pod == null) { return new TaskStatus(id, LaunchState.unknown, new HashMap<>()); } PodStatus podStatus = pod.getStatus(); if (podStatus == null) { return new TaskStatus(id, LaunchState.unknown, new HashMap<>()); } String phase = podStatus.getPhase(); return switch (phase) { case "Pending" -> new TaskStatus(id, LaunchState.launching, new HashMap<>()); case "Failed" -> new TaskStatus(id, LaunchState.failed, new HashMap<>()); case "Succeeded" -> new TaskStatus(id, LaunchState.complete, new HashMap<>()); default -> new TaskStatus(id, LaunchState.running, new HashMap<>()); }; } private void deleteJob(String id) { FilterWatchListDeletable> jobsToDelete = client.batch().v1().jobs() .withLabel(SPRING_APP_KEY, id); if (jobsToDelete == null || ObjectUtils.isEmpty(jobsToDelete.list().getItems())) { logger.warn(String.format("Cannot delete job for task \"%s\" (reason: job does not exist)", id)); } else { logger.debug(String.format("Deleting job for task: %s", id)); List deleted = jobsToDelete.delete(); if(logger.isDebugEnabled()) { logger.debug(String.format("Job was%s deleted for task: %s", id, (deleted != null && !deleted.isEmpty() ? "" : " not"))); } } } private void deletePod(String id) { FilterWatchListDeletable podsToDelete = client.pods() .withLabel(SPRING_APP_KEY, id); if (podsToDelete == null || ObjectUtils.isEmpty(podsToDelete.list().getItems())) { logger.warn(String.format("Cannot delete pod for task \"%s\" (reason: pod does not exist)", id)); } else { logger.debug(String.format("Deleting pod for task: %s", id)); List deleted = podsToDelete.delete(); if(logger.isDebugEnabled()) { logger.debug(String.format("Pod was%s deleted for task: %s", id, (deleted != null && !deleted.isEmpty() ? "" : " not"))); } } } private Job getJob(String jobName) { List jobs = client.batch().v1().jobs().withLabel(SPRING_APP_KEY, jobName).list().getItems(); for (Job job : jobs) { if (jobName.equals(job.getMetadata().getName())) { return job; } } return null; } private Pod getPodByName(String name) { PodResource podResource = client.pods().withName(name); return podResource == null ? null : client.pods().withName(name).get(); } /** * Get the RestartPolicy setting for the deployment request. * * @param request The deployment request. * @return Whether RestartPolicy is requested */ protected RestartPolicy getRestartPolicy(AppDeploymentRequest request) { String restartPolicyString = PropertyParserUtils.getDeploymentPropertyValue(request.getDeploymentProperties(), "spring.cloud.deployer.kubernetes.restartPolicy"); RestartPolicy restartPolicy = (!StringUtils.hasText(restartPolicyString)) ? this.taskLauncherProperties.getRestartPolicy() : RestartPolicy.valueOf(restartPolicyString); if (this.properties.isCreateJob()) { Assert.isTrue(!restartPolicy.equals(RestartPolicy.Always), "RestartPolicy should not be 'Always' when the JobSpec is used."); } return restartPolicy; } /** * Get the BackoffLimit setting for the deployment request. * * @param request The deployment request. * @return the backoffLimit */ protected Integer getBackoffLimit(AppDeploymentRequest request) { String backoffLimitString = PropertyParserUtils.getDeploymentPropertyValue(request.getDeploymentProperties(), "spring.cloud.deployer.kubernetes.backoffLimit"); if (StringUtils.hasText(backoffLimitString)) { return Integer.valueOf(backoffLimitString); } else { return this.taskLauncherProperties.getBackoffLimit(); } } /** * Get the ttlSecondsAfterFinihsed setting for the deployment request. * * @param request The deployment request. * @return the ttlSecondsAfterFinished */ protected Integer getTtlSecondsAfterFinished(AppDeploymentRequest request) { String ttlSecondsAfterFinished = PropertyParserUtils.getDeploymentPropertyValue(request.getDeploymentProperties(), "spring.cloud.deployer.kubernetes.ttlSecondsAfterFinished"); if (StringUtils.hasText(ttlSecondsAfterFinished)) { return Integer.valueOf(ttlSecondsAfterFinished); } else { return this.taskLauncherProperties.getTtlSecondsAfterFinished(); } } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/KubernetesTaskLauncherProperties.java ================================================ /* * Copyright 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.cloud.deployer.spi.kubernetes; import org.springframework.boot.context.properties.ConfigurationProperties; /** * Configuration properties for the Kubernetes Task Launcher. * * @author Ilayaperumal Gopinathan */ @ConfigurationProperties(prefix = "spring.cloud.deployer.kubernetes") public class KubernetesTaskLauncherProperties { /** * The {@link RestartPolicy} to use. Defaults to {@link RestartPolicy#Never}. */ private RestartPolicy restartPolicy = RestartPolicy.Never; /** * The backoff limit to specify the number of retries before considering a Job as failed. */ private Integer backoffLimit; /** * The number of seconds after a job has finished before it is eligible to be automatically * removed by the TTL controller - note that logs from removed jobs will not be able to * be retrieved. */ private Integer ttlSecondsAfterFinished; /** * Obtains the {@link RestartPolicy} to use. Defaults to * {@link KubernetesTaskLauncherProperties#restartPolicy}. * * @return the {@link RestartPolicy} to use */ public RestartPolicy getRestartPolicy() { return restartPolicy; } /** * Sets the {@link RestartPolicy} to use. * * @param restartPolicy the {@link RestartPolicy} to use */ public void setRestartPolicy(RestartPolicy restartPolicy) { this.restartPolicy = restartPolicy; } /** * Get the BackoffLimit value * @return the integer value of BackoffLimit */ public Integer getBackoffLimit() { return backoffLimit; } /** * Sets the BackoffLimit. * * @param backoffLimit the integer value of BackoffLimit */ public void setBackoffLimit(Integer backoffLimit) { this.backoffLimit = backoffLimit; } /** * Get the ttlSecondsAfterFinished value * @return the integer value of ttlSecondsAfterFinished */ public Integer getTtlSecondsAfterFinished() { return ttlSecondsAfterFinished; } /** * Sets the ttlSecondsAfterFinished. * * @param ttlSecondsAfterFinished the integer value of ttlSecondsAfterFinished */ public void setTtlSecondsAfterFinished(Integer ttlSecondsAfterFinished) { this.ttlSecondsAfterFinished = ttlSecondsAfterFinished; } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/LivenessCommandProbeCreator.java ================================================ /* * Copyright 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.cloud.deployer.spi.kubernetes; import org.springframework.cloud.deployer.spi.util.CommandLineTokenizer; import org.springframework.util.StringUtils; /** * Creates a command based liveness probe * * @author Chris Schaefer * @author Corneil du Plessis * @since 2.5 */ class LivenessCommandProbeCreator extends CommandProbeCreator { LivenessCommandProbeCreator(KubernetesDeployerProperties kubernetesDeployerProperties, ContainerConfiguration containerConfiguration) { super(kubernetesDeployerProperties, containerConfiguration); } @Override int getInitialDelay() { return getProbeIntProperty(LIVENESS_DEPLOYER_PROPERTY_PREFIX, "Command", "ProbeDelay", getKubernetesDeployerProperties().getLivenessCommandProbeDelay()); } @Override int getPeriod() { return getProbeIntProperty(LIVENESS_DEPLOYER_PROPERTY_PREFIX, "Command", "ProbePeriod", getKubernetesDeployerProperties().getLivenessCommandProbePeriod()); } @Override int getFailure() { return getProbeIntProperty(LIVENESS_DEPLOYER_PROPERTY_PREFIX, "Command", "ProbeFailure", getKubernetesDeployerProperties().getLivenessCommandProbeFailure()); } @Override int getSuccess() { return getProbeIntProperty(LIVENESS_DEPLOYER_PROPERTY_PREFIX, "Command", "ProbeSuccess", getKubernetesDeployerProperties().getLivenessCommandProbeSuccess()); } @Override String[] getCommand() { String probeCommandValue = getDeploymentPropertyValue(LIVENESS_DEPLOYER_PROPERTY_PREFIX + "CommandProbeCommand"); if (StringUtils.hasText(probeCommandValue)) { return new CommandLineTokenizer(probeCommandValue).getArgs().toArray(new String[0]); } if (getKubernetesDeployerProperties().getLivenessCommandProbeCommand() != null) { return new CommandLineTokenizer(getKubernetesDeployerProperties().getLivenessCommandProbeCommand()) .getArgs().toArray(new String[0]); } throw new IllegalArgumentException("The livenessCommandProbeCommand property must be set."); } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/LivenessHttpProbeCreator.java ================================================ /* * Copyright 2018-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.cloud.deployer.spi.kubernetes; import org.springframework.util.StringUtils; /** * Creates an HTTP Liveness probe * * @author Chris Schaefer * @author Ilayaperumal Gopinathan * @author Corneil du Plessis */ class LivenessHttpProbeCreator extends HttpProbeCreator { LivenessHttpProbeCreator(KubernetesDeployerProperties kubernetesDeployerProperties, ContainerConfiguration containerConfiguration) { super(kubernetesDeployerProperties, containerConfiguration); } @Override public Integer getPort() { String probePortValue = getProbeProperty(LIVENESS_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbePort"); if (StringUtils.hasText(probePortValue)) { return Integer.parseInt(probePortValue); } if (getKubernetesDeployerProperties().getLivenessHttpProbePort() != null) { return getKubernetesDeployerProperties().getLivenessHttpProbePort(); } if (getDefaultPort() != null) { return getDefaultPort(); } return null; } @Override protected String getProbePath() { String probePathValue = getProbeProperty(LIVENESS_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbePath", getKubernetesDeployerProperties().getLivenessHttpProbePath()); if (StringUtils.hasText(probePathValue)) { return probePathValue; } if (useBoot1ProbePath()) { return BOOT_1_LIVENESS_PROBE_PATH; } return BOOT_2_LIVENESS_PROBE_PATH; } @Override protected String getScheme() { String probeSchemeValue = getProbeProperty(LIVENESS_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbeScheme", getKubernetesDeployerProperties().getLivenessHttpProbeScheme()); if (StringUtils.hasText(probeSchemeValue)) { return probeSchemeValue; } return DEFAULT_PROBE_SCHEME; } @Override protected int getTimeout() { return getProbeIntProperty(LIVENESS_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbeTimeout", getKubernetesDeployerProperties().getLivenessHttpProbeTimeout()); } @Override protected int getInitialDelay() { return getProbeIntProperty(LIVENESS_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbeDelay", getKubernetesDeployerProperties().getLivenessHttpProbeDelay()); } @Override protected int getPeriod() { return getProbeIntProperty(LIVENESS_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbePeriod", getKubernetesDeployerProperties().getLivenessHttpProbePeriod()); } @Override int getFailure() { return getProbeIntProperty(LIVENESS_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbeFailure", getKubernetesDeployerProperties().getLivenessHttpProbeFailure()); } @Override int getSuccess() { return getProbeIntProperty(LIVENESS_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbeSuccess", getKubernetesDeployerProperties().getLivenessHttpProbeSuccess()); } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/LivenessTcpProbeCreator.java ================================================ /* * Copyright 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.cloud.deployer.spi.kubernetes; import org.springframework.util.StringUtils; /** * Creates a TCP liveness probe * * @author Chris Schaefer * @author Corneil du Plessis * @since 2.5 */ class LivenessTcpProbeCreator extends TcpProbeCreator { LivenessTcpProbeCreator(KubernetesDeployerProperties kubernetesDeployerProperties, ContainerConfiguration containerConfiguration) { super(kubernetesDeployerProperties, containerConfiguration); } @Override int getInitialDelay() { return getProbeIntProperty(LIVENESS_DEPLOYER_PROPERTY_PREFIX, "Tcp", "ProbeDelay", getKubernetesDeployerProperties().getLivenessTcpProbeDelay()); } @Override int getPeriod() { return getProbeIntProperty(LIVENESS_DEPLOYER_PROPERTY_PREFIX, "Tcp", "ProbePeriod", getKubernetesDeployerProperties().getLivenessTcpProbePeriod()); } @Override protected int getTimeout() { return getProbeIntProperty(LIVENESS_DEPLOYER_PROPERTY_PREFIX, "Tcp", "ProbeTimeout", getKubernetesDeployerProperties().getLivenessTcpProbeTimeout()); } @Override Integer getPort() { String probePortValue = getProbeProperty(LIVENESS_DEPLOYER_PROPERTY_PREFIX, "Tcp", "ProbePort"); if (StringUtils.hasText(probePortValue)) { if (!probePortValue.chars().allMatch(Character::isDigit)) { throw new IllegalArgumentException("LivenessTcpProbePort must contain all digits"); } return Integer.parseInt(probePortValue); } if (getKubernetesDeployerProperties().getLivenessTcpProbePort() != null) { return getKubernetesDeployerProperties().getLivenessTcpProbePort(); } throw new IllegalArgumentException("The livenessTcpProbePort property must be set."); } @Override int getFailure() { return getProbeIntProperty(LIVENESS_DEPLOYER_PROPERTY_PREFIX, "Tcp", "ProbeFailure", getKubernetesDeployerProperties().getLivenessTcpProbeFailure()); } @Override int getSuccess() { return getProbeIntProperty(LIVENESS_DEPLOYER_PROPERTY_PREFIX, "Tcp", "ProbeSuccess", getKubernetesDeployerProperties().getLivenessTcpProbeSuccess()); } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/PredicateRunningPhaseDeploymentStateResolver.java ================================================ /* * Copyright 2017 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.cloud.deployer.spi.kubernetes; import java.util.function.Predicate; import java.util.stream.Stream; import io.fabric8.kubernetes.api.model.ContainerStatus; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.cloud.deployer.spi.app.DeploymentState; /** * @author David Turanski **/ public class PredicateRunningPhaseDeploymentStateResolver implements RunningPhaseDeploymentStateResolver { private static Log logger = LogFactory.getLog(PredicateRunningPhaseDeploymentStateResolver.class); private final ContainerStatusCondition[] conditions; private final DeploymentState resolvedState; protected final KubernetesDeployerProperties properties; PredicateRunningPhaseDeploymentStateResolver( KubernetesDeployerProperties properties, DeploymentState resolvedState, ContainerStatusCondition... conditions) { this.conditions = conditions; this.resolvedState = resolvedState; this.properties = properties; } public DeploymentState resolve(ContainerStatus containerStatus) { Stream> conditionsStream = Stream.of(conditions); Boolean allConditionsMet = conditionsStream.reduce((x, y) -> x.and(y)).get().test(containerStatus); if (allConditionsMet) { logger.debug("deployment state is " + resolvedState.name()); return this.resolvedState; } else { Stream report = Stream.of(conditions); report.filter(c -> { boolean result= false; try { result = c.test(containerStatus); } catch (NullPointerException e) { } return !result; }).forEach(c -> logger.debug(c + " is not satisfied")); } return null; } static abstract class ContainerStatusCondition implements Predicate { private final String description; ContainerStatusCondition(String description) { this.description = description; } public String toString() { String className = this.getClass().getName(); return className.substring(className.lastIndexOf(".") + 1) + ":" + description; } } static class ContainerReady extends PredicateRunningPhaseDeploymentStateResolver { ContainerReady(KubernetesDeployerProperties properties) { super(properties, DeploymentState.deployed, new ContainerStatusCondition("container ready") { @Override public boolean test(ContainerStatus containerStatus) { return containerStatus.getReady(); } }); } } static class ContainerCrashed extends PredicateRunningPhaseDeploymentStateResolver { ContainerCrashed(KubernetesDeployerProperties properties) { super(properties, DeploymentState.failed, new ContainerStatusCondition("restart count > maxTerminatedErrorRestarts") { @Override public boolean test(ContainerStatus containerStatus) { return containerStatus.getRestartCount() > properties.getMaxTerminatedErrorRestarts(); } }, new ContainerStatusCondition("exit code in (1, 137, 143)") { @Override public boolean test(ContainerStatus containerStatus) { // if we are being killed repeatedly due to OOM or using too much CPU, or abnormal termination. return containerStatus.getLastState() != null && containerStatus.getLastState().getTerminated() != null && (containerStatus.getLastState().getTerminated().getExitCode() == 137 || containerStatus.getLastState().getTerminated().getExitCode() == 143 || containerStatus.getLastState().getTerminated().getExitCode() == 1); } }); } } // if we are being restarted repeatedly due to the same error, consider the app crashed static class RestartsDueToTheSameError extends PredicateRunningPhaseDeploymentStateResolver { RestartsDueToTheSameError(KubernetesDeployerProperties properties) { super(properties, DeploymentState.failed, new ContainerStatusCondition("restart count > " + "maxTerminatedErrorRestarts") { @Override public boolean test(ContainerStatus containerStatus) { return containerStatus.getRestartCount() > properties.getMaxTerminatedErrorRestarts(); } }, new ContainerStatusCondition("last state termination reason == 'Error' and termination reason == " + "'Error'") { public boolean test(ContainerStatus containerStatus) { return containerStatus.getLastState() != null && containerStatus.getState() != null && containerStatus.getLastState().getTerminated() != null && containerStatus.getLastState().getTerminated().getReason() != null && containerStatus.getLastState().getTerminated().getReason().contains("Error") && containerStatus.getState().getTerminated() != null && containerStatus.getState().getTerminated().getReason() != null && containerStatus.getState().getTerminated().getReason().contains("Error"); } }, new ContainerStatusCondition("last state exit code == exit code") { @Override public boolean test(ContainerStatus containerStatus) { return containerStatus.getLastState().getTerminated().getExitCode().equals( containerStatus.getState().getTerminated().getExitCode()); } }); } } static class CrashLoopBackOffRestarts extends PredicateRunningPhaseDeploymentStateResolver { CrashLoopBackOffRestarts(KubernetesDeployerProperties properties) { super(properties, DeploymentState.failed, new ContainerStatusCondition("restart count > " + "CrashLoopBackOffRestarts") { @Override public boolean test(ContainerStatus containerStatus) { return containerStatus.getRestartCount() > properties.getMaxCrashLoopBackOffRestarts(); } }, new ContainerStatusCondition("waiting in CrashLoopBackOff") { @Override public boolean test(ContainerStatus containerStatus) { return containerStatus.getLastState() != null && containerStatus.getState() != null && containerStatus.getLastState().getTerminated() != null && containerStatus.getState().getWaiting() != null && containerStatus.getState().getWaiting().getReason() != null && containerStatus.getState().getWaiting().getReason().contains("CrashLoopBackOff"); } }); } } static class ContainerTerminated extends PredicateRunningPhaseDeploymentStateResolver { ContainerTerminated(KubernetesDeployerProperties properties) { super(properties, DeploymentState.undeployed, new ContainerStatusCondition("restart count == 0") { @Override public boolean test(ContainerStatus containerStatus) { return containerStatus.getRestartCount() == 0; } }, new ContainerStatusCondition("state is terminated") { @Override public boolean test(ContainerStatus containerStatus) { return containerStatus.getState() != null && containerStatus.getState().getTerminated() != null; } }); } } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/ProbeAuthenticationType.java ================================================ /* * Copyright 2018 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.cloud.deployer.spi.kubernetes; /** * Defines supported authentication types to use when accessing secured * probe endpoints. * * @author Chris Schaefer */ public enum ProbeAuthenticationType { Basic } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/ProbeCreator.java ================================================ /* * Copyright 2018-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.cloud.deployer.spi.kubernetes; import java.util.Map; import io.fabric8.kubernetes.api.model.Probe; import org.springframework.cloud.deployer.spi.kubernetes.support.PropertyParserUtils; import org.springframework.util.StringUtils; /** * Base class for creating Probe's * * @author Chris Schaefer * @author Ilayaperumal Gopinathan */ abstract class ProbeCreator { static final String KUBERNETES_DEPLOYER_PREFIX = "spring.cloud.deployer.kubernetes"; static final String LIVENESS_DEPLOYER_PROPERTY_PREFIX = KUBERNETES_DEPLOYER_PREFIX + ".liveness"; static final String READINESS_DEPLOYER_PROPERTY_PREFIX = KUBERNETES_DEPLOYER_PREFIX + ".readiness"; static final String STARTUP_DEPLOYER_PROPERTY_PREFIX = KUBERNETES_DEPLOYER_PREFIX + ".startup"; private ContainerConfiguration containerConfiguration; private KubernetesDeployerProperties kubernetesDeployerProperties; ProbeCreator(KubernetesDeployerProperties kubernetesDeployerProperties, ContainerConfiguration containerConfiguration) { this.containerConfiguration = containerConfiguration; this.kubernetesDeployerProperties = kubernetesDeployerProperties; } abstract Probe create(); abstract int getInitialDelay(); abstract int getPeriod(); abstract int getFailure(); abstract int getSuccess(); KubernetesDeployerProperties getKubernetesDeployerProperties() { return kubernetesDeployerProperties; } private Map getDeploymentProperties() { return this.containerConfiguration.getAppDeploymentRequest().getDeploymentProperties(); } protected String getDeploymentPropertyValue(String propertyName) { return PropertyParserUtils.getDeploymentPropertyValue(getDeploymentProperties(), propertyName); } protected String getDeploymentPropertyValue(String propertyName, String defaultValue) { return PropertyParserUtils.getDeploymentPropertyValue(getDeploymentProperties(), propertyName, defaultValue); } ContainerConfiguration getContainerConfiguration() { return containerConfiguration; } // used to resolve deprecated HTTP probe property names that do not include "Http" in them // can be removed when deprecated HTTP probes without "Http" in them get removed String getProbeProperty(String propertyPrefix, String probeName, String propertySuffix) { String defaultValue = getDeploymentPropertyValue(propertyPrefix + probeName + propertySuffix); return StringUtils.hasText(defaultValue) ? defaultValue : getDeploymentPropertyValue(propertyPrefix + propertySuffix); } String getProbeProperty(String propertyPrefix, String probeName, String propertySuffix, String defaultValue) { return getDeploymentPropertyValue(propertyPrefix + probeName + propertySuffix, getDeploymentPropertyValue(propertyPrefix + propertySuffix, defaultValue) ); } int getProbeIntProperty(String propertyPrefix, String probeName, String propertySuffix, int defaultValue) { String propertyValue = getDeploymentPropertyValue(propertyPrefix + probeName + propertySuffix, getDeploymentPropertyValue(propertyPrefix + propertySuffix) ); return StringUtils.hasText(propertyValue) ? Integer.parseInt(propertyValue) : defaultValue; } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/ProbeCreatorFactory.java ================================================ /* * Copyright 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.cloud.deployer.spi.kubernetes; import io.fabric8.kubernetes.api.model.Probe; /** * Creates health check probes * * @author Chris Schaefer * @since 2.5 */ class ProbeCreatorFactory { static Probe createStartupProbe(ContainerConfiguration containerConfiguration, KubernetesDeployerProperties kubernetesDeployerProperties, ProbeType probeType) { switch (probeType) { case HTTP: return new StartupHttpProbeCreator(kubernetesDeployerProperties, containerConfiguration).create(); case TCP: return new StartupTcpProbeCreator(kubernetesDeployerProperties, containerConfiguration).create(); case COMMAND: return new StartupCommandProbeCreator(kubernetesDeployerProperties, containerConfiguration).create(); default: throw new IllegalArgumentException("Unknown startup probe type: " + probeType); } } static Probe createReadinessProbe(ContainerConfiguration containerConfiguration, KubernetesDeployerProperties kubernetesDeployerProperties, ProbeType probeType) { switch (probeType) { case HTTP: return new ReadinessHttpProbeCreator(kubernetesDeployerProperties, containerConfiguration).create(); case TCP: return new ReadinessTcpProbeCreator(kubernetesDeployerProperties, containerConfiguration).create(); case COMMAND: return new ReadinessCommandProbeCreator(kubernetesDeployerProperties, containerConfiguration).create(); default: throw new IllegalArgumentException("Unknown readiness probe type: " + probeType); } } static Probe createLivenessProbe(ContainerConfiguration containerConfiguration, KubernetesDeployerProperties kubernetesDeployerProperties, ProbeType probeType) { switch (probeType) { case HTTP: return new LivenessHttpProbeCreator(kubernetesDeployerProperties, containerConfiguration).create(); case TCP: return new LivenessTcpProbeCreator(kubernetesDeployerProperties, containerConfiguration).create(); case COMMAND: return new LivenessCommandProbeCreator(kubernetesDeployerProperties, containerConfiguration).create(); default: throw new IllegalArgumentException("Unknown liveness probe type: " + probeType); } } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/ProbeType.java ================================================ /* * Copyright 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.cloud.deployer.spi.kubernetes; /** * Defines probe types. * * @author Chris Schaefer * @since 2.5 */ enum ProbeType { HTTP, TCP, COMMAND } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/ReadinessCommandProbeCreator.java ================================================ /* * Copyright 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.cloud.deployer.spi.kubernetes; import org.springframework.cloud.deployer.spi.util.CommandLineTokenizer; import org.springframework.util.StringUtils; /** * Creates a command based readiness probe * * @author Chris Schaefer * @author Corneil du Plessis * @since 2.5 */ class ReadinessCommandProbeCreator extends CommandProbeCreator { ReadinessCommandProbeCreator(KubernetesDeployerProperties kubernetesDeployerProperties, ContainerConfiguration containerConfiguration) { super(kubernetesDeployerProperties, containerConfiguration); } @Override int getInitialDelay() { return getProbeIntProperty(READINESS_DEPLOYER_PROPERTY_PREFIX, "Command", "ProbeDelay", getKubernetesDeployerProperties().getReadinessCommandProbeDelay()); } @Override int getPeriod() { return getProbeIntProperty(READINESS_DEPLOYER_PROPERTY_PREFIX, "Command", "ProbePeriod", getKubernetesDeployerProperties().getReadinessCommandProbePeriod()); } @Override int getFailure() { return getProbeIntProperty(READINESS_DEPLOYER_PROPERTY_PREFIX, "Command", "ProbeFailure", getKubernetesDeployerProperties().getReadinessCommandProbeFailure()); } @Override int getSuccess() { return getProbeIntProperty(READINESS_DEPLOYER_PROPERTY_PREFIX, "Command", "ProbeSuccess", getKubernetesDeployerProperties().getReadinessCommandProbeSuccess()); } @Override String[] getCommand() { String probeCommandValue = getDeploymentPropertyValue(READINESS_DEPLOYER_PROPERTY_PREFIX + "CommandProbeCommand"); if (StringUtils.hasText(probeCommandValue)) { return new CommandLineTokenizer(probeCommandValue).getArgs().toArray(new String[0]); } if (getKubernetesDeployerProperties().getReadinessCommandProbeCommand() != null) { return new CommandLineTokenizer(getKubernetesDeployerProperties().getReadinessCommandProbeCommand()) .getArgs().toArray(new String[0]); } throw new IllegalArgumentException("The readinessCommandProbeCommand property must be set."); } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/ReadinessHttpProbeCreator.java ================================================ /* * Copyright 2018-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.cloud.deployer.spi.kubernetes; import org.springframework.util.StringUtils; /** * Creates an HTTP Readiness Probe. * * @author Chris Schaefer * @author Ilayaperumal Gopinathan * @author Corneil du Plessis */ class ReadinessHttpProbeCreator extends HttpProbeCreator { ReadinessHttpProbeCreator(KubernetesDeployerProperties kubernetesDeployerProperties, ContainerConfiguration containerConfiguration) { super(kubernetesDeployerProperties, containerConfiguration); } @Override public Integer getPort() { String probePortValue = getProbeProperty(READINESS_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbePort"); if (StringUtils.hasText(probePortValue)) { if (!probePortValue.chars().allMatch(Character::isDigit)) { throw new IllegalArgumentException("ReadinessHttpProbeCreator must contain all digits"); } return Integer.parseInt(probePortValue); } if (getKubernetesDeployerProperties().getReadinessHttpProbePort() != null) { return getKubernetesDeployerProperties().getReadinessHttpProbePort(); } if (getDefaultPort() != null) { return getDefaultPort(); } return null; } @Override protected String getProbePath() { String probePathValue = getProbeProperty(READINESS_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbePath"); if (StringUtils.hasText(probePathValue)) { return probePathValue; } if (getKubernetesDeployerProperties().getReadinessHttpProbePath() != null) { return getKubernetesDeployerProperties().getReadinessHttpProbePath(); } if (useBoot1ProbePath()) { return BOOT_1_READINESS_PROBE_PATH; } return BOOT_2_READINESS_PROBE_PATH; } @Override protected String getScheme() { String probeSchemeValue = getProbeProperty(READINESS_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbeScheme"); if (StringUtils.hasText(probeSchemeValue)) { return probeSchemeValue; } if (getKubernetesDeployerProperties().getReadinessHttpProbeScheme() != null) { return getKubernetesDeployerProperties().getReadinessHttpProbeScheme(); } return DEFAULT_PROBE_SCHEME; } @Override protected int getTimeout() { return getProbeIntProperty(READINESS_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbeTimeout", getKubernetesDeployerProperties().getReadinessHttpProbeTimeout()); } @Override protected int getInitialDelay() { return getProbeIntProperty(READINESS_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbeDelay", getKubernetesDeployerProperties().getReadinessHttpProbeDelay()); } @Override protected int getPeriod() { return getProbeIntProperty(READINESS_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbePeriod", getKubernetesDeployerProperties().getReadinessHttpProbePeriod()); } @Override int getFailure() { return getProbeIntProperty(READINESS_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbeFailure", getKubernetesDeployerProperties().getReadinessHttpProbeFailure()); } @Override int getSuccess() { return getProbeIntProperty(READINESS_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbeSuccess", getKubernetesDeployerProperties().getReadinessHttpProbeSuccess()); } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/ReadinessTcpProbeCreator.java ================================================ /* * Copyright 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.cloud.deployer.spi.kubernetes; import org.springframework.util.StringUtils; /** * Creates a TCP readiness probe * * @author Chris Schaefer * @author Corneil du Plessis * @since 2.5 */ class ReadinessTcpProbeCreator extends TcpProbeCreator { ReadinessTcpProbeCreator(KubernetesDeployerProperties kubernetesDeployerProperties, ContainerConfiguration containerConfiguration) { super(kubernetesDeployerProperties, containerConfiguration); } @Override int getInitialDelay() { return getProbeIntProperty(READINESS_DEPLOYER_PROPERTY_PREFIX, "Tcp", "ProbeDelay", getKubernetesDeployerProperties().getReadinessTcpProbeDelay()); } @Override int getPeriod() { return getProbeIntProperty(READINESS_DEPLOYER_PROPERTY_PREFIX, "Tcp", "ProbePeriod", getKubernetesDeployerProperties().getReadinessTcpProbePeriod()); } @Override protected int getTimeout() { return getProbeIntProperty(READINESS_DEPLOYER_PROPERTY_PREFIX, "Tcp", "ProbeTimeout", getKubernetesDeployerProperties().getReadinessTcpProbeTimeout()); } @Override Integer getPort() { String probePortValue = getProbeProperty(READINESS_DEPLOYER_PROPERTY_PREFIX, "Tcp", "ProbePort"); if (StringUtils.hasText(probePortValue)) { if (!probePortValue.chars().allMatch(Character::isDigit)) { throw new IllegalArgumentException("ReadinessTcpProbePort must contain all digits"); } return Integer.parseInt(probePortValue); } if (getKubernetesDeployerProperties().getReadinessTcpProbePort() != null) { return getKubernetesDeployerProperties().getReadinessTcpProbePort(); } throw new IllegalArgumentException("A readinessTcpProbePort property must be set."); } @Override int getFailure() { return getProbeIntProperty(READINESS_DEPLOYER_PROPERTY_PREFIX, "Tcp", "ProbeFailure", getKubernetesDeployerProperties().getReadinessTcpProbeFailure()); } @Override int getSuccess() { return getProbeIntProperty(READINESS_DEPLOYER_PROPERTY_PREFIX, "Tcp", "ProbeSuccess", getKubernetesDeployerProperties().getReadinessTcpProbeSuccess()); } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/RestartPolicy.java ================================================ /* * Copyright 2018-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.cloud.deployer.spi.kubernetes; /** * Defines restart policies that are available. * * @author Chris Schaefer */ public enum RestartPolicy { /** * Always restart a failed container. */ Always, /** * Restart a failed container with an exponential back-off delay, capped at 5 minutes. */ OnFailure, /** * Never restarts a successful or failed container. */ Never } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/RunningPhaseDeploymentStateResolver.java ================================================ /* * Copyright 2017 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.cloud.deployer.spi.kubernetes; import io.fabric8.kubernetes.api.model.ContainerStatus; import org.springframework.cloud.deployer.spi.app.DeploymentState; /** * @author David Turanski **/ public interface RunningPhaseDeploymentStateResolver { DeploymentState resolve(ContainerStatus containerStatus); } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/StartupCommandProbeCreator.java ================================================ /* * Copyright 2022 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.cloud.deployer.spi.kubernetes; import org.springframework.cloud.deployer.spi.util.CommandLineTokenizer; import org.springframework.util.StringUtils; /** * Creates a command based startup probe * * @author Corneil du Plessis * @since 2.5 */ class StartupCommandProbeCreator extends CommandProbeCreator { StartupCommandProbeCreator(KubernetesDeployerProperties kubernetesDeployerProperties, ContainerConfiguration containerConfiguration) { super(kubernetesDeployerProperties, containerConfiguration); } @Override int getInitialDelay() { return getProbeIntProperty(STARTUP_DEPLOYER_PROPERTY_PREFIX, "Command", "ProbeDelay", getKubernetesDeployerProperties().getStartupCommandProbeDelay()); } @Override int getPeriod() { return getProbeIntProperty(STARTUP_DEPLOYER_PROPERTY_PREFIX, "Command", "ProbePeriod", getKubernetesDeployerProperties().getStartupCommandProbePeriod()); } @Override int getFailure() { return getProbeIntProperty(STARTUP_DEPLOYER_PROPERTY_PREFIX, "Command", "ProbeFailure", getKubernetesDeployerProperties().getLivenessCommandProbeFailure()); } @Override int getSuccess() { return getProbeIntProperty(STARTUP_DEPLOYER_PROPERTY_PREFIX, "Command", "ProbeSuccess", getKubernetesDeployerProperties().getLivenessCommandProbeSuccess()); } @Override String[] getCommand() { String probeCommandValue = getDeploymentPropertyValue(STARTUP_DEPLOYER_PROPERTY_PREFIX + "CommandProbeCommand"); if (StringUtils.hasText(probeCommandValue)) { return new CommandLineTokenizer(probeCommandValue).getArgs().toArray(new String[0]); } if (getKubernetesDeployerProperties().getStartupCommandProbeCommand() != null) { return new CommandLineTokenizer(getKubernetesDeployerProperties().getStartupCommandProbeCommand()) .getArgs().toArray(new String[0]); } throw new IllegalArgumentException("The startupCommandProbeCommand property must be set."); } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/StartupHttpProbeCreator.java ================================================ /* * Copyright 2022 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.cloud.deployer.spi.kubernetes; import org.springframework.util.StringUtils; /** * Creates an HTTP Startup Probe. * * @author Corneil du Plessis */ class StartupHttpProbeCreator extends HttpProbeCreator { StartupHttpProbeCreator(KubernetesDeployerProperties kubernetesDeployerProperties, ContainerConfiguration containerConfiguration) { super(kubernetesDeployerProperties, containerConfiguration); } @Override public Integer getPort() { String probePortValue = getProbeProperty(STARTUP_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbePort"); if (StringUtils.hasText(probePortValue)) { if (!probePortValue.chars().allMatch(Character::isDigit)) { throw new IllegalArgumentException("StartupHttpProbeCreator must contain all digits"); } return Integer.parseInt(probePortValue); } if (getKubernetesDeployerProperties().getStartupHttpProbePort() != null) { return getKubernetesDeployerProperties().getStartupHttpProbePort(); } if (getDefaultPort() != null) { return getDefaultPort(); } return null; } @Override protected String getProbePath() { String probePathValue = getProbeProperty(STARTUP_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbePath"); if (StringUtils.hasText(probePathValue)) { return probePathValue; } if (getKubernetesDeployerProperties().getStartupHttpProbePath() != null) { return getKubernetesDeployerProperties().getStartupHttpProbePath(); } if (useBoot1ProbePath()) { return BOOT_1_READINESS_PROBE_PATH; } return BOOT_2_READINESS_PROBE_PATH; } @Override protected String getScheme() { String probeSchemeValue = getProbeProperty(STARTUP_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbeScheme"); if (StringUtils.hasText(probeSchemeValue)) { return probeSchemeValue; } if (getKubernetesDeployerProperties().getStartupProbeScheme() != null) { return getKubernetesDeployerProperties().getStartupProbeScheme(); } return DEFAULT_PROBE_SCHEME; } @Override protected int getTimeout() { return getProbeIntProperty(STARTUP_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbeTimeout", getKubernetesDeployerProperties().getStartupHttpProbeTimeout()); } @Override protected int getInitialDelay() { return getProbeIntProperty(STARTUP_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbeDelay", getKubernetesDeployerProperties().getStartupHttpProbeDelay()); } @Override protected int getPeriod() { return getProbeIntProperty(STARTUP_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbePeriod", getKubernetesDeployerProperties().getStartupHttpProbePeriod()); } @Override int getFailure() { return getProbeIntProperty(STARTUP_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbeFailure", getKubernetesDeployerProperties().getStartupHttpProbeFailure()); } @Override int getSuccess() { return getProbeIntProperty(STARTUP_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbeSuccess", getKubernetesDeployerProperties().getStartupHttpProbeSuccess()); } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/StartupTcpProbeCreator.java ================================================ /* * Copyright 2022 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.cloud.deployer.spi.kubernetes; import org.springframework.util.StringUtils; /** * Creates a TCP startup probe * * @author Corneil du Plessis * @since 2.5 */ class StartupTcpProbeCreator extends TcpProbeCreator { StartupTcpProbeCreator(KubernetesDeployerProperties kubernetesDeployerProperties, ContainerConfiguration containerConfiguration) { super(kubernetesDeployerProperties, containerConfiguration); } @Override int getInitialDelay() { return getProbeIntProperty(STARTUP_DEPLOYER_PROPERTY_PREFIX, "Tcp", "ProbeDelay", getKubernetesDeployerProperties().getStartupTcpProbeDelay()); } @Override int getPeriod() { return getProbeIntProperty(STARTUP_DEPLOYER_PROPERTY_PREFIX, "Tcp", "ProbePeriod", getKubernetesDeployerProperties().getStartupTcpProbePeriod()); } @Override protected int getTimeout() { return getProbeIntProperty(STARTUP_DEPLOYER_PROPERTY_PREFIX, "Tcp", "ProbeTimeout", getKubernetesDeployerProperties().getStartupTcpProbeTimeout()); } @Override Integer getPort() { String probePortValue = getProbeProperty(STARTUP_DEPLOYER_PROPERTY_PREFIX, "Tcp", "ProbePort"); if (StringUtils.hasText(probePortValue)) { if (!probePortValue.chars().allMatch(Character::isDigit)) { throw new IllegalArgumentException("StartupTcpProbePort must contain all digits"); } return Integer.parseInt(probePortValue); } if (getKubernetesDeployerProperties().getStartupTcpProbePort() != null) { return getKubernetesDeployerProperties().getStartupTcpProbePort(); } throw new IllegalArgumentException("A startupTcpProbePort property must be set."); } @Override int getFailure() { return getProbeIntProperty(STARTUP_DEPLOYER_PROPERTY_PREFIX, "Tcp", "ProbeFailure", getKubernetesDeployerProperties().getStartupTcpProbeFailure()); } @Override int getSuccess() { return getProbeIntProperty(STARTUP_DEPLOYER_PROPERTY_PREFIX, "Tcp", "ProbeSuccess", getKubernetesDeployerProperties().getStartupTcpProbeSuccess()); } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/TcpProbeCreator.java ================================================ /* * Copyright 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.cloud.deployer.spi.kubernetes; import io.fabric8.kubernetes.api.model.Probe; import io.fabric8.kubernetes.api.model.ProbeBuilder; /** * Base class for TCP based probe creators * * @author Chris Schaefer * @since 2.5 */ abstract class TcpProbeCreator extends ProbeCreator { TcpProbeCreator(KubernetesDeployerProperties kubernetesDeployerProperties, ContainerConfiguration containerConfiguration) { super(kubernetesDeployerProperties, containerConfiguration); } abstract Integer getPort(); protected abstract int getTimeout(); protected Probe create() { return new ProbeBuilder() .withNewTcpSocket() .withNewPort(getPort()) .endTcpSocket() .withInitialDelaySeconds(getInitialDelay()) .withPeriodSeconds(getPeriod()) .withFailureThreshold(getFailure()) .withSuccessThreshold(getSuccess()) .build(); } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/support/ArgumentSanitizer.java ================================================ /* * Copyright 2017-2022 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.cloud.deployer.spi.kubernetes.support; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.regex.Pattern; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; /** * Sanitizes potentially sensitive keys for a specific command line arg. * * @author Glenn Renfro * @author Gunnar Hillert * @author Ilayaperumal Gopinathan * @author Corneil du Plessis */ public class ArgumentSanitizer { private static final String[] REGEX_PARTS = { "*", "$", "^", "+" }; private static final String REDACTION_STRING = "******"; private static final String[] KEYS_TO_SANITIZE = { "username", "password", "secret", "key", "token", ".*credentials.*", "vcap_services", "url" }; private Pattern[] keysToSanitize; public ArgumentSanitizer() { this.keysToSanitize = new Pattern[KEYS_TO_SANITIZE.length]; for (int i = 0; i < keysToSanitize.length; i++) { this.keysToSanitize[i] = getPattern(KEYS_TO_SANITIZE[i]); } } private Pattern getPattern(String value) { if (isRegex(value)) { return Pattern.compile(value, Pattern.CASE_INSENSITIVE); } return Pattern.compile(".*" + value + "$", Pattern.CASE_INSENSITIVE); } private boolean isRegex(String value) { for (String part : REGEX_PARTS) { if (value.contains(part)) { return true; } } return false; } /** * Replaces a potential secure value with "******". * * @param argument the argument to cleanse. * @return the argument with a potentially sanitized value */ public String sanitize(String argument) { int indexOfFirstEqual = argument.indexOf("="); if (indexOfFirstEqual == -1) { return argument; } String key = argument.substring(0, indexOfFirstEqual); String value = argument.substring(indexOfFirstEqual + 1); value = sanitize(key, value); return String.format("%s=%s", key, value); } /** * Replaces a potential secure value with "******". * * @param key to check for sensitive words. * @param value the argument to cleanse. * @return the argument with a potentially sanitized value */ public String sanitize(String key, String value) { if (StringUtils.hasText(value)) { for (Pattern pattern : this.keysToSanitize) { if (pattern.matcher(key).matches()) { value = REDACTION_STRING; break; } } } return value; } /** * For all sensitive properties (e.g. key names containing words like password, secret, * key, token) replace the value with '*****' string * @param properties to be sanitized * @return sanitized properties */ public Map sanitizeProperties(Map properties) { if (!CollectionUtils.isEmpty(properties)) { final Map sanitizedProperties = new LinkedHashMap<>(properties.size()); for (Map.Entry property : properties.entrySet()) { sanitizedProperties.put(property.getKey(), this.sanitize(property.getKey(), property.getValue())); } return sanitizedProperties; } return properties; } /** * For all sensitive arguments (e.g. key names containing words like password, secret, * key, token) replace the value with '*****' string * @param arguments to be sanitized * @return sanitized arguments */ public List sanitizeArguments(List arguments) { if (!CollectionUtils.isEmpty(arguments)) { final List sanitizedArguments = new ArrayList<>(arguments.size()); for (String argument : arguments) { sanitizedArguments.add(this.sanitize(argument)); } return sanitizedArguments; } return arguments; } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/support/PropertyParserUtils.java ================================================ /* * Copyright 2018-2022 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.cloud.deployer.spi.kubernetes.support; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** * Utility methods for formatting and parsing properties * * @author Chris Schaefer * @author Ilayaperumal Gopinathan * @author Glenn Renfro */ public class PropertyParserUtils { /** * Extracts annotations from the provided value * * @param stringPairs The deployment request annotations * @return {@link Map} of annotations */ public static Map getStringPairsToMap(String stringPairs) { Map mapValue = new HashMap<>(); if (StringUtils.hasText(stringPairs)) { /** * Positive look ahead that into a non capturing group that will skip all commas in quotes. * Even number quotes will be ignored by the non capturing group. */ String[] pairs = stringPairs.split(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)", -1); for (String pair : pairs) { String[] splitString = pair.split(":", 2); Assert.isTrue(splitString.length == 2, String.format("Invalid annotation value: %s", pair)); String value = splitString[1].trim(); mapValue.put(splitString[0].trim(), value); } } return mapValue; } public static String getDeploymentPropertyValue(Map deploymentProperties, String propertyName) { return getDeploymentPropertyValue(deploymentProperties, propertyName, null); } public static String getDeploymentPropertyValue(Map deploymentProperties, String propertyName, String defaultValue) { RelaxedNames relaxedNames = new RelaxedNames(propertyName); for (Iterator itr = relaxedNames.iterator(); itr.hasNext();) { String relaxedName = itr.next(); if (deploymentProperties.containsKey(relaxedName)) { return deploymentProperties.get(relaxedName); } } return defaultValue; } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/java/org/springframework/cloud/deployer/spi/kubernetes/support/RelaxedNames.java ================================================ /* * Copyright 2012-2019 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.cloud.deployer.spi.kubernetes.support; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.Locale; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.springframework.util.StringUtils; /** * Generates relaxed name variations from a given source. * * @author Phillip Webb * @author Dave Syer */ public final class RelaxedNames implements Iterable { private static final Pattern CAMEL_CASE_PATTERN = Pattern.compile("([^A-Z-])([A-Z])"); private static final Pattern SEPARATED_TO_CAMEL_CASE_PATTERN = Pattern .compile("[_\\-.]"); private final String name; private final Set values = new LinkedHashSet(); /** * Create a new {@link RelaxedNames} instance. * @param name the source name. For the maximum number of variations specify the name * using dashed notation (e.g. {@literal my-property-name} */ public RelaxedNames(String name) { this.name = (name != null ? name : ""); initialize(RelaxedNames.this.name, this.values); } @Override public Iterator iterator() { return this.values.iterator(); } private void initialize(String name, Set values) { if (values.contains(name)) { return; } for (Variation variation : Variation.values()) { for (Manipulation manipulation : Manipulation.values()) { String result = name; result = manipulation.apply(result); result = variation.apply(result); values.add(result); initialize(result, values); } } } /** * Name variations. */ enum Variation { NONE { @Override public String apply(String value) { return value; } }, LOWERCASE { @Override public String apply(String value) { return (value.isEmpty() ? value : value.toLowerCase(Locale.ROOT)); } }, UPPERCASE { @Override public String apply(String value) { return (value.isEmpty() ? value : value.toUpperCase(Locale.ROOT)); } }; public abstract String apply(String value); } /** * Name manipulations. */ enum Manipulation { NONE { @Override public String apply(String value) { return value; } }, HYPHEN_TO_UNDERSCORE { @Override public String apply(String value) { return (value.indexOf('-') != -1 ? value.replace('-', '_') : value); } }, UNDERSCORE_TO_PERIOD { @Override public String apply(String value) { return (value.indexOf('_') != -1 ? value.replace('_', '.') : value); } }, PERIOD_TO_UNDERSCORE { @Override public String apply(String value) { return (value.indexOf('.') != -1 ? value.replace('.', '_') : value); } }, CAMELCASE_TO_UNDERSCORE { @Override public String apply(String value) { if (value.isEmpty()) { return value; } Matcher matcher = CAMEL_CASE_PATTERN.matcher(value); if (!matcher.find()) { return value; } matcher = matcher.reset(); StringBuffer result = new StringBuffer(); while (matcher.find()) { matcher.appendReplacement(result, matcher.group(1) + '_' + StringUtils.uncapitalize(matcher.group(2))); } matcher.appendTail(result); return result.toString(); } }, CAMELCASE_TO_HYPHEN { @Override public String apply(String value) { if (value.isEmpty()) { return value; } Matcher matcher = CAMEL_CASE_PATTERN.matcher(value); if (!matcher.find()) { return value; } matcher = matcher.reset(); StringBuffer result = new StringBuffer(); while (matcher.find()) { matcher.appendReplacement(result, matcher.group(1) + '-' + StringUtils.uncapitalize(matcher.group(2))); } matcher.appendTail(result); return result.toString(); } }, SEPARATED_TO_CAMELCASE { @Override public String apply(String value) { return separatedToCamelCase(value, false); } }, CASE_INSENSITIVE_SEPARATED_TO_CAMELCASE { @Override public String apply(String value) { return separatedToCamelCase(value, true); } }; private static final char[] SUFFIXES = new char[] { '_', '-', '.' }; public abstract String apply(String value); private static String separatedToCamelCase(String value, boolean caseInsensitive) { if (value.isEmpty()) { return value; } StringBuilder builder = new StringBuilder(); for (String field : SEPARATED_TO_CAMEL_CASE_PATTERN.split(value)) { field = (caseInsensitive ? field.toLowerCase(Locale.ROOT) : field); builder.append( builder.length() != 0 ? StringUtils.capitalize(field) : field); } char lastChar = value.charAt(value.length() - 1); for (char suffix : SUFFIXES) { if (lastChar == suffix) { builder.append(suffix); break; } } return builder.toString(); } } /** * Return a {@link RelaxedNames} for the given source camelCase source name. * @param name the source name in camelCase * @return the relaxed names */ public static RelaxedNames forCamelCase(String name) { StringBuilder result = new StringBuilder(); for (char c : name.toCharArray()) { result.append(Character.isUpperCase(c) && result.length() > 0 && result.charAt(result.length() - 1) != '-' ? "-" + Character.toLowerCase(c) : c); } return new RelaxedNames(result.toString()); } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ org.springframework.cloud.deployer.spi.kubernetes.KubernetesAutoConfiguration ================================================ FILE: spring-cloud-deployer-kubernetes/src/main/resources/META-INF/spring.factories ================================================ org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.springframework.cloud.deployer.spi.kubernetes.KubernetesAutoConfiguration ================================================ FILE: spring-cloud-deployer-kubernetes/src/test/java/org/springframework/cloud/deployer/spi/kubernetes/AbstractKubernetesTaskLauncherIntegrationTests.java ================================================ /* * Copyright 2021-2021 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.cloud.deployer.spi.kubernetes; import java.lang.reflect.Method; import java.time.Duration; import java.util.List; import java.util.UUID; import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.api.model.batch.v1.Job; import io.fabric8.kubernetes.client.KubernetesClient; import org.awaitility.core.ConditionFactory; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.deployer.resource.docker.DockerResource; import org.springframework.cloud.deployer.spi.task.TaskLauncher; import org.springframework.cloud.deployer.spi.test.AbstractTaskLauncherIntegrationJUnit5Tests; import org.springframework.cloud.deployer.spi.test.Timeout; import org.springframework.core.io.Resource; import static org.awaitility.Awaitility.await; /** * Abstract base class for integration tests for {@link KubernetesTaskLauncher}. * * @author Chris Bono * @author Corneil du Plessis */ abstract class AbstractKubernetesTaskLauncherIntegrationTests extends AbstractTaskLauncherIntegrationJUnit5Tests { @Autowired @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") protected TaskLauncher taskLauncher; @Autowired @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") protected KubernetesClient kubernetesClient; @Override protected TaskLauncher provideTaskLauncher() { return taskLauncher; } @Override protected String randomName() { return "task-" + UUID.randomUUID().toString().substring(0, 18); } @Override protected Resource testApplication() { return new DockerResource("springcloud/spring-cloud-deployer-spi-test-app:latest"); } @Override protected Timeout deploymentTimeout() { return new Timeout(30, 5000); } @Test @Override @Disabled("Currently reported as failed instead of cancelled") public void testSimpleCancel() throws InterruptedException { super.testSimpleCancel(); } protected void logTestInfo(TestInfo testInfo) { if (log.isInfoEnabled()) { log.info("Testing {}...", testInfo.getTestMethod().map(Method::getName).orElse(testInfo.getDisplayName())); } } protected ConditionFactory awaitWithPollAndTimeout(Timeout timeout) { return await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)); } protected List getPodsForTask(String taskName) { return kubernetesClient.pods().withLabel("task-name", taskName).list().getItems(); } protected List getJobsForTask(String taskName) { return kubernetesClient.batch().v1().jobs().withLabel("task-name", taskName).list().getItems(); } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/test/java/org/springframework/cloud/deployer/spi/kubernetes/DefaultContainerFactoryTests.java ================================================ /* * Copyright 2016-2022 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.cloud.deployer.spi.kubernetes; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.Stream; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import io.fabric8.kubernetes.api.model.Container; import io.fabric8.kubernetes.api.model.ContainerPort; import io.fabric8.kubernetes.api.model.ContainerPortBuilder; import io.fabric8.kubernetes.api.model.EnvVar; import io.fabric8.kubernetes.api.model.HTTPHeader; import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.Probe; import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.api.model.VolumeMount; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.deployer.resource.docker.DockerResource; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.core.io.Resource; import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Unit tests for {@link DefaultContainerFactory}. * * @author Will Kennedy * @author Donovan Muller * @author Chris Schaefer * @author David Turanski * @author Ilayaperumal Gopinathan * @author Glenn Renfro */ @ExtendWith(SpringExtension.class) @SpringBootTest(classes = {KubernetesAutoConfiguration.class}) public class DefaultContainerFactoryTests { @Test public void create() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.limits.memory", "128Mi"); props.put("spring.cloud.deployer.kubernetes.environment-variables", "JAVA_OPTIONS=-Xmx64m,KUBERNETES_NAMESPACE=test-space"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); assertThat(container.getEnv().size()).isEqualTo(3); EnvVar envVar1 = container.getEnv().get(0); EnvVar envVar2 = container.getEnv().get(1); assertThat(envVar1.getName()).isEqualTo("JAVA_OPTIONS"); assertThat(envVar1.getValue()).isEqualTo("-Xmx64m"); assertThat(envVar2.getName()).isEqualTo("KUBERNETES_NAMESPACE"); assertThat(envVar2.getValue()).isEqualTo("test-space"); } @Test public void createWithContainerCommand() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.containerCommand", "echo arg1 'arg2'"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); assertThat(container.getCommand()).containsExactly("echo", "arg1", "arg2"); } @Test public void createWithPorts() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.containerPorts", "8081, 8082, 65535"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); List containerPorts = container.getPorts(); assertThat(containerPorts).isNotNull(); assertThat(containerPorts.size()).isEqualTo(3); assertThat(containerPorts.get(0).getContainerPort()).isEqualTo(8081); assertThat(containerPorts.get(1).getContainerPort()).isEqualTo(8082); assertThat(containerPorts.get(2).getContainerPort()).isEqualTo(65535); } @Test public void createWithInvalidPort() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.containerPorts", "8081, 8082, invalid, 9212"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, props); //Attempting to create with an invalid integer set for a port should cause an exception to bubble up. ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest); assertThatThrownBy(() -> { defaultContainerFactory.create(containerConfiguration); }).isInstanceOf(NumberFormatException.class); } @Test public void createWithPortAndHostNetwork() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.containerPorts", "8081, 8082, 65535"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); List containerPorts = container.getPorts(); assertThat(containerPorts).isNotNull(); assertThat(containerPorts.size()).isEqualTo(3); assertThat(containerPorts.get(0).getContainerPort()).isEqualTo(8081); assertThat(containerPorts.get(0).getHostPort()).isEqualTo(8081); assertThat(containerPorts.get(1).getContainerPort()).isEqualTo(8082); assertThat(containerPorts.get(1).getHostPort()).isEqualTo(8082); assertThat(containerPorts.get(2).getContainerPort()).isEqualTo(65535); assertThat(containerPorts.get(2).getHostPort()).isEqualTo(65535); } @Test public void createWithEntryPointStyle() throws JsonProcessingException { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); Map appProps = new HashMap<>(); appProps.put("foo.bar.baz", "test"); AppDefinition definition = new AppDefinition("app-test", appProps); Resource resource = getResource(); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.entryPointStyle", "shell"); AppDeploymentRequest appDeploymentRequestShell = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration shellContainerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequestShell); Container containerShell = defaultContainerFactory.create(shellContainerConfiguration); assertThat(containerShell).isNotNull(); assertThat(containerShell.getEnv().get(0).getName()).isEqualTo("FOO_BAR_BAZ"); assertThat(containerShell.getArgs()).isEmpty(); List cmdLineArgs = new ArrayList<>(); cmdLineArgs.add("--foo.bar=value1"); cmdLineArgs.add("--spring.cloud.task.executionid=1"); cmdLineArgs.add("--spring.cloud.data.flow.platformname=platform1"); cmdLineArgs.add("--spring.cloud.data.flow.taskappname==a1"); cmdLineArgs.add("blah=chacha"); appDeploymentRequestShell = new AppDeploymentRequest(definition, resource, props, cmdLineArgs); shellContainerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequestShell); containerShell = defaultContainerFactory.create(shellContainerConfiguration); assertThat(containerShell).isNotNull(); assertThat(containerShell.getEnv()).hasSize(7); assertThat(containerShell.getArgs()).isEmpty(); String envVarString = containerShell.getEnv().toString(); assertThat(envVarString.contains("name=FOO_BAR_BAZ, value=test")).isTrue(); assertThat(envVarString.contains("name=FOO_BAR, value=value1")).isTrue(); assertThat(envVarString.contains("name=SPRING_CLOUD_TASK_EXECUTIONID, value=1")).isTrue(); assertThat(envVarString.contains("name=SPRING_CLOUD_DATA_FLOW_TASKAPPNAME, value==a1")).isTrue(); assertThat(envVarString.contains("name=SPRING_CLOUD_DATA_FLOW_PLATFORMNAME, value=platform1")).isTrue(); assertThat(envVarString.contains("name=BLAH, value=chacha")).isTrue(); props.put("spring.cloud.deployer.kubernetes.entryPointStyle", "exec"); AppDeploymentRequest appDeploymentRequestExec = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration execContainerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequestExec); Container containerExec = defaultContainerFactory.create(execContainerConfiguration); assertThat(containerExec).isNotNull(); assertThat(containerExec.getEnv()).hasSize(1); assertThat(containerExec.getArgs().get(0)).isEqualTo("--foo.bar.baz=test"); props.put("spring.cloud.deployer.kubernetes.entryPointStyle", "boot"); AppDeploymentRequest appDeploymentRequestBoot = new AppDeploymentRequest(definition, resource, props, Arrays.asList("--arg1=val1", "--arg2=val2")); ContainerConfiguration bootContainerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequestBoot); Container containerBoot = defaultContainerFactory.create(bootContainerConfiguration); assertThat(containerBoot).isNotNull(); assertThat(containerBoot.getEnv().get(0).getName()).isEqualTo("SPRING_APPLICATION_JSON"); assertThat(containerBoot.getEnv().get(0).getValue()).isEqualTo(new ObjectMapper().writeValueAsString(appProps)); assertThat(containerBoot.getArgs()).hasSize(2); assertThat(containerBoot.getArgs().get(0)).isEqualTo("--arg1=val1"); assertThat(containerBoot.getArgs().get(1)).isEqualTo("--arg2=val2"); } @Test public void createWithVolumeMounts() { // test volume mounts defined as deployer properties KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory(kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.volumeMounts", "[" + "{name: 'testhostpath', mountPath: '/test/hostPath'}, " + "{name: 'testpvc', mountPath: '/test/pvc', readOnly: 'true'}, " + "{name: 'testnfs', mountPath: '/test/nfs'}" + "]"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container.getVolumeMounts()).containsOnly( new VolumeMount("/test/hostPath", null, "testhostpath", null, null, null, null), new VolumeMount("/test/pvc", null, "testpvc", true, null, null, null), new VolumeMount("/test/nfs", null, "testnfs", null, null, null, null)); // test volume mounts defined as app deployment property, overriding the deployer property kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties .setVolumeMounts(Stream.of( new VolumeMount("/test/hostPath", null, "testhostpath", false, null, null, null), new VolumeMount("/test/pvc", null, "testpvc", true, null, null, null), new VolumeMount("/test/nfs", null, "testnfs", false, null, null, null)) .collect(Collectors.toList())); defaultContainerFactory = new DefaultContainerFactory(kubernetesDeployerProperties); props.clear(); props.put("spring.cloud.deployer.kubernetes.volumeMounts", "[" + "{name: 'testpvc', mountPath: '/test/pvc/overridden'}, " + "{name: 'testnfs', mountPath: '/test/nfs/overridden', readOnly: 'true'}" + "]"); containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest); container = defaultContainerFactory.create(containerConfiguration); assertThat(container.getVolumeMounts()).containsOnly( new VolumeMount("/test/hostPath", null, "testhostpath", false, null, null, null), new VolumeMount("/test/pvc/overridden", null, "testpvc", null, null, null, null), new VolumeMount("/test/nfs/overridden", null, "testnfs", true, null, null, null)); } /** * @deprecated {@see {@link #createCustomLivenessHttpPortFromProperties()}} */ @Test @Deprecated public void createCustomLivenessPortFromProperties() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setLivenessHttpProbePort(8090); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); Map props = new HashMap<>(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withExternalPort(8080) .withHostNetwork(true); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); List containerPorts = container.getPorts(); assertThat(containerPorts).isNotNull(); assertThat(containerPorts).as("Only two container ports should be set").hasSize(2); assertThat((int) containerPorts.get(0).getContainerPort()).isEqualTo(8080); assertThat((int) containerPorts.get(0).getHostPort()).isEqualTo(8080); assertThat((int) containerPorts.get(1).getContainerPort()).isEqualTo(8090); assertThat((int) containerPorts.get(1).getHostPort()).isEqualTo(8090); assertThat((int) container.getLivenessProbe().getHttpGet().getPort().getIntVal()).isEqualTo(8090); } @Test public void createCustomLivenessHttpPortFromProperties() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setLivenessHttpProbePort(8090); kubernetesDeployerProperties.setStartupHttpProbePort(kubernetesDeployerProperties.getLivenessHttpProbePort()); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); Map props = new HashMap<>(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withExternalPort(8080) .withHostNetwork(true); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); List containerPorts = container.getPorts(); assertThat(containerPorts).isNotNull(); assertThat(containerPorts).as("Only two container ports should be set").hasSize(2); assertThat((int) containerPorts.get(0).getContainerPort()).isEqualTo(8080); assertThat((int) containerPorts.get(0).getHostPort()).isEqualTo(8080); assertThat((int) containerPorts.get(1).getContainerPort()).isEqualTo(8090); assertThat((int) containerPorts.get(1).getHostPort()).isEqualTo(8090); assertThat((int) container.getLivenessProbe().getHttpGet().getPort().getIntVal()).isEqualTo(8090); } @Test public void createCustomLivenessHttpProbeSchemeFromProperties() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setLivenessHttpProbeScheme("HTTPS"); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); Map props = new HashMap<>(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withExternalPort(8080) .withHostNetwork(true); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); List containerPorts = container.getPorts(); assertThat(containerPorts).isNotNull(); assertThat((int) containerPorts.get(0).getContainerPort()).isEqualTo(8080); assertThat((int) containerPorts.get(0).getHostPort()).isEqualTo(8080); assertThat((int) container.getLivenessProbe().getHttpGet().getPort().getIntVal()).isEqualTo(8080); assertThat(container.getLivenessProbe().getHttpGet().getScheme()).isEqualTo("HTTPS"); } @Test public void createCustomReadinessHttpSchemeFromProperties() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setReadinessHttpProbeScheme("HTTPS"); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); Map props = new HashMap<>(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); List containerPorts = container.getPorts(); assertThat(containerPorts).isNotNull(); assertThat((int) containerPorts.get(0).getContainerPort()).isEqualTo(8080); assertThat((int) containerPorts.get(0).getHostPort()).isEqualTo(8080); assertThat((int) container.getReadinessProbe().getHttpGet().getPort().getIntVal()).isEqualTo(8080); assertThat(container.getReadinessProbe().getHttpGet().getScheme()).isEqualTo("HTTPS"); } @Test public void createCustomLivenessAndReadinessHttpProbeSchemeFromAppRequest() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); Map appProperties = new HashMap<>(); appProperties.put("spring.cloud.deployer.kubernetes.livenessHttpProbeScheme", "HTTPS"); appProperties.put("spring.cloud.deployer.kubernetes.readinessHttpProbeScheme", "HTTPS"); AppDefinition definition = new AppDefinition("app-test", appProperties); Resource resource = getResource(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, appProperties); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); assertThat(container.getReadinessProbe().getHttpGet().getPath()).isNotNull(); assertThat(container.getReadinessProbe().getHttpGet().getScheme()).isEqualTo("HTTPS"); assertThat(container.getLivenessProbe().getHttpGet().getPath()).isNotNull(); assertThat(container.getLivenessProbe().getHttpGet().getScheme()).isEqualTo("HTTPS"); } /** * @deprecated {@see {@link #createCustomLivenessHttpPortFromAppRequest()}} */ @Test @Deprecated public void createCustomLivenessPortFromAppRequest() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.liveness-probe-port", Integer.toString(8090)); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); List containerPorts = container.getPorts(); assertThat(containerPorts).isNotNull(); assertThat(containerPorts).hasSize(2); assertThat(containerPorts.get(0).getContainerPort()).isEqualTo(8080); assertThat(containerPorts.get(0).getHostPort()).isEqualTo(8080); assertThat(containerPorts.get(1).getContainerPort()).isEqualTo(8090); assertThat(containerPorts.get(1).getHostPort()).isEqualTo(8090); assertThat(container.getLivenessProbe().getHttpGet().getPort().getIntVal()).isEqualTo(8090); } @Test public void createCustomLivenessHttpPortFromAppRequest() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.liveness-http-probe-port", Integer.toString(8090)); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); List containerPorts = container.getPorts(); assertThat(containerPorts).isNotNull(); assertThat(containerPorts).hasSize(2); assertThat(containerPorts.get(0).getContainerPort()).isEqualTo(8080); assertThat(containerPorts.get(0).getHostPort()).isEqualTo(8080); assertThat(containerPorts.get(1).getContainerPort()).isEqualTo(8090); assertThat(containerPorts.get(1).getHostPort()).isEqualTo(8090); assertThat(container.getLivenessProbe().getHttpGet().getPort().getIntVal()).isEqualTo(8090); } /** * @deprecated {@see {@link #createCustomReadinessHttpPortFromAppRequest()}} */ @Test @Deprecated public void createCustomReadinessPortFromAppRequest() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.readinessProbePort", Integer.toString(8090)); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); List containerPorts = container.getPorts(); assertThat(containerPorts).isNotNull(); assertThat(containerPorts).hasSize(2); assertThat(containerPorts.get(0).getContainerPort()).isEqualTo(8080); assertThat(containerPorts.get(0).getHostPort()).isEqualTo(8080); assertThat(containerPorts.get(1).getContainerPort()).isEqualTo(8090); assertThat(containerPorts.get(1).getHostPort()).isEqualTo(8090); assertThat(container.getReadinessProbe().getHttpGet().getPort().getIntVal()).isEqualTo(8090); } @Test public void createCustomReadinessHttpPortFromAppRequest() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.readinessHttpProbePort", Integer.toString(8090)); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); List containerPorts = container.getPorts(); assertThat(containerPorts).isNotNull(); assertThat(containerPorts).hasSize(2); assertThat(containerPorts.get(0).getContainerPort()).isEqualTo(8080); assertThat(containerPorts.get(0).getHostPort()).isEqualTo(8080); assertThat(containerPorts.get(1).getContainerPort()).isEqualTo(8090); assertThat(containerPorts.get(1).getHostPort()).isEqualTo(8090); assertThat(container.getReadinessProbe().getHttpGet().getPort().getIntVal()).isEqualTo(8090); } /** * @deprecated {@see {@link #createCustomReadinessHttpPortFromProperties()}} */ @Test @Deprecated public void createCustomReadinessPortFromProperties() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setReadinessHttpProbePort(8090); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); Map props = new HashMap<>(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); List containerPorts = container.getPorts(); assertThat(containerPorts).isNotNull(); assertThat(containerPorts).hasSize(2); assertThat(containerPorts.get(0).getContainerPort()).isEqualTo(8080); assertThat(containerPorts.get(0).getHostPort()).isEqualTo(8080); assertThat(containerPorts.get(1).getContainerPort()).isEqualTo(8090); assertThat(containerPorts.get(1).getHostPort()).isEqualTo(8090); assertThat(container.getReadinessProbe().getHttpGet().getPort().getIntVal()).isEqualTo(8090); } @Test public void createCustomReadinessHttpPortFromProperties() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setReadinessHttpProbePort(8090); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); Map props = new HashMap<>(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); List containerPorts = container.getPorts(); assertThat(containerPorts).isNotNull(); assertThat(containerPorts).hasSize(2); assertThat(containerPorts.get(0).getContainerPort()).isEqualTo(8080); assertThat(containerPorts.get(0).getHostPort()).isEqualTo(8080); assertThat(containerPorts.get(1).getContainerPort()).isEqualTo(8090); assertThat(containerPorts.get(1).getHostPort()).isEqualTo(8090); assertThat(container.getReadinessProbe().getHttpGet().getPort().getIntVal()).isEqualTo(8090); } @Test public void createDefaultHttpProbePorts() { int defaultPort = 8080; KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); Map props = new HashMap<>(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(defaultPort); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); List containerPorts = container.getPorts(); assertThat(containerPorts).isNotNull(); assertThat(containerPorts).as("Only the default container port should set").hasSize(1); assertThat((int) containerPorts.get(0).getContainerPort()).isEqualTo(8080); assertThat((int) containerPorts.get(0).getHostPort()).isEqualTo(8080); assertThat((int) container.getLivenessProbe().getHttpGet().getPort().getIntVal()).isEqualTo(8080); assertThat((int) container.getReadinessProbe().getHttpGet().getPort().getIntVal()).isEqualTo(8080); } @Test public void createHttpProbesWithDefaultEndpoints() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); Map props = new HashMap<>(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); assertThat(container.getReadinessProbe().getHttpGet().getPath()).isNotNull(); assertThat(container.getReadinessProbe().getHttpGet().getPath()).isEqualTo(HttpProbeCreator.BOOT_2_READINESS_PROBE_PATH); assertThat(container.getLivenessProbe().getHttpGet().getPath()).isNotNull(); assertThat(container.getLivenessProbe().getHttpGet().getPath()).isEqualTo(HttpProbeCreator.BOOT_2_LIVENESS_PROBE_PATH); } @Test public void createHttpProbesWithBoot1Endpoints() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); Map appProperties = new HashMap<>(); appProperties.put("spring.cloud.deployer.kubernetes.boot-major-version", "1"); AppDefinition definition = new AppDefinition("app-test", appProperties); Resource resource = getResource(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, appProperties); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); assertThat(container.getReadinessProbe().getHttpGet().getPath()).isNotNull(); assertThat(container.getReadinessProbe().getHttpGet().getPath()).isEqualTo(HttpProbeCreator.BOOT_1_READINESS_PROBE_PATH); assertThat(container.getLivenessProbe().getHttpGet().getPath()).isNotNull(); assertThat(container.getLivenessProbe().getHttpGet().getPath()).isEqualTo(HttpProbeCreator.BOOT_1_LIVENESS_PROBE_PATH); } /** * @deprecated {@see {@link #createHttpProbesWithOverrides()}} */ @Test @Deprecated public void createProbesWithOverrides() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); Map appProperties = new HashMap<>(); appProperties.put("spring.cloud.deployer.kubernetes.livenessProbePath", "/liveness"); appProperties.put("spring.cloud.deployer.kubernetes.readinessProbePath", "/readiness"); AppDefinition definition = new AppDefinition("app-test", appProperties); Resource resource = getResource(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, appProperties); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); assertThat(container.getReadinessProbe().getHttpGet().getPath()).isNotNull(); assertThat(container.getReadinessProbe().getHttpGet().getPath()).isEqualTo("/readiness"); assertThat(container.getLivenessProbe().getHttpGet().getPath()).isNotNull(); assertThat(container.getLivenessProbe().getHttpGet().getPath()).isEqualTo("/liveness"); } @Test public void createHttpProbesWithOverrides() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); Map appProperties = new HashMap<>(); appProperties.put("spring.cloud.deployer.kubernetes.livenessHttpProbePath", "/liveness"); appProperties.put("spring.cloud.deployer.kubernetes.readinessHttpProbePath", "/readiness"); AppDefinition definition = new AppDefinition("app-test", appProperties); Resource resource = getResource(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, appProperties); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); assertThat(container.getReadinessProbe().getHttpGet().getPath()).isNotNull(); assertThat(container.getReadinessProbe().getHttpGet().getPath()).isEqualTo("/readiness"); assertThat(container.getLivenessProbe().getHttpGet().getPath()).isNotNull(); assertThat(container.getLivenessProbe().getHttpGet().getPath()).isEqualTo("/liveness"); } /** * @deprecated {@see {@link #createHttpProbesWithPropertyOverrides()}} */ @Test @Deprecated public void createProbesWithPropertyOverrides() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setReadinessHttpProbePath("/readiness"); kubernetesDeployerProperties.setLivenessHttpProbePath("/liveness"); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, null); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); assertThat(container.getReadinessProbe().getHttpGet().getPath()).isNotNull(); assertThat(container.getReadinessProbe().getHttpGet().getPath()).isEqualTo("/readiness"); assertThat(container.getLivenessProbe().getHttpGet().getPath()).isNotNull(); assertThat(container.getLivenessProbe().getHttpGet().getPath()).isEqualTo("/liveness"); } @Test public void createHttpProbesWithPropertyOverrides() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setReadinessHttpProbePath("/readiness"); kubernetesDeployerProperties.setLivenessHttpProbePath("/liveness"); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, null); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); assertThat(container.getReadinessProbe().getHttpGet().getPath()).isNotNull(); assertThat(container.getReadinessProbe().getHttpGet().getPath()).isEqualTo("/readiness"); assertThat(container.getLivenessProbe().getHttpGet().getPath()).isNotNull(); assertThat(container.getLivenessProbe().getHttpGet().getPath()).isEqualTo("/liveness"); } @Test public void testHttpProbeCredentialsSecret() { Secret secret = randomSecret(); String secretName = secret.getMetadata().getName(); Map appProperties = new HashMap<>(); appProperties.put("spring.cloud.deployer.kubernetes.probeCredentialsSecret", secretName); AppDefinition definition = new AppDefinition("app-test", appProperties); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), appProperties); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withExternalPort(8080) .withProbeCredentialsSecret(secret); ContainerFactory containerFactory = new DefaultContainerFactory(new KubernetesDeployerProperties()); Container container = containerFactory.create(containerConfiguration); String credentials = containerConfiguration.getProbeCredentialsSecret().getData() .get(HttpProbeCreator.PROBE_CREDENTIALS_SECRET_KEY_NAME); HTTPHeader livenessProbeHeader = container.getLivenessProbe().getHttpGet().getHttpHeaders().get(0); assertThat(livenessProbeHeader.getName()).isEqualTo(HttpProbeCreator.AUTHORIZATION_HEADER_NAME); assertThat(livenessProbeHeader.getValue()).isEqualTo(ProbeAuthenticationType.Basic.name() + " " + credentials); HTTPHeader readinessProbeHeader = container.getReadinessProbe().getHttpGet().getHttpHeaders().get(0); assertThat(readinessProbeHeader.getName()).isEqualTo(HttpProbeCreator.AUTHORIZATION_HEADER_NAME); assertThat(readinessProbeHeader.getValue()).isEqualTo(ProbeAuthenticationType.Basic.name() + " " + credentials); } @Test public void testHttpProbeCredentialsInvalidSecret() { Secret secret = randomSecret(); secret.setData(Collections.singletonMap("unexpectedkey", "dXNlcjpwYXNz")); String secretName = secret.getMetadata().getName(); Map appProperties = new HashMap<>(); appProperties.put("spring.cloud.deployer.kubernetes.probeCredentialsSecret", secretName); AppDefinition definition = new AppDefinition("app-test", appProperties); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), appProperties); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withExternalPort(8080) .withProbeCredentialsSecret(secret); ContainerFactory containerFactory = new DefaultContainerFactory(new KubernetesDeployerProperties()); assertThatThrownBy(() -> { containerFactory.create(containerConfiguration); }).isInstanceOf(IllegalArgumentException.class); } @Test public void testHttpProbeHeadersWithoutAuth() { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource()); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withExternalPort(8080); ContainerFactory containerFactory = new DefaultContainerFactory(new KubernetesDeployerProperties()); Container container = containerFactory.create(containerConfiguration); assertThat(container.getLivenessProbe().getHttpGet().getHttpHeaders()).isEmpty(); assertThat(container.getReadinessProbe().getHttpGet().getHttpHeaders()).isEmpty(); } @Test public void createTcpProbe() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); Map appProperties = new HashMap<>(); appProperties.put("spring.cloud.deployer.kubernetes.probeType", ProbeType.TCP.name()); appProperties.put("spring.cloud.deployer.kubernetes.readinessTcpProbePort", "5050"); appProperties.put("spring.cloud.deployer.kubernetes.livenessTcpProbePort", "9090"); appProperties.put("spring.cloud.deployer.kubernetes.startupTcpProbePort", "9090"); AppDefinition definition = new AppDefinition("app-test", appProperties); Resource resource = getResource(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, appProperties); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); assertThat(container.getPorts()) .contains(new ContainerPortBuilder().withHostPort(5050).withContainerPort(5050).build()); assertThat(container.getPorts()) .contains(new ContainerPortBuilder().withHostPort(9090).withContainerPort(9090).build()); Probe livenessProbe = container.getLivenessProbe(); Probe readinessProbe = container.getReadinessProbe(); assertThat(livenessProbe).isNotNull(); assertThat(readinessProbe).isNotNull(); Integer livenessTcpProbePort = livenessProbe.getTcpSocket().getPort().getIntVal(); assertThat(livenessTcpProbePort).isNotNull(); assertThat(livenessTcpProbePort.intValue()).isEqualTo(9090); Integer readinessTcpProbePort = readinessProbe.getTcpSocket().getPort().getIntVal(); assertThat(readinessTcpProbePort).isNotNull(); assertThat(readinessTcpProbePort.intValue()).isEqualTo(5050); assertThat(livenessProbe.getPeriodSeconds()).isNotNull(); assertThat(readinessProbe.getPeriodSeconds()).isNotNull(); assertThat(livenessProbe.getInitialDelaySeconds()).isNotNull(); assertThat(readinessProbe.getInitialDelaySeconds()).isNotNull(); } @Test public void createTcpProbeMissingLivenessPort() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); Map appProperties = new HashMap<>(); appProperties.put("spring.cloud.deployer.kubernetes.probeType", ProbeType.TCP.name()); appProperties.put("spring.cloud.deployer.kubernetes.readinessTcpProbePort", "5050"); appProperties.put("spring.cloud.deployer.kubernetes.startupTcpProbePort", "9090"); AppDefinition definition = new AppDefinition("app-test", appProperties); Resource resource = getResource(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, appProperties); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); assertThatThrownBy(() -> { defaultContainerFactory.create(containerConfiguration); }).isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("The livenessTcpProbePort property must be set"); } @Test public void createTcpProbeMissingReadinessPort() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); Map appProperties = new HashMap<>(); appProperties.put("spring.cloud.deployer.kubernetes.probeType", ProbeType.TCP.name()); appProperties.put("spring.cloud.deployer.kubernetes.livenessTcpProbePort", "5050"); appProperties.put("spring.cloud.deployer.kubernetes.startupTcpProbePort", "5050"); AppDefinition definition = new AppDefinition("app-test", appProperties); Resource resource = getResource(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, appProperties); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); assertThatThrownBy(() -> { defaultContainerFactory.create(containerConfiguration); }).isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("A readinessTcpProbePort property must be set"); } @Test public void createReadinessTcpProbeWithNonDigitPort() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); Map appProperties = new HashMap<>(); appProperties.put("spring.cloud.deployer.kubernetes.probeType", ProbeType.TCP.name()); appProperties.put("spring.cloud.deployer.kubernetes.livenessTcpProbePort", "9090"); appProperties.put("spring.cloud.deployer.kubernetes.readinessTcpProbePort", "somePort"); appProperties.put("spring.cloud.deployer.kubernetes.startupTcpProbePort", "9090"); AppDefinition definition = new AppDefinition("app-test", appProperties); Resource resource = getResource(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, appProperties); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); assertThatThrownBy(() -> { defaultContainerFactory.create(containerConfiguration); }).isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("ReadinessTcpProbePort must contain all digits"); } @Test public void createLivenessTcpProbeWithNonDigitPort() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); Map appProperties = new HashMap<>(); appProperties.put("spring.cloud.deployer.kubernetes.probeType", ProbeType.TCP.name()); appProperties.put("spring.cloud.deployer.kubernetes.livenessTcpProbePort", "somePort"); appProperties.put("spring.cloud.deployer.kubernetes.readinessTcpProbePort", "5050"); appProperties.put("spring.cloud.deployer.kubernetes.startupTcpProbePort", "9090"); AppDefinition definition = new AppDefinition("app-test", appProperties); Resource resource = getResource(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, appProperties); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); assertThatThrownBy(() -> { defaultContainerFactory.create(containerConfiguration); }).isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("LivenessTcpProbePort must contain all digits"); } @Test public void createTcpProbeGlobalProperties() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setProbeType(ProbeType.TCP); kubernetesDeployerProperties.setReadinessTcpProbePort(5050); kubernetesDeployerProperties.setReadinessTcpProbeDelay(1); kubernetesDeployerProperties.setReadinessTcpProbePeriod(2); kubernetesDeployerProperties.setLivenessTcpProbePort(9090); kubernetesDeployerProperties.setLivenessTcpProbeDelay(3); kubernetesDeployerProperties.setLivenessTcpProbePeriod(4); kubernetesDeployerProperties.setStartupTcpProbePort(9090); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, null); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); assertThat(container.getPorts()) .contains(new ContainerPortBuilder().withHostPort(5050).withContainerPort(5050).build()); assertThat(container.getPorts()) .contains(new ContainerPortBuilder().withHostPort(9090).withContainerPort(9090).build()); Probe livenessProbe = container.getLivenessProbe(); Probe readinessProbe = container.getReadinessProbe(); assertThat(livenessProbe).isNotNull(); assertThat(readinessProbe).isNotNull(); Integer livenessTcpProbePort = livenessProbe.getTcpSocket().getPort().getIntVal(); assertThat(livenessTcpProbePort).isNotNull(); assertThat(livenessTcpProbePort.intValue()).isEqualTo(9090); Integer readinessTcpProbePort = readinessProbe.getTcpSocket().getPort().getIntVal(); assertThat(readinessTcpProbePort).isNotNull(); assertThat(readinessTcpProbePort.intValue()).isEqualTo(5050); Integer livenessProbePeriodSeconds = livenessProbe.getPeriodSeconds(); assertThat(livenessProbePeriodSeconds).isNotNull(); assertThat(livenessProbePeriodSeconds.intValue()).isEqualTo(4); Integer readinessProbePeriodSeconds = readinessProbe.getPeriodSeconds(); assertThat(readinessProbePeriodSeconds).isNotNull(); assertThat(readinessProbePeriodSeconds.intValue()).isEqualTo(2); Integer livenessProbeInitialDelaySeconds = livenessProbe.getInitialDelaySeconds(); assertThat(livenessProbeInitialDelaySeconds).isNotNull(); assertThat(livenessProbeInitialDelaySeconds.intValue()).isEqualTo(3); Integer readinessProbeInitialDelaySeconds = readinessProbe.getInitialDelaySeconds(); assertThat(readinessProbeInitialDelaySeconds).isNotNull(); assertThat(readinessProbeInitialDelaySeconds.intValue()).isEqualTo(1); } @Test public void createTcpProbeGlobalPropertyOverride() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setProbeType(ProbeType.TCP); kubernetesDeployerProperties.setReadinessTcpProbePort(5050); kubernetesDeployerProperties.setReadinessTcpProbeDelay(1); kubernetesDeployerProperties.setReadinessTcpProbePeriod(2); kubernetesDeployerProperties.setLivenessTcpProbePort(9090); kubernetesDeployerProperties.setLivenessTcpProbeDelay(3); kubernetesDeployerProperties.setLivenessTcpProbePeriod(4); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); Map appProperties = new HashMap<>(); appProperties.put("spring.cloud.deployer.kubernetes.probeType", ProbeType.TCP.name()); appProperties.put("spring.cloud.deployer.kubernetes.readinessTcpProbePort", "5050"); appProperties.put("spring.cloud.deployer.kubernetes.readinessProbePeriod", "11"); appProperties.put("spring.cloud.deployer.kubernetes.readinessProbeDelay", "12"); appProperties.put("spring.cloud.deployer.kubernetes.livenessTcpProbePort", "9090"); appProperties.put("spring.cloud.deployer.kubernetes.livenessProbePeriod", "13"); appProperties.put("spring.cloud.deployer.kubernetes.livenessProbeDelay", "14"); appProperties.put("spring.cloud.deployer.kubernetes.startupTcpProbePort", "9090"); AppDefinition definition = new AppDefinition("app-test", appProperties); Resource resource = getResource(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, appProperties); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); assertThat(container.getPorts()) .contains(new ContainerPortBuilder().withHostPort(5050).withContainerPort(5050).build()); assertThat(container.getPorts()) .contains(new ContainerPortBuilder().withHostPort(9090).withContainerPort(9090).build()); Probe livenessProbe = container.getLivenessProbe(); Probe readinessProbe = container.getReadinessProbe(); assertThat(livenessProbe).isNotNull(); assertThat(readinessProbe).isNotNull(); Integer livenessTcpProbePort = livenessProbe.getTcpSocket().getPort().getIntVal(); assertThat(livenessTcpProbePort).isNotNull(); assertThat(livenessTcpProbePort.intValue()).isEqualTo(9090); Integer readinessTcpProbePort = readinessProbe.getTcpSocket().getPort().getIntVal(); assertThat(readinessTcpProbePort).isNotNull(); assertThat(readinessTcpProbePort.intValue()).isEqualTo(5050); Integer livenessProbePeriodSeconds = livenessProbe.getPeriodSeconds(); assertThat(livenessProbePeriodSeconds).isNotNull(); assertThat(livenessProbePeriodSeconds.intValue()).isEqualTo(13); Integer readinessProbePeriodSeconds = readinessProbe.getPeriodSeconds(); assertThat(readinessProbePeriodSeconds).isNotNull(); assertThat(readinessProbePeriodSeconds.intValue()).isEqualTo(11); Integer livenessProbeInitialDelaySeconds = livenessProbe.getInitialDelaySeconds(); assertThat(livenessProbeInitialDelaySeconds).isNotNull(); assertThat(livenessProbeInitialDelaySeconds.intValue()).isEqualTo(14); Integer readinessProbeInitialDelaySeconds = readinessProbe.getInitialDelaySeconds(); assertThat(readinessProbeInitialDelaySeconds).isNotNull(); assertThat(readinessProbeInitialDelaySeconds.intValue()).isEqualTo(12); } @Test public void createCommandProbe() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); Map appProperties = new HashMap<>(); appProperties.put("spring.cloud.deployer.kubernetes.probeType", ProbeType.COMMAND.name()); appProperties.put("spring.cloud.deployer.kubernetes.readinessCommandProbeCommand", "ls /"); appProperties.put("spring.cloud.deployer.kubernetes.livenessCommandProbeCommand", "ls /dev"); appProperties.put("spring.cloud.deployer.kubernetes.startupCommandProbeCommand", "ls /dev"); AppDefinition definition = new AppDefinition("app-test", appProperties); Resource resource = getResource(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, appProperties); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); Probe livenessProbe = container.getLivenessProbe(); Probe readinessProbe = container.getReadinessProbe(); assertThat(livenessProbe).isNotNull(); assertThat(readinessProbe).isNotNull(); List livenessCommandProbeCommand = livenessProbe.getExec().getCommand(); assertThat(livenessCommandProbeCommand).containsExactly("ls", "/dev"); List readinessCommandProbeCommand = readinessProbe.getExec().getCommand(); assertThat(readinessCommandProbeCommand).containsExactly("ls", "/"); assertThat(livenessProbe.getPeriodSeconds()).isNotNull(); assertThat(readinessProbe.getPeriodSeconds()).isNotNull(); assertThat(livenessProbe.getInitialDelaySeconds()).isNotNull(); assertThat(readinessProbe.getInitialDelaySeconds()).isNotNull(); } @Test public void createCommandProbeMissingCommand() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); Map appProperties = new HashMap<>(); appProperties.put("spring.cloud.deployer.kubernetes.probeType", ProbeType.COMMAND.name()); appProperties.put("spring.cloud.deployer.kubernetes.startupCommandProbeCommand", "ls /"); AppDefinition definition = new AppDefinition("app-test", appProperties); Resource resource = getResource(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, appProperties); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); assertThatThrownBy(() -> { defaultContainerFactory.create(containerConfiguration); }).isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("The readinessCommandProbeCommand property must be set"); } @Test public void createCommandProbeGlobalProperties() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setProbeType(ProbeType.COMMAND); kubernetesDeployerProperties.setReadinessCommandProbeCommand("ls /"); kubernetesDeployerProperties.setReadinessCommandProbeDelay(1); kubernetesDeployerProperties.setReadinessCommandProbePeriod(2); kubernetesDeployerProperties.setLivenessCommandProbeCommand("ls /dev"); kubernetesDeployerProperties.setLivenessCommandProbeDelay(3); kubernetesDeployerProperties.setLivenessCommandProbePeriod(4); kubernetesDeployerProperties.setStartupCommandProbeCommand("ls /dev"); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, null); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); Probe livenessProbe = container.getLivenessProbe(); Probe readinessProbe = container.getReadinessProbe(); assertThat(livenessProbe).isNotNull(); assertThat(readinessProbe).isNotNull(); String livenessTcpProbeCommand = String.join(" ", livenessProbe.getExec().getCommand()); assertThat(livenessTcpProbeCommand).isNotNull(); assertThat(livenessTcpProbeCommand).isEqualTo("ls /dev"); String readinessTcpProbeCommand = String.join(" ", readinessProbe.getExec().getCommand()); assertThat(readinessTcpProbeCommand).isNotNull(); assertThat(readinessTcpProbeCommand).isEqualTo("ls /"); Integer livenessProbePeriodSeconds = livenessProbe.getPeriodSeconds(); assertThat(livenessProbePeriodSeconds).isNotNull(); assertThat(livenessProbePeriodSeconds.intValue()).isEqualTo(4); Integer readinessProbePeriodSeconds = readinessProbe.getPeriodSeconds(); assertThat(readinessProbePeriodSeconds).isNotNull(); assertThat(readinessProbePeriodSeconds.intValue()).isEqualTo(2); Integer livenessProbeInitialDelaySeconds = livenessProbe.getInitialDelaySeconds(); assertThat(livenessProbeInitialDelaySeconds).isNotNull(); assertThat(livenessProbeInitialDelaySeconds.intValue()).isEqualTo(3); Integer readinessProbeInitialDelaySeconds = readinessProbe.getInitialDelaySeconds(); assertThat(readinessProbeInitialDelaySeconds).isNotNull(); assertThat(readinessProbeInitialDelaySeconds.intValue()).isEqualTo(1); } @Test public void createCommandProbeGlobalPropertyOverride() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setProbeType(ProbeType.COMMAND); kubernetesDeployerProperties.setReadinessCommandProbeCommand("ls /"); kubernetesDeployerProperties.setReadinessCommandProbeDelay(1); kubernetesDeployerProperties.setReadinessCommandProbePeriod(2); kubernetesDeployerProperties.setLivenessCommandProbeCommand("ls /dev"); kubernetesDeployerProperties.setLivenessCommandProbeDelay(3); kubernetesDeployerProperties.setLivenessCommandProbePeriod(4); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); Map appProperties = new HashMap<>(); appProperties.put("spring.cloud.deployer.kubernetes.probeType", ProbeType.COMMAND.name()); appProperties.put("spring.cloud.deployer.kubernetes.readinessCommandProbeCommand", "ls /"); appProperties.put("spring.cloud.deployer.kubernetes.readinessProbePeriod", "11"); appProperties.put("spring.cloud.deployer.kubernetes.readinessProbeDelay", "12"); appProperties.put("spring.cloud.deployer.kubernetes.livenessCommandProbeCommand", "ls /dev"); appProperties.put("spring.cloud.deployer.kubernetes.livenessProbePeriod", "13"); appProperties.put("spring.cloud.deployer.kubernetes.livenessProbeDelay", "14"); appProperties.put("spring.cloud.deployer.kubernetes.startupCommandProbeCommand", "ls /dev"); AppDefinition definition = new AppDefinition("app-test", appProperties); Resource resource = getResource(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, appProperties); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); Probe livenessProbe = container.getLivenessProbe(); Probe readinessProbe = container.getReadinessProbe(); assertThat(livenessProbe).isNotNull(); assertThat(readinessProbe).isNotNull(); String livenessTcpProbeCommand = String.join(" ", livenessProbe.getExec().getCommand()); assertThat(livenessTcpProbeCommand).isNotNull(); assertThat(livenessTcpProbeCommand).isEqualTo("ls /dev"); String readinessTcpProbeCommand = String.join(" ", readinessProbe.getExec().getCommand()); assertThat(readinessTcpProbeCommand).isNotNull(); assertThat(readinessTcpProbeCommand).isEqualTo("ls /"); Integer livenessProbePeriodSeconds = livenessProbe.getPeriodSeconds(); assertThat(livenessProbePeriodSeconds).isNotNull(); assertThat(livenessProbePeriodSeconds.intValue()).isEqualTo(13); Integer readinessProbePeriodSeconds = readinessProbe.getPeriodSeconds(); assertThat(readinessProbePeriodSeconds).isNotNull(); assertThat(readinessProbePeriodSeconds.intValue()).isEqualTo(11); Integer livenessProbeInitialDelaySeconds = livenessProbe.getInitialDelaySeconds(); assertThat(livenessProbeInitialDelaySeconds).isNotNull(); assertThat(livenessProbeInitialDelaySeconds.intValue()).isEqualTo(14); Integer readinessProbeInitialDelaySeconds = readinessProbe.getInitialDelaySeconds(); assertThat(readinessProbeInitialDelaySeconds).isNotNull(); assertThat(readinessProbeInitialDelaySeconds.intValue()).isEqualTo(12); } @Test public void testCommandLineArgsOverridesExistingProperties() { AppDefinition definition = new AppDefinition("app-test", Collections.singletonMap("foo", "bar")); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), null, Collections.singletonList("--foo=newValue")); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); assertThat(defaultContainerFactory.createCommandArgs(appDeploymentRequest)).containsExactly("--foo=newValue"); } @Test public void testCommandLineArgsNoAssignment() { List args = new ArrayList(); args.add("a"); args.add("--b = c"); args.add("d=e"); args.add("f = g"); AppDefinition definition = new AppDefinition("app-test", Collections.singletonMap("b", "d")); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), null, args); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); assertThat(defaultContainerFactory.createCommandArgs(appDeploymentRequest)).containsExactly("a", "--b = c", "d=e", "f = g"); } @Test public void testCommandLineArgsExcludesMalformedProperties() { Map properties = new HashMap<>(); properties.put("sun.cpu.isalist", ""); properties.put("foo", "bar"); AppDefinition definition = new AppDefinition("app-test", properties); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource()); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); assertThat(defaultContainerFactory.createCommandArgs(appDeploymentRequest)).containsExactly("--foo=bar"); } private Resource getResource() { return new DockerResource( "springcloud/spring-cloud-deployer-spi-test-app:latest"); } private Secret randomSecret() { String secretName = "secret-" + UUID.randomUUID().toString().substring(0, 18); String secretValue = "dXNlcjpwYXNz"; // base64 encoded string of: user:pass ObjectMeta objectMeta = new ObjectMeta(); objectMeta.setName(secretName); Secret secret = new Secret(); secret.setData(Collections.singletonMap(HttpProbeCreator.PROBE_CREDENTIALS_SECRET_KEY_NAME, secretValue)); secret.setMetadata(objectMeta); return secret; } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/test/java/org/springframework/cloud/deployer/spi/kubernetes/DeploymentPropertiesResolverTests.java ================================================ /* * Copyright 2021-2021 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.cloud.deployer.spi.kubernetes; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import static org.assertj.core.api.Assertions.assertThat; /** * Test the {@link DeploymentPropertiesResolver} class * @author Glenn Renfro */ public class DeploymentPropertiesResolverTests { @ParameterizedTest @ValueSource(booleans = {true, false}) public void testRestartPolicy(boolean isDeprecated) { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DeploymentPropertiesResolver deploymentPropertiesResolver = getDeploymentPropertiesResolver(isDeprecated, kubernetesDeployerProperties); Map properties = new HashMap<>(); RestartPolicy restartPolicy = deploymentPropertiesResolver.getRestartPolicy(properties); assertThat(restartPolicy).isEqualTo(RestartPolicy.Always); if (isDeprecated) { properties.put(KubernetesSchedulerProperties.KUBERNETES_SCHEDULER_PROPERTIES_PREFIX + ".restartPolicy", RestartPolicy.Never.name()); } else { properties.put(KubernetesDeployerProperties.KUBERNETES_DEPLOYER_PROPERTIES_PREFIX + ".restartPolicy", RestartPolicy.Never.name()); } restartPolicy = deploymentPropertiesResolver.getRestartPolicy(properties); assertThat(restartPolicy).isEqualTo(RestartPolicy.Never); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testTaskServiceAccountName(boolean isDeprecated) { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DeploymentPropertiesResolver deploymentPropertiesResolver = getDeploymentPropertiesResolver(isDeprecated, kubernetesDeployerProperties); Map properties = new HashMap<>(); String taskServiceAccountName = deploymentPropertiesResolver.getTaskServiceAccountName(properties); assertThat(taskServiceAccountName).isEqualTo("default"); if (isDeprecated) { properties.put(KubernetesSchedulerProperties.KUBERNETES_SCHEDULER_PROPERTIES_PREFIX + ".taskServiceAccountName", "FOO"); } else { properties.put(KubernetesDeployerProperties.KUBERNETES_DEPLOYER_PROPERTIES_PREFIX + ".task-service-account-name", "FOO"); } taskServiceAccountName = deploymentPropertiesResolver.getTaskServiceAccountName(properties); assertThat(taskServiceAccountName).isEqualTo("FOO"); } private DeploymentPropertiesResolver getDeploymentPropertiesResolver(boolean isDeprecated, KubernetesDeployerProperties properties) { String propertiesPrefix = (isDeprecated) ? KubernetesSchedulerProperties.KUBERNETES_SCHEDULER_PROPERTIES_PREFIX : KubernetesDeployerProperties.KUBERNETES_DEPLOYER_PROPERTIES_PREFIX; return new DeploymentPropertiesResolver(propertiesPrefix, properties); } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/test/java/org/springframework/cloud/deployer/spi/kubernetes/EntryPointStyleTests.java ================================================ /* * Copyright 2019-2021 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.cloud.deployer.spi.kubernetes; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** * Tests for {@link EntryPointStyle} * * @author Chris Schaefer */ public class EntryPointStyleTests { @Test public void testInvalidEntryPointStyleDefaulting() { EntryPointStyle entryPointStyle = EntryPointStyle .relaxedValueOf("unknown"); assertThat(entryPointStyle).isEqualTo(EntryPointStyle.exec); } @Test public void testMatchEntryPointStyle() { EntryPointStyle entryPointStyle = EntryPointStyle .relaxedValueOf("shell"); assertThat(entryPointStyle).isEqualTo(EntryPointStyle.shell); } @Test public void testMixedCaseEntryPointStyle() { EntryPointStyle entryPointStyle = EntryPointStyle .relaxedValueOf("bOOt"); assertThat(entryPointStyle).isEqualTo(EntryPointStyle.boot); } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/test/java/org/springframework/cloud/deployer/spi/kubernetes/ImagePullPolicyTests.java ================================================ /* * Copyright 2016-2021 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.cloud.deployer.spi.kubernetes; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link ImagePullPolicy}. * * @author Moritz Schulze */ public class ImagePullPolicyTests { @Test public void relaxedValueOf_ignoresCase() throws Exception { ImagePullPolicy pullPolicy = ImagePullPolicy.relaxedValueOf("aLWays"); assertThat(pullPolicy).isEqualTo(ImagePullPolicy.Always); } @Test public void relaxedValueOf_parsesValueWithDashesInsteadOfCamelCase() throws Exception { ImagePullPolicy pullPolicy = ImagePullPolicy.relaxedValueOf("if-not-present"); assertThat(pullPolicy).isEqualTo(ImagePullPolicy.IfNotPresent); } @Test public void relaxedValueOf_returnsNullIfValueNotParseable() throws Exception { ImagePullPolicy pullPolicy = ImagePullPolicy.relaxedValueOf("not-a-real-policy"); assertThat(pullPolicy).isNull(); } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/test/java/org/springframework/cloud/deployer/spi/kubernetes/KubernetesActuatorTemplateTests.java ================================================ /* * Copyright 2022 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.cloud.deployer.spi.kubernetes; import java.io.IOException; import java.net.ServerSocket; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.cloud.deployer.spi.app.ActuatorOperations; import org.springframework.cloud.deployer.spi.app.AppAdmin; import org.springframework.cloud.deployer.spi.app.AppDeployer; import org.springframework.cloud.deployer.spi.app.AppInstanceStatus; import org.springframework.cloud.deployer.spi.app.AppStatus; import org.springframework.cloud.deployer.spi.app.DeploymentState; import org.springframework.http.HttpEntity; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class KubernetesActuatorTemplateTests { private final RestTemplate restTemplate = mock(RestTemplate.class); private final AppDeployer appDeployer = mock(AppDeployer.class); private final ActuatorOperations actuatorOperations = new KubernetesActuatorTemplate( restTemplate, appDeployer, new AppAdmin()); private AppInstanceStatus appInstanceStatus; @BeforeEach void setUp() { appInstanceStatus = mock(AppInstanceStatus.class); int port = findRandomOpenPort(); Map attributes = new HashMap<>(); attributes.put("pod.ip", "127.0.0.1"); attributes.put("actuator.port", String.valueOf(port)); attributes.put("actuator.path", "/actuator"); attributes.put("guid", "test-application-0"); when(appInstanceStatus.getAttributes()).thenReturn(attributes); when(appInstanceStatus.getState()).thenReturn(DeploymentState.deployed); AppStatus appStatus = AppStatus.of("test-application-id") .with(appInstanceStatus) .build(); when(appDeployer.status(anyString())).thenReturn(appStatus); // Mock the actual call to RestTemplate Map appInfo = new HashMap<>(); Map appDetails = new HashMap<>(); appDetails.put("name", "log-sink-rabbit"); appInfo.put("app", appDetails); // restTemplate.exchange(url, HttpMethod.GET, new HttpEntity(requestHeaders), responseType) MultiValueMap header = new LinkedMultiValueMap<>(); header.add("Content-Type", "application/json"); header.add("Accept", "application/json"); when(restTemplate.exchange(getUrl(port) + "/actuator/info",HttpMethod.GET,new HttpEntity<>(header), Map.class)) .thenReturn(new ResponseEntity<>(Collections.singletonMap("app", Collections.singletonMap("name", "log-sink-rabbit")), HttpStatus.OK)); when(restTemplate.exchange(getUrl(port) + "/actuator/health",HttpMethod.GET,new HttpEntity<>(header), Map.class)) .thenReturn(new ResponseEntity<>(Collections.singletonMap("app", Collections.singletonMap("status", "UP")), HttpStatus.OK)); when(restTemplate.exchange(getUrl(port) + "/actuator/bindings",HttpMethod.GET,new HttpEntity<>(header), List.class)) .thenReturn(new ResponseEntity<>(Collections.singletonList(Collections.singletonMap("bindingName", "input")), HttpStatus.OK)); when(restTemplate.exchange(getUrl(port) + "/actuator/bindings/input",HttpMethod.GET,new HttpEntity<>(header), Map.class)) .thenReturn(new ResponseEntity<>(Collections.singletonMap("bindingName", "input"), HttpStatus.OK)); when(restTemplate.exchange(getUrl(port) + "/actuator/bindings/input", HttpMethod.POST, new HttpEntity<>(Collections.singletonMap("state", "STOPPED"), header), Map.class)) .thenReturn(new ResponseEntity<>(Collections.singletonMap("state", "STOPPED"), HttpStatus.OK)); ; } @Test void actuatorInfo() { Map info = actuatorOperations .getFromActuator("test-application-id", "test-application-0", "/info", Map.class); assertThat(((Map) (info.get("app"))).get("name")).isEqualTo("log-sink-rabbit"); } @Test void actuatorBindings() { List bindings = actuatorOperations .getFromActuator("test-application-id", "test-application-0", "/bindings", List.class); assertThat(((Map) (bindings.get(0))).get("bindingName")).isEqualTo("input"); } @Test void actuatorBindingInput() { Map binding = actuatorOperations .getFromActuator("test-application-id", "test-application-0", "/bindings/input", Map.class); assertThat(binding.get("bindingName")).isEqualTo("input"); } @Test void actuatorPostBindingInput() { Map state = actuatorOperations .postToActuator("test-application-id", "test-application-0", "/bindings/input", Collections.singletonMap("state", "STOPPED"), Map.class); assertThat(state.get("state")).isEqualTo("STOPPED"); } @Test void noInstanceDeployed() { when(appInstanceStatus.getState()).thenReturn(DeploymentState.failed); assertThatThrownBy(() -> { actuatorOperations .getFromActuator("test-application-id", "test-application-0", "/info", Map.class); }).isInstanceOf(IllegalStateException.class).hasMessageContaining("not deployed"); } public static String getUrl(int port) { return "http://127.0.0.1:" + port; } public static int findRandomOpenPort() { try (ServerSocket socket = new ServerSocket(0)) { socket.setReuseAddress(true); return socket.getLocalPort(); } catch (IOException e) { throw new RuntimeException("Failed to find a random open port", e); } } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/test/java/org/springframework/cloud/deployer/spi/kubernetes/KubernetesAppDeployerIntegrationIT.java ================================================ /* * Copyright 2016-2023 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.cloud.deployer.spi.kubernetes; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.lang.reflect.Type; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.stream.Collectors; import java.util.stream.Stream; import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.Container; import io.fabric8.kubernetes.api.model.ContainerBuilder; import io.fabric8.kubernetes.api.model.EnvFromSource; import io.fabric8.kubernetes.api.model.EnvVar; import io.fabric8.kubernetes.api.model.HostPathVolumeSource; import io.fabric8.kubernetes.api.model.HostPathVolumeSourceBuilder; import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.PersistentVolumeClaim; import io.fabric8.kubernetes.api.model.PersistentVolumeClaimSpec; import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.api.model.PodSecurityContext; import io.fabric8.kubernetes.api.model.PodSecurityContextBuilder; import io.fabric8.kubernetes.api.model.PodSpec; import io.fabric8.kubernetes.api.model.SELinuxOptions; import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.api.model.SecurityContext; import io.fabric8.kubernetes.api.model.SecurityContextBuilder; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.ServiceAccount; import io.fabric8.kubernetes.api.model.ServiceAccountBuilder; import io.fabric8.kubernetes.api.model.ServicePort; import io.fabric8.kubernetes.api.model.Volume; import io.fabric8.kubernetes.api.model.VolumeBuilder; import io.fabric8.kubernetes.api.model.VolumeMount; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.StatefulSet; import io.fabric8.kubernetes.api.model.apps.StatefulSetSpec; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.dsl.ExecListener; import io.fabric8.kubernetes.client.dsl.ExecWatch; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.mockito.stubbing.Answer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.deployer.resource.docker.DockerResource; import org.springframework.cloud.deployer.spi.app.AppAdmin; import org.springframework.cloud.deployer.spi.app.AppDeployer; import org.springframework.cloud.deployer.spi.app.AppInstanceStatus; import org.springframework.cloud.deployer.spi.app.AppScaleRequest; import org.springframework.cloud.deployer.spi.app.AppStatus; import org.springframework.cloud.deployer.spi.app.DeploymentState; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.test.AbstractAppDeployerIntegrationJUnit5Tests; import org.springframework.cloud.deployer.spi.test.Timeout; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.io.Resource; import org.springframework.http.HttpEntity; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.util.StringUtils; import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestTemplate; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.entry; import static org.assertj.core.api.Assertions.tuple; import static org.awaitility.Awaitility.await; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; /** * Integration tests for {@link KubernetesAppDeployer}. * * @author Thomas Risberg * @author Donovan Muller * @author David Turanski * @author Chris Schaefer * @author Christian Tzolov * @author Chris Bono * @author Corneil du Plessis */ @SpringBootTest(classes = {KubernetesAutoConfiguration.class}, properties = { "logging.level.org.springframework.cloud.deployer=DEBUG" }) public class KubernetesAppDeployerIntegrationIT extends AbstractAppDeployerIntegrationJUnit5Tests { @Autowired private AppDeployer appDeployer; @Autowired private KubernetesClient kubernetesClient; @Override protected AppDeployer provideAppDeployer() { return appDeployer; } @BeforeEach public void setup() { if (kubernetesClient.getNamespace() == null) { kubernetesClient.getConfiguration().setNamespace("default"); } } @Test public void testScaleStatefulSet() { log.info("Testing {}...", "ScaleStatefulSet"); KubernetesAppDeployer appDeployer = kubernetesAppDeployer(); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); Map props = new HashMap<>(); props.put(AppDeployer.COUNT_PROPERTY_KEY, "3"); props.put(AppDeployer.INDEXED_PROPERTY_KEY, "true"); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, props); log.info("Deploying {}...", request.getDefinition().getName()); Timeout timeout = deploymentTimeout(); String deploymentId = appDeployer.deploy(request); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); // assertThat(deploymentId, eventually(appInstanceCount(is(3)))); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getInstances()).hasSize(3)); // Ensure that a StatefulSet is deployed Map selector = Collections.singletonMap(AbstractKubernetesDeployer.SPRING_APP_KEY, deploymentId); List statefulSets = kubernetesClient.apps().statefulSets().withLabels(selector).list().getItems(); assertThat(statefulSets).isNotNull(); assertThat(statefulSets).hasSize(1); StatefulSet statefulSet = statefulSets.get(0); StatefulSetSpec statefulSetSpec = statefulSet.getSpec(); assertThat(statefulSetSpec.getPodManagementPolicy()).isEqualTo("Parallel"); assertThat(statefulSetSpec.getReplicas()).isEqualTo(3); assertThat(statefulSetSpec.getServiceName()).isEqualTo(deploymentId); assertThat(statefulSet.getMetadata().getName()).isEqualTo(deploymentId); log.info("Scale Down {}...", request.getDefinition().getName()); appDeployer.scale(new AppScaleRequest(deploymentId, 1)); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getInstances()).hasSize(1)); statefulSets = kubernetesClient.apps().statefulSets().withLabels(selector).list().getItems(); assertThat(statefulSets).hasSize(1); statefulSetSpec = statefulSets.get(0).getSpec(); assertThat(statefulSetSpec.getReplicas()).isEqualTo(1); assertThat(statefulSetSpec.getServiceName()).isEqualTo(deploymentId); assertThat(statefulSet.getMetadata().getName()).isEqualTo(deploymentId); appDeployer.undeploy(deploymentId); } @Test public void testScaleDeployment() { log.info("Testing {}...", "ScaleDeployment"); KubernetesAppDeployer appDeployer = kubernetesAppDeployer(); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, Collections.emptyMap()); log.info("Deploying {}...", request.getDefinition().getName()); Timeout timeout = deploymentTimeout(); String deploymentId = appDeployer.deploy(request); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getInstances()).hasSize(1)); log.info("Scale Up {}...", request.getDefinition().getName()); appDeployer.scale(new AppScaleRequest(deploymentId, 3)); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getInstances()).hasSize(3)); log.info("Scale Down {}...", request.getDefinition().getName()); appDeployer.scale(new AppScaleRequest(deploymentId, 1)); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getInstances()).hasSize(1)); appDeployer.undeploy(deploymentId); } @Test public void testScaleWithNonExistingApps() { assertThatThrownBy(() -> appDeployer.scale(new AppScaleRequest("Fake App", 10))) .isInstanceOf(IllegalStateException.class); } @Test public void testFailedDeploymentWithLoadBalancer() { log.info("Testing {}...", "FailedDeploymentWithLoadBalancer"); KubernetesDeployerProperties deployProperties = new KubernetesDeployerProperties(); deployProperties.setCreateLoadBalancer(true); deployProperties.setLivenessHttpProbePeriod(10); deployProperties.setMaxTerminatedErrorRestarts(1); deployProperties.setMaxCrashLoopBackOffRestarts(1); KubernetesAppDeployer lbAppDeployer = kubernetesAppDeployer(deployProperties); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.livenessHttpProbePath", "/invalidpath"); props.put("spring.cloud.deployer.kubernetes.livenessHttpProbeDelay", "1"); props.put("spring.cloud.deployer.kubernetes.livenessHttpProbePeriod", "1"); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, props); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = lbAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.failed)); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); lbAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); } @Test public void testGoodDeploymentWithLoadBalancer() { log.info("Testing {}...", "GoodDeploymentWithLoadBalancer"); KubernetesDeployerProperties deployProperties = new KubernetesDeployerProperties(); deployProperties.setCreateLoadBalancer(true); deployProperties.setMinutesToWaitForLoadBalancer(1); KubernetesAppDeployer lbAppDeployer = kubernetesAppDeployer(deployProperties); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = lbAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); lbAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); } @Test @Disabled("Disabled until we can test lbs") public void testDeploymentWithLoadBalancerHasUrlAndAnnotation() { log.info("Testing {}...", "DeploymentWithLoadBalancerShowsUrl"); KubernetesDeployerProperties deployProperties = new KubernetesDeployerProperties(); deployProperties.setCreateLoadBalancer(true); deployProperties.setMinutesToWaitForLoadBalancer(1); KubernetesAppDeployer lbAppDeployer = kubernetesAppDeployer(deployProperties); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, Collections.singletonMap("spring.cloud.deployer.kubernetes.serviceAnnotations", "foo:bar,fab:baz")); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = lbAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); log.info("Checking instance attributes of {}...", request.getDefinition().getName()); AppStatus status = lbAppDeployer.status(deploymentId); for (String inst : status.getInstances().keySet()) { appDeployer().status(deploymentId).getInstances().get(inst); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getInstances().get(inst).getAttributes()).containsKey("url")); } log.info("Checking service annotations of {}...", request.getDefinition().getName()); Map annotations = kubernetesClient.services().withName(request.getDefinition().getName()).get() .getMetadata().getAnnotations(); assertThat(annotations).isNotNull(); assertThat(annotations).hasSize(2); assertThat(annotations).containsKey("foo"); assertThat(annotations.get("foo")).isEqualTo("bar"); assertThat(annotations).containsKey("fab"); assertThat(annotations.get("fab")).isEqualTo("baz"); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); lbAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); } @Test public void testAttributes() { log.info("Testing {}...", "Attributes"); KubernetesDeployerProperties properties = new KubernetesDeployerProperties(); AppAdmin appAdmin = new AppAdmin(); appAdmin.setUser("user"); appAdmin.setPassword("password"); properties.setAppAdmin(appAdmin); KubernetesAppDeployer appDeployer = kubernetesAppDeployer(properties); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = appDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer.status(deploymentId).getInstances().values().stream() .map(AppInstanceStatus::getAttributes).filter(a -> StringUtils.hasText(a.get("pod.ip"))) .findFirst()).isPresent(); assertThat(appDeployer.status(deploymentId).getInstances().values().stream() .map(AppInstanceStatus::getAttributes).filter(a -> StringUtils.hasText(a.get("actuator.path"))) .map(a -> a.get("actuator.path")) .findFirst().orElse(null)).isEqualTo("/actuator"); assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); } @Test public void testDeploymentWithPodAnnotation() { log.info("Testing {}...", "DeploymentWithPodAnnotation"); KubernetesAppDeployer appDeployer = kubernetesAppDeployer(); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, Collections.singletonMap("spring.cloud.deployer.kubernetes.podAnnotations", "iam.amazonaws.com/role:role-arn,foo:bar")); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = appDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); log.info("Checking pod spec annotations of {}...", request.getDefinition().getName()); List pods = kubernetesClient.pods().withLabel("spring-deployment-id", request.getDefinition() .getName()).list().getItems(); assertThat(pods).hasSize(1); Pod pod = pods.get(0); Map annotations = pod.getMetadata().getAnnotations(); log.info("Number of annotations found" + annotations.size()); for (Map.Entry annotationsEntry : annotations.entrySet()) { log.info("Annotation key: " + annotationsEntry.getKey()); } assertThat(annotations.get("iam.amazonaws.com/role")).isEqualTo("role-arn"); assertThat(annotations.get("foo")).isEqualTo("bar"); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); } @Test public void testDeploymentWithMountedHostPathVolume() throws IOException { log.info("Testing {}...", "DeploymentWithMountedVolume"); String containerPath = "/tmp/"; String subPath = randomName(); String mountName = "mount"; HostPathVolumeSource hostPathVolumeSource = new HostPathVolumeSourceBuilder() .withPath("/tmp/" + randomName() + '/').build(); KubernetesDeployerProperties deployProperties = new KubernetesDeployerProperties(); deployProperties.setVolumes(Collections.singletonList(new VolumeBuilder() .withHostPath(hostPathVolumeSource) .withName(mountName) .build())); deployProperties.setVolumeMounts(Collections.singletonList(new VolumeMount(hostPathVolumeSource.getPath(), null, mountName, false, null, null, null))); KubernetesAppDeployer lbAppDeployer = kubernetesAppDeployer(deployProperties); AppDefinition definition = new AppDefinition(randomName(), Collections.singletonMap("logging.file", containerPath + subPath)); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = lbAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); Map selector = Collections.singletonMap(AbstractKubernetesDeployer.SPRING_APP_KEY, deploymentId); PodSpec spec = kubernetesClient.pods().withLabels(selector).list().getItems().get(0).getSpec(); assertThat(spec.getVolumes()).isNotNull(); Volume volume = spec.getVolumes().stream() .filter(v -> mountName.equals(v.getName())) .findAny() .orElseThrow(() -> new AssertionError("Volume not mounted")); assertThat(volume.getHostPath()).isNotNull(); assertThat(hostPathVolumeSource.getPath()).isEqualTo(volume.getHostPath().getPath()); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); lbAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); } private void verifyAppEnv(String appId) { String ip = ""; int port = 0; KubernetesDeployerProperties properties = new KubernetesDeployerProperties(); boolean success = false; Service svc = kubernetesClient.services().withName(appId).get(); RestTemplate restTemplate = new RestTemplate(); if (svc != null && "LoadBalancer".equals(svc.getSpec().getType())) { int tries = 0; int maxWait = properties.getMinutesToWaitForLoadBalancer() * 6; // we check 6 times per minute while (tries++ < maxWait && !success) { if (svc.getStatus() != null && svc.getStatus().getLoadBalancer() != null && svc.getStatus().getLoadBalancer().getIngress() != null && !(svc.getStatus().getLoadBalancer().getIngress().isEmpty())) { ip = svc.getStatus().getLoadBalancer().getIngress().get(0).getIp(); if (ip == null) { ip = svc.getStatus().getLoadBalancer().getIngress().get(0).getHostname(); } port = svc.getSpec().getPorts().get(0).getPort(); success = true; try { String url = String.format("http://%s:%d/actuator/env", ip, port); restTemplate.exchange(url, HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>>() { @Override public Type getType() { return Map.class; } }); } catch (ResourceAccessException rae) { success = false; try { Thread.sleep(5000L); } catch (InterruptedException e) { } } } else { try { Thread.sleep(5000L); } catch (InterruptedException e) { } svc = kubernetesClient.services().withName(appId).get(); } } log.debug(String.format("LoadBalancer Ingress: %s", svc.getStatus().getLoadBalancer().getIngress().toString())); } assertThat(success).as("cannot get service information for " + appId).isFalse(); String url = String.format("http://%s:%d/actuator/env", ip, port); log.debug("getting app environment from " + url); restTemplate = new RestTemplate(); ResponseEntity>> response = restTemplate.exchange(url, HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>>() { @Override public Type getType() { return Map.class; } }); LinkedHashMap> env = response.getBody(); ArrayList propertySources = env.get("propertySources"); String hostName = null; String instanceIndex = null; for (LinkedHashMap propertySource : propertySources) { if (propertySource.get("name").equals("systemEnvironment")) { LinkedHashMap s = (LinkedHashMap) propertySource.get("properties"); hostName = (String) ((LinkedHashMap) s.get("HOSTNAME")).get("value"); } if (propertySource.get("name").equals("applicationConfig: [file:./config/application.properties]")) { LinkedHashMap s = (LinkedHashMap) propertySource.get("properties"); instanceIndex = (String) ((LinkedHashMap) s.get("INSTANCE_INDEX")).get("value"); } } assertThat(hostName).as("Hostname is null").isNotNull(); assertThat(instanceIndex).as("Instance index is null").isNotNull(); String expectedIndex = hostName.substring(hostName.lastIndexOf("-") + 1); assertThat(instanceIndex).isEqualTo(expectedIndex); } @Test @Disabled("Disabled until we can test lbs") public void testDeploymentWithGroupAndIndex() throws IOException { log.info("Testing {}...", "DeploymentWithWithGroupAndIndex"); KubernetesDeployerProperties deployProperties = new KubernetesDeployerProperties(); deployProperties.setCreateLoadBalancer(true); deployProperties.setMinutesToWaitForLoadBalancer(1); KubernetesAppDeployer testAppDeployer = kubernetesAppDeployer(deployProperties); Map appProperties = new HashMap<>(); appProperties.put("security.basic.enabled", "false"); appProperties.put("management.security.enabled", "false"); AppDefinition definition = new AppDefinition(randomName(), appProperties); Resource resource = testApplication(); Map props = new HashMap<>(); props.put(AppDeployer.GROUP_PROPERTY_KEY, "foo"); props.put(AppDeployer.INDEXED_PROPERTY_KEY, "true"); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, props); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = testAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); Map selector = Collections.singletonMap(AbstractKubernetesDeployer.SPRING_APP_KEY, deploymentId); PodSpec spec = kubernetesClient.pods().withLabels(selector).list().getItems().get(0).getSpec(); Map envVars = new HashMap<>(); for (EnvVar e : spec.getContainers().get(0).getEnv()) { envVars.put(e.getName(), e.getValue()); } assertThat(envVars).contains(entry("SPRING_CLOUD_APPLICATION_GROUP", "foo")); verifyAppEnv(deploymentId); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); testAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); } @Test public void testDeploymentServiceAccountName() { log.info("Testing {}...", "DeploymentServiceAccountName"); ServiceAccount deploymentServiceAccount = new ServiceAccountBuilder().withNewMetadata().withName("appsa") .endMetadata().build(); this.kubernetesClient.serviceAccounts().inNamespace("default").resource(deploymentServiceAccount).create(); String serviceAccountName = deploymentServiceAccount.getMetadata().getName(); KubernetesDeployerProperties deployProperties = new KubernetesDeployerProperties(); deployProperties.setDeploymentServiceAccountName(serviceAccountName); ContainerFactory containerFactory = new DefaultContainerFactory(deployProperties); KubernetesAppDeployer appDeployer = kubernetesAppDeployer(deployProperties); AppDefinition definition = new AppDefinition(randomName(), null); AppDeploymentRequest request = new AppDeploymentRequest(definition, testApplication()); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = appDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); kubernetesClient.serviceAccounts().inNamespace("default").resource(deploymentServiceAccount).delete(); } @Test public void testCreateStatefulSet() throws Exception { Map props = new HashMap<>(); props.put(AppDeployer.COUNT_PROPERTY_KEY, "3"); props.put(AppDeployer.INDEXED_PROPERTY_KEY, "true"); AppDefinition definition = new AppDefinition(randomName(), null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, testApplication(), props); KubernetesAppDeployer deployer = kubernetesAppDeployer(); log.info("Deploying {}...", appDeploymentRequest.getDefinition().getName()); String deploymentId = deployer.deploy(appDeploymentRequest); Map idMap = deployer.createIdMap(deploymentId, appDeploymentRequest); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); Map selector = Collections.singletonMap(AbstractKubernetesDeployer.SPRING_APP_KEY, deploymentId); List statefulSets = kubernetesClient.apps().statefulSets().withLabels(selector).list().getItems(); assertThat(statefulSets).hasSize(1); StatefulSet statefulSet = statefulSets.get(0); StatefulSetSpec statefulSetSpec = statefulSet.getSpec(); List statefulSetInitContainers = statefulSetSpec.getTemplate().getSpec().getInitContainers(); assertThat(statefulSetInitContainers).hasSize(1); Container statefulSetInitContainer = statefulSetInitContainers.get(0); assertThat(statefulSetInitContainer.getImage()).isEqualTo(DeploymentPropertiesResolver.STATEFUL_SET_IMAGE_NAME); assertThat(statefulSetInitContainer.getSecurityContext()).isNull(); assertThat(statefulSetSpec.getPodManagementPolicy()).isEqualTo("Parallel"); assertThat(statefulSetSpec.getReplicas()).isEqualTo(3); assertThat(statefulSetSpec.getServiceName()).isEqualTo(deploymentId); assertThat(statefulSet.getMetadata().getName()).isEqualTo(deploymentId); assertThat(statefulSetSpec.getSelector().getMatchLabels()) .containsAllEntriesOf(deployer.createIdMap(deploymentId, appDeploymentRequest)); assertThat(statefulSetSpec.getSelector().getMatchLabels()) .contains(entry(KubernetesAppDeployer.SPRING_MARKER_KEY, KubernetesAppDeployer.SPRING_MARKER_VALUE)); assertThat(statefulSetSpec.getTemplate().getMetadata().getLabels()).containsAllEntriesOf(idMap); assertThat(statefulSetSpec.getTemplate().getMetadata().getLabels()) .contains(entry(KubernetesAppDeployer.SPRING_MARKER_KEY, KubernetesAppDeployer.SPRING_MARKER_VALUE)); Container container = statefulSetSpec.getTemplate().getSpec().getContainers().get(0); assertThat(container.getName()).isEqualTo(deploymentId); assertThat(container.getPorts().get(0).getContainerPort()).isEqualTo(8080); assertThat(container.getImage()).isEqualTo(testApplication().getURI().getSchemeSpecificPart()); PersistentVolumeClaim pvc = statefulSetSpec.getVolumeClaimTemplates().get(0); assertThat(pvc.getMetadata().getName()).isEqualTo(deploymentId); PersistentVolumeClaimSpec pvcSpec = pvc.getSpec(); assertThat(pvcSpec.getAccessModes()).containsOnly("ReadWriteOnce"); assertThat(pvcSpec.getStorageClassName()).isNull(); assertThat(pvcSpec.getResources().getLimits().get("storage").getAmount()).isEqualTo("10"); assertThat(pvcSpec.getResources().getRequests().get("storage").getAmount()).isEqualTo("10"); assertThat(pvcSpec.getResources().getLimits().get("storage").getFormat()).isEqualTo("Mi"); assertThat(pvcSpec.getResources().getRequests().get("storage").getFormat()).isEqualTo("Mi"); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); } @Test public void testCreateStatefulSetInitContainerImageNamePropOverride() throws Exception { Map props = new HashMap<>(); props.put(AppDeployer.COUNT_PROPERTY_KEY, "3"); props.put(AppDeployer.INDEXED_PROPERTY_KEY, "true"); String imageName = testApplication().getURI().getSchemeSpecificPart(); props.put("spring.cloud.deployer.kubernetes.statefulSetInitContainerImageName", imageName); AppDefinition definition = new AppDefinition(randomName(), null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, testApplication(), props); KubernetesAppDeployer deployer = kubernetesAppDeployer(); log.info("Deploying {}...", appDeploymentRequest.getDefinition().getName()); String deploymentId = deployer.deploy(appDeploymentRequest); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); Map selector = Collections.singletonMap(AbstractKubernetesDeployer.SPRING_APP_KEY, deploymentId); List statefulSets = kubernetesClient.apps().statefulSets().withLabels(selector).list().getItems(); assertThat(statefulSets).hasSize(1); StatefulSet statefulSet = statefulSets.get(0); StatefulSetSpec statefulSetSpec = statefulSet.getSpec(); List statefulSetInitContainers = statefulSetSpec.getTemplate().getSpec().getInitContainers(); assertThat(statefulSetInitContainers).hasSize(1); Container statefulSetInitContainer = statefulSetInitContainers.get(0); assertThat(statefulSetInitContainer.getImage()).isEqualTo(imageName); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); } @Test public void createStatefulSetInitContainerImageNameGlobalOverride() throws Exception { Map props = new HashMap<>(); props.put(AppDeployer.COUNT_PROPERTY_KEY, "3"); props.put(AppDeployer.INDEXED_PROPERTY_KEY, "true"); AppDefinition definition = new AppDefinition(randomName(), null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, testApplication(), props); String imageName = testApplication().getURI().getSchemeSpecificPart(); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setStatefulSetInitContainerImageName(imageName); KubernetesAppDeployer deployer = kubernetesAppDeployer(kubernetesDeployerProperties); log.info("Deploying {}...", appDeploymentRequest.getDefinition().getName()); String deploymentId = deployer.deploy(appDeploymentRequest); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); Map selector = Collections.singletonMap(AbstractKubernetesDeployer.SPRING_APP_KEY, deploymentId); List statefulSets = kubernetesClient.apps().statefulSets().withLabels(selector).list().getItems(); assertThat(statefulSets).hasSize(1); StatefulSet statefulSet = statefulSets.get(0); StatefulSetSpec statefulSetSpec = statefulSet.getSpec(); List statefulSetInitContainers = statefulSetSpec.getTemplate().getSpec().getInitContainers(); assertThat(statefulSetInitContainers).hasSize(1); Container statefulSetInitContainer = statefulSetInitContainers.get(0); assertThat(statefulSetInitContainer.getImage()).isEqualTo(imageName); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); } @Test public void createStatefulSetWithOverridingRequest() throws Exception { Map props = new HashMap<>(); props.put(AppDeployer.COUNT_PROPERTY_KEY, "3"); props.put(AppDeployer.INDEXED_PROPERTY_KEY, "true"); props.put("spring.cloud.deployer.kubernetes.statefulSet.volumeClaimTemplate.name", "mystorage"); props.put("spring.cloud.deployer.kubernetes.statefulSet.volumeClaimTemplate.storage", "1g"); AppDefinition definition = new AppDefinition(randomName(), null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, testApplication(), props); KubernetesAppDeployer deployer = kubernetesAppDeployer(); log.info("Deploying {}...", appDeploymentRequest.getDefinition().getName()); String deploymentId = deployer.deploy(appDeploymentRequest); Map idMap = deployer.createIdMap(deploymentId, appDeploymentRequest); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); Map selector = Collections.singletonMap(AbstractKubernetesDeployer.SPRING_APP_KEY, deploymentId); StatefulSet statefulSet = kubernetesClient.apps().statefulSets().withLabels(selector).list().getItems().get(0); StatefulSetSpec statefulSetSpec = statefulSet.getSpec(); assertThat(statefulSetSpec.getPodManagementPolicy()).isEqualTo("Parallel"); assertThat(statefulSetSpec.getReplicas()).isEqualTo(3); assertThat(statefulSetSpec.getServiceName()).isEqualTo(deploymentId); assertThat(statefulSet.getMetadata().getName()).isEqualTo(deploymentId); assertThat(statefulSetSpec.getSelector().getMatchLabels()) .containsAllEntriesOf(deployer.createIdMap(deploymentId, appDeploymentRequest)); assertThat(statefulSetSpec.getSelector().getMatchLabels()) .contains(entry(KubernetesAppDeployer.SPRING_MARKER_KEY, KubernetesAppDeployer.SPRING_MARKER_VALUE)); assertThat(statefulSetSpec.getTemplate().getMetadata().getLabels()).containsAllEntriesOf(idMap); assertThat(statefulSetSpec.getTemplate().getMetadata().getLabels()) .contains(entry(KubernetesAppDeployer.SPRING_MARKER_KEY, KubernetesAppDeployer.SPRING_MARKER_VALUE)); Container container = statefulSetSpec.getTemplate().getSpec().getContainers().get(0); assertThat(container.getName()).isEqualTo(deploymentId); assertThat(container.getPorts().get(0).getContainerPort()).isEqualTo(8080); assertThat(container.getImage()).isEqualTo(testApplication().getURI().getSchemeSpecificPart()); PersistentVolumeClaim pvc = statefulSetSpec.getVolumeClaimTemplates().get(0); assertThat(pvc.getMetadata().getName()).isEqualTo("mystorage"); PersistentVolumeClaimSpec pvcSpec = pvc.getSpec(); assertThat(pvcSpec.getAccessModes()).containsOnly("ReadWriteOnce"); assertThat(pvcSpec.getResources().getLimits().get("storage").getAmount()).isEqualTo("1"); assertThat(pvcSpec.getResources().getRequests().get("storage").getAmount()).isEqualTo("1"); assertThat(pvcSpec.getResources().getLimits().get("storage").getFormat()).isEqualTo("Gi"); assertThat(pvcSpec.getResources().getRequests().get("storage").getFormat()).isEqualTo("Gi"); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); } @Test public void createStatefulSetWithPVCDefaultName() throws Exception { Map props = new HashMap<>(); props.put(AppDeployer.COUNT_PROPERTY_KEY, "3"); props.put(AppDeployer.INDEXED_PROPERTY_KEY, "true"); AppDefinition definition = new AppDefinition(randomName(), null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, testApplication(), props); KubernetesAppDeployer deployer = kubernetesAppDeployer(); log.info("Deploying {}...", appDeploymentRequest.getDefinition().getName()); String deploymentId = deployer.deploy(appDeploymentRequest); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); Map selector = Collections.singletonMap(AbstractKubernetesDeployer.SPRING_APP_KEY, deploymentId); StatefulSet statefulSet = kubernetesClient.apps().statefulSets().withLabels(selector).list().getItems().get(0); StatefulSetSpec statefulSetSpec = statefulSet.getSpec(); Container container = statefulSetSpec.getTemplate().getSpec().getContainers().get(0); assertThat(container.getName()).isEqualTo(deploymentId); PersistentVolumeClaim pvc = statefulSetSpec.getVolumeClaimTemplates().get(0); assertThat(pvc.getMetadata().getName()).isEqualTo(deploymentId); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); } @Test public void testStatefulSetPodAnnotations() { log.info("Testing {}...", "StatefulSetPodAnnotations"); KubernetesAppDeployer appDeployer = kubernetesAppDeployer(); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); Map props = new HashMap<>(); props.put(AppDeployer.COUNT_PROPERTY_KEY, "3"); props.put(AppDeployer.INDEXED_PROPERTY_KEY, "true"); props.put("spring.cloud.deployer.kubernetes.podAnnotations", "iam.amazonaws.com/role:role-arn,foo:bar"); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, props); log.info("Deploying {}...", request.getDefinition().getName()); Timeout timeout = deploymentTimeout(); String deploymentId = appDeployer.deploy(request); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getInstances()).hasSize(3)); // Ensure that a StatefulSet is deployed Map selector = Collections.singletonMap(AbstractKubernetesDeployer.SPRING_APP_KEY, deploymentId); List statefulSets = kubernetesClient.apps().statefulSets().withLabels(selector).list().getItems(); assertThat(statefulSets).isNotNull(); assertThat(statefulSets).hasSize(1); StatefulSet statefulSet = statefulSets.get(0); StatefulSetSpec statefulSetSpec = statefulSet.getSpec(); Map annotations = statefulSetSpec.getTemplate().getMetadata().getAnnotations(); assertThat(annotations.get("iam.amazonaws.com/role")).isEqualTo("role-arn"); assertThat(annotations.get("foo")).isEqualTo("bar"); appDeployer.undeploy(deploymentId); } @Test public void testDeploymentLabels() { Map props = Collections.singletonMap("spring.cloud.deployer.kubernetes.deploymentLabels", "label1:value1,label2:value2"); AppDefinition definition = new AppDefinition(randomName(), null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, testApplication(), props); KubernetesAppDeployer deployer = kubernetesAppDeployer(); log.info("Deploying {}...", appDeploymentRequest.getDefinition().getName()); String deploymentId = deployer.deploy(appDeploymentRequest); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); Map selector = Collections.singletonMap(AbstractKubernetesDeployer.SPRING_APP_KEY, deploymentId); List deployments = kubernetesClient.apps().deployments().withLabels(selector).list().getItems(); Map specLabels = deployments.get(0).getSpec().getTemplate().getMetadata().getLabels(); assertThat(specLabels.containsKey("label1")).as("Label 'label1' not found in deployment spec").isTrue(); assertThat(specLabels.get("label1")).as("Unexpected value for label1").isEqualTo("value1"); assertThat(specLabels).as("Label 'label2' not found in deployment spec").containsKey("label2"); assertThat(specLabels.get("label2")).as("Unexpected value for label1").isEqualTo("value2"); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); } @Test public void testDeploymentLabelsStatefulSet() { log.info("Testing {}...", "DeploymentLabelsForStatefulSet"); Map props = new HashMap<>(); props.put(AppDeployer.COUNT_PROPERTY_KEY, "2"); props.put(AppDeployer.INDEXED_PROPERTY_KEY, "true"); props.put("spring.cloud.deployer.kubernetes.deploymentLabels", "stateful-label1:stateful-value1,stateful-label2:stateful-value2"); AppDefinition definition = new AppDefinition(randomName(), null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, testApplication(), props); KubernetesAppDeployer deployer = kubernetesAppDeployer(); log.info("Deploying {}...", appDeploymentRequest.getDefinition().getName()); String deploymentId = deployer.deploy(appDeploymentRequest); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); Map idMap = deployer.createIdMap(deploymentId, appDeploymentRequest); Map selector = Collections.singletonMap(AbstractKubernetesDeployer.SPRING_APP_KEY, deploymentId); StatefulSet statefulSet = kubernetesClient.apps().statefulSets().withLabels(selector).list().getItems().get(0); StatefulSetSpec statefulSetSpec = statefulSet.getSpec(); assertThat(statefulSetSpec.getReplicas()).isEqualTo(2); assertThat(statefulSetSpec.getTemplate().getMetadata().getLabels()).containsAllEntriesOf(idMap); //verify stateful set match labels Map setLabels = statefulSet.getMetadata().getLabels(); assertThat(setLabels).contains(entry("stateful-label1", "stateful-value1"), entry("stateful-label2", "stateful-value2")); //verify pod template labels Map specLabels = statefulSetSpec.getTemplate().getMetadata().getLabels(); assertThat(specLabels).contains(entry("stateful-label1", "stateful-value1"), entry("stateful-label2", "stateful-value2")); //verify that labels got replicated to one of the deployments List pods = kubernetesClient.pods().withLabels(selector).list().getItems(); Map podLabels = pods.get(0).getMetadata().getLabels(); assertThat(podLabels).contains(entry("stateful-label1", "stateful-value1"), entry("stateful-label2", "stateful-value2")); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); } @Test public void testCleanupOnDeployFailure() throws InterruptedException { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, testApplication(), null); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); // simulate a pod going into an un-schedulable state KubernetesDeployerProperties.LimitsResources resources = new KubernetesDeployerProperties.LimitsResources(); resources.setCpu("9000000"); kubernetesDeployerProperties.setLimits(resources); KubernetesAppDeployer deployer = kubernetesAppDeployer(kubernetesDeployerProperties); log.info("Deploying {}...", appDeploymentRequest.getDefinition().getName()); String deploymentId = deployer.deploy(appDeploymentRequest); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); // attempt to undeploy the failed deployment log.info("Undeploying {}...", deploymentId); try { appDeployer.undeploy(deploymentId); } catch (Exception e) { log.info("Got expected not not deployed exception on undeployment: " + e.getMessage()); } deployer = kubernetesAppDeployer(); log.info("Deploying {}... again", deploymentId); // ensure a previous failed deployment with the same name was cleaned up and can be deployed again deployer.deploy(appDeploymentRequest); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); appDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); } @Test public void testMultipleContainersInPod() { log.info("Testing {}...", "MultipleContainersInPod"); KubernetesAppDeployer kubernetesAppDeployer = Mockito.spy(kubernetesAppDeployer()); AppDefinition definition = new AppDefinition(randomName(), Collections.singletonMap("server.port", "9090")); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); doAnswer((Answer) invocationOnMock -> { PodSpec podSpec = (PodSpec) invocationOnMock.callRealMethod(); Container container = new ContainerBuilder().withName("asecondcontainer") .withImage(resource.getURI().getSchemeSpecificPart()).build(); podSpec.getContainers().add(container); return podSpec; }).when(kubernetesAppDeployer).createPodSpec(Mockito.any(AppDeploymentRequest.class)); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); } @Test public void testDefaultServicePort() { log.info("Testing {}...", "DefaultServicePort"); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); List servicePorts = kubernetesClient.services().withName(request.getDefinition().getName()).get() .getSpec().getPorts(); assertThat(servicePorts).hasSize(1); assertThat(servicePorts.get(0).getPort()).isEqualTo(8080); assertThat(servicePorts.get(0).getName()).isEqualTo("port-8080"); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); } @Test public void testDefaultServicePortOverride() { log.info("Testing {}...", "DefaultServicePortOverride"); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(); AppDefinition definition = new AppDefinition(randomName(), Collections.singletonMap("server.port", "9090")); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); List servicePorts = kubernetesClient.services().withName(request.getDefinition().getName()).get() .getSpec().getPorts(); assertThat(servicePorts).hasSize(1); assertThat(servicePorts.get(0).getPort()).isEqualTo(9090); assertThat(servicePorts.get(0).getName()).isEqualTo("port-9090"); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); } @Test public void testServiceWithMultiplePorts() { log.info("Testing {}...", "ServiceWithMultiplePorts"); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, Collections.singletonMap("spring.cloud.deployer.kubernetes.servicePorts", "8080,9090")); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); List servicePorts = kubernetesClient.services().withName(request.getDefinition().getName()).get() .getSpec().getPorts(); assertThat(servicePorts).hasSize(2); assertThat(servicePorts.stream().anyMatch(o -> o.getPort().equals(8080))).isTrue(); assertThat(servicePorts.stream().anyMatch(o -> o.getName().equals("port-8080"))).isTrue(); assertThat(servicePorts.stream().anyMatch(o -> o.getPort().equals(9090))).isTrue(); assertThat(servicePorts.stream().anyMatch(o -> o.getName().equals("port-9090"))).isTrue(); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); } @Test public void testCreateInitContainer() { log.info("Testing {}...", "CreateInitContainer"); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(); Map props = Collections.singletonMap("spring.cloud.deployer.kubernetes.initContainer", "{containerName: 'test', imageName: 'busybox:latest', commands: ['sh', '-c', 'echo hello']}"); AppDefinition definition = new AppDefinition(randomName(), null); AppDeploymentRequest request = new AppDeploymentRequest(definition, testApplication(), props); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); Deployment deployment = kubernetesClient.apps().deployments().withName(request.getDefinition().getName()).get(); List initContainers = deployment.getSpec().getTemplate().getSpec().getInitContainers(); Optional initContainer = initContainers.stream().filter(i -> i.getName().equals("test")).findFirst(); assertThat(initContainer.isPresent()).as("Init container not found").isTrue(); Container testInitContainer = initContainer.get(); assertThat(testInitContainer.getName()).as("Unexpected init container name").isEqualTo("test"); assertThat(testInitContainer.getImage()).as("Unexpected init container image").isEqualTo("busybox:latest"); List commands = testInitContainer.getCommand(); assertThat(commands).contains("sh", "-c", "echo hello"); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); } @Test public void testCreateInitContainerWithEnvVariables() { log.info("Testing {}...", "CreateInitContainer"); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(); Map props = Collections.singletonMap("spring.cloud.deployer.kubernetes.initContainer", "{containerName: 'test', imageName: 'busybox:latest', commands: ['sh', '-c', 'echo hello'], environmentVariables: ['KEY1=VAL1', 'KEY2=VAL2']}"); AppDefinition definition = new AppDefinition(randomName(), null); AppDeploymentRequest request = new AppDeploymentRequest(definition, testApplication(), props); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); Deployment deployment = kubernetesClient.apps().deployments().withName(request.getDefinition().getName()).get(); List initContainers = deployment.getSpec().getTemplate().getSpec().getInitContainers(); Optional initContainer = initContainers.stream().filter(i -> i.getName().equals("test")).findFirst(); assertThat(initContainer.isPresent()).as("Init container not found").isTrue(); Container testInitContainer = initContainer.get(); List containerEnvs = testInitContainer.getEnv(); assertThat(containerEnvs).hasSize(2); assertThat(containerEnvs.stream().map(EnvVar::getName).collect(Collectors.toList())).contains("KEY1", "KEY2"); assertThat(containerEnvs.stream().map(EnvVar::getValue).collect(Collectors.toList())).contains("VAL1", "VAL2"); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); } @Test public void initContainerFromGlobalProps() { // Set up a global initContainer (it should be chosen) KubernetesDeployerProperties.InitContainer globalInitContainerProps = new KubernetesDeployerProperties.InitContainer(); globalInitContainerProps.setName("test-global"); globalInitContainerProps.setImage("busybox:latest"); globalInitContainerProps.setCommand(Arrays.asList("sh", "-c", "echo hello-global")); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setInitContainer(globalInitContainerProps); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(kubernetesDeployerProperties); AppDefinition definition = new AppDefinition(randomName(), null); AppDeploymentRequest request = new AppDeploymentRequest(definition, testApplication(), Collections.emptyMap()); String deploymentId = deploy(kubernetesAppDeployer, request); Deployment deployment = kubernetesClient.apps().deployments().withName(request.getDefinition().getName()).get(); List initContainers = deployment.getSpec().getTemplate().getSpec().getInitContainers(); Optional initContainer = initContainers.stream().filter(i -> i.getName().equals("test-global")).findFirst(); assertThat(initContainer).isPresent(); Container testInitContainer = initContainer.get(); assertThat(testInitContainer.getName()).isEqualTo("test-global"); assertThat(testInitContainer.getImage()).isEqualTo("busybox:latest"); assertThat(testInitContainer.getCommand()).containsExactly("sh", "-c", "echo hello-global"); undeploy(kubernetesAppDeployer, deploymentId); } @Test public void initContainerFromDeployerProps() { // Set up a global mock initContainer (it should not be chosen) KubernetesDeployerProperties.InitContainer globalInitContainerProps = mock(KubernetesDeployerProperties.InitContainer.class); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setInitContainer(globalInitContainerProps); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(kubernetesDeployerProperties); // Set up a deployer props initContainer (it should be chosen) Map deployerProps = Collections.singletonMap("spring.cloud.deployer.kubernetes.initContainer", "{containerName: 'test', imageName: 'busybox:latest', imagePullPolicy: 'IfNotPresent', commands: ['sh', '-c', 'echo hello']" + ", environmentVariables: ['KEY1=VAL1', 'KEY2=VAL2']}"); AppDefinition definition = new AppDefinition(randomName(), null); AppDeploymentRequest request = new AppDeploymentRequest(definition, testApplication(), deployerProps); String deploymentId = deploy(kubernetesAppDeployer, request); Deployment deployment = kubernetesClient.apps().deployments().withName(request.getDefinition().getName()).get(); List initContainers = deployment.getSpec().getTemplate().getSpec().getInitContainers(); Optional initContainer = initContainers.stream().filter(i -> i.getName().equals("test")).findFirst(); assertThat(initContainer).isPresent(); Container testInitContainer = initContainer.get(); assertThat(testInitContainer.getName()).isEqualTo("test"); assertThat(testInitContainer.getImage()).isEqualTo("busybox:latest"); assertThat(testInitContainer.getEnv()).extracting("name", "value") .containsExactlyInAnyOrder(tuple("KEY1", "VAL1"), tuple("KEY2", "VAL2")); assertThat(testInitContainer.getCommand()).containsExactly("sh", "-c", "echo hello"); undeploy(kubernetesAppDeployer, deploymentId); } @Test public void initContainerPartialFromDeployerProps() { // Set up a global mock initContainer (it should not be chosen) KubernetesDeployerProperties.InitContainer globalInitContainerProps = mock(KubernetesDeployerProperties.InitContainer.class); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setInitContainer(globalInitContainerProps); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(kubernetesDeployerProperties); // Set up a deployer props initContainer partial (they should be used) Map deployerProps = new HashMap<>(); deployerProps.put("spring.cloud.deployer.kubernetes.initContainer.containerName", "test"); deployerProps.put("spring.cloud.deployer.kubernetes.initContainer.imageName", "busybox:latest"); deployerProps.put("spring.cloud.deployer.kubernetes.initContainer.commands", "sh,-c,echo hello"); AppDefinition definition = new AppDefinition(randomName(), null); AppDeploymentRequest request = new AppDeploymentRequest(definition, testApplication(), deployerProps); String deploymentId = deploy(kubernetesAppDeployer, request); Deployment deployment = kubernetesClient.apps().deployments().withName(request.getDefinition().getName()).get(); List initContainers = deployment.getSpec().getTemplate().getSpec().getInitContainers(); Optional initContainer = initContainers.stream().filter(i -> i.getName().equals("test")).findFirst(); assertThat(initContainer).isPresent(); Container testInitContainer = initContainer.get(); assertThat(testInitContainer.getName()).isEqualTo("test"); assertThat(testInitContainer.getImage()).isEqualTo("busybox:latest"); assertThat(testInitContainer.getCommand()).containsExactly("sh", "-c", "echo hello"); undeploy(kubernetesAppDeployer, deploymentId); } @Test public void initContainerFromDeployerPropsWithVolumeMounts() { log.info("Testing {}...", "CreateInitContainerWithVolumeMounts"); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(); Map props = Stream.of(new String[][]{ { "spring.cloud.deployer.kubernetes.volumes", "[{name: 'test-volume', emptyDir: {}}]", }, { "spring.cloud.deployer.kubernetes.volumeMounts", "[{name: 'test-volume', mountPath: '/tmp'}]", }, { "spring.cloud.deployer.kubernetes.initContainer", "{containerName: 'test', imageName: 'busybox:latest', commands: ['sh', '-c', 'echo hello'], " + "volumeMounts: [{name: 'test-volume', mountPath: '/tmp', readOnly: true}]}", } }).collect(Collectors.toMap(data -> data[0], data -> data[1])); AppDefinition definition = new AppDefinition(randomName(), null); AppDeploymentRequest request = new AppDeploymentRequest(definition, testApplication(), props); String deploymentId = deploy(kubernetesAppDeployer, request); Deployment deployment = kubernetesClient.apps().deployments().withName(request.getDefinition().getName()).get(); List initContainers = deployment.getSpec().getTemplate().getSpec().getInitContainers(); Optional initContainer = initContainers.stream().filter(i -> i.getName().equals("test")).findFirst(); assertThat(initContainer.isPresent()).as("Init container not found").isTrue(); Container testInitContainer = initContainer.get(); assertThat(testInitContainer.getName()).as("Unexpected init container name").isEqualTo("test"); assertThat(testInitContainer.getImage()).as("Unexpected init container image").isEqualTo("busybox:latest"); List commands = testInitContainer.getCommand(); assertThat(commands != null && !commands.isEmpty()).as("Init container commands missing").isTrue(); assertThat(commands).hasSize(3); assertThat(commands).contains("sh", "-c", "echo hello"); List volumeMounts = testInitContainer.getVolumeMounts(); assertThat(volumeMounts != null && !volumeMounts.isEmpty()).as("Init container volumeMounts missing").isTrue(); assertThat(volumeMounts).hasSize(1); VolumeMount vm = volumeMounts.get(0); assertThat(vm.getName()).as("Unexpected init container volume mount name").isEqualTo("test-volume"); assertThat(vm.getMountPath()).as("Unexpected init container volume mount path").isEqualTo("/tmp"); assertThat(vm.getReadOnly()).as("Expected read only volume mount").isTrue(); undeploy(kubernetesAppDeployer, deploymentId); } private String deploy(KubernetesAppDeployer kubernetesAppDeployer, AppDeploymentRequest deployRequest) { log.info("Deploying {}...", deployRequest.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(deployRequest); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis((long) timeout.maxAttempts * (long) timeout.pause)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()) .isEqualTo(DeploymentState.deployed)); return deploymentId; } private void undeploy(KubernetesAppDeployer kubernetesAppDeployer, String deploymentId) { log.info("Undeploying {}...", deploymentId); Timeout timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis((long) timeout.maxAttempts * (long) timeout.pause)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()) .isEqualTo(DeploymentState.unknown)); } @Test public void testCreateAdditionalContainers() { log.info("Testing {}...", "CreateAdditionalContainers"); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(); Map props = Stream.of(new String[][]{ { "spring.cloud.deployer.kubernetes.volumes", "[{name: 'test-volume', emptyDir: {}}]", }, { "spring.cloud.deployer.kubernetes.volumeMounts", "[{name: 'test-volume', mountPath: '/tmp'}]", }, { "spring.cloud.deployer.kubernetes.additional-containers", "[{name: 'c1', image: 'busybox:latest', command: ['sh', '-c', 'echo hello1'], volumeMounts: [{name: 'test-volume', mountPath: '/tmp', readOnly: true}]}," + "{name: 'c2', image: 'busybox:1.26.1', command: ['sh', '-c', 'echo hello2']}]" }}).collect(Collectors.toMap(data -> data[0], data -> data[1])); AppDefinition definition = new AppDefinition(randomName(), null); AppDeploymentRequest request = new AppDeploymentRequest(definition, testApplication(), props); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); Deployment deployment = kubernetesClient.apps().deployments().withName(request.getDefinition().getName()).get(); List containers = deployment.getSpec().getTemplate().getSpec().getContainers(); assertThat(containers).hasSize(3); Optional additionalContainer1 = containers.stream().filter(i -> i.getName().equals("c1")).findFirst(); assertThat(additionalContainer1.isPresent()).isTrue(); Container testAdditionalContainer1 = additionalContainer1.get(); assertThat(testAdditionalContainer1.getName()).as("Unexpected additional container name").isEqualTo("c1"); assertThat(testAdditionalContainer1.getImage()).as("Unexpected additional container image").isEqualTo("busybox:latest"); List commands = testAdditionalContainer1.getCommand(); assertThat(commands).contains("sh", "-c", "echo hello1"); List volumeMounts = testAdditionalContainer1.getVolumeMounts(); assertThat(volumeMounts).hasSize(1); assertThat(volumeMounts.get(0).getName()).isEqualTo("test-volume"); assertThat(volumeMounts.get(0).getMountPath()).isEqualTo("/tmp"); assertThat(volumeMounts.get(0).getReadOnly()).isTrue(); Optional additionalContainer2 = containers.stream().filter(i -> i.getName().equals("c2")).findFirst(); assertThat(additionalContainer2.isPresent()).as("Additional container c2 not found").isTrue(); Container testAdditionalContainer2 = additionalContainer2.get(); assertThat(testAdditionalContainer2.getName()).as("Unexpected additional container name").isEqualTo("c2"); assertThat(testAdditionalContainer2.getImage()).as("Unexpected additional container image").isEqualTo("busybox:1.26.1"); List container2Commands = testAdditionalContainer2.getCommand(); assertThat(container2Commands).contains("sh", "-c", "echo hello2"); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); } @Test public void testCreateAdditionalContainersOverride() { log.info("Testing {}...", "CreateAdditionalContainersOverride"); KubernetesDeployerProperties.Container container1 = new KubernetesDeployerProperties.Container(); container1.setName("c1"); container1.setImage("busybox:1.31.0"); container1.setCommand(Arrays.asList("sh", "-c", "echo hello-from-original-properties")); KubernetesDeployerProperties.Container container2 = new KubernetesDeployerProperties.Container(); container2.setName("container2"); container2.setImage("busybox:1.31.0"); container2.setCommand(Arrays.asList("sh", "-c", "echo hello-from-original-properties")); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setAdditionalContainers(Arrays.asList(container1, container2)); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(kubernetesDeployerProperties); Map props = Stream.of(new String[][]{ { "spring.cloud.deployer.kubernetes.volumes", "[{name: 'test-volume', emptyDir: {}}]", }, { "spring.cloud.deployer.kubernetes.volumeMounts", "[{name: 'test-volume', mountPath: '/tmp'}]", }, { "spring.cloud.deployer.kubernetes.additional-containers", "[{name: 'c1', image: 'busybox:latest', command: ['sh', '-c', 'echo hello1'], volumeMounts: [{name: 'test-volume', mountPath: '/tmp', readOnly: true}]}," + "{name: 'c2', image: 'busybox:1.26.1', command: ['sh', '-c', 'echo hello2']}]" }}).collect(Collectors.toMap(data -> data[0], data -> data[1])); AppDefinition definition = new AppDefinition(randomName(), null); AppDeploymentRequest request = new AppDeploymentRequest(definition, testApplication(), props); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); Deployment deployment = kubernetesClient.apps().deployments().withName(request.getDefinition().getName()).get(); List containers = deployment.getSpec().getTemplate().getSpec().getContainers(); assertThat(containers).hasSize(4); // c1 from the deployment properties should have overridden the c1 from the original deployer properties Optional additionalContainer1 = containers.stream().filter(i -> i.getName().equals("c1")).findFirst(); assertThat(additionalContainer1.isPresent()).as("Additional container c1 not found").isTrue(); Container testAdditionalContainer1 = additionalContainer1.get(); assertThat(testAdditionalContainer1.getName()).as("Unexpected additional container name").isEqualTo("c1"); assertThat(testAdditionalContainer1.getImage()).as("Unexpected additional container image").isEqualTo("busybox:latest"); List commands = testAdditionalContainer1.getCommand(); assertThat(commands).contains("sh", "-c", "echo hello1"); List volumeMounts = testAdditionalContainer1.getVolumeMounts(); assertThat(volumeMounts).hasSize(1); assertThat(volumeMounts.get(0).getName()).isEqualTo("test-volume"); assertThat(volumeMounts.get(0).getMountPath()).isEqualTo("/tmp"); assertThat(volumeMounts.get(0).getReadOnly()).isTrue(); Optional additionalContainer2 = containers.stream().filter(i -> i.getName().equals("c2")).findFirst(); assertThat(additionalContainer2.isPresent()).as("Additional container c2 not found").isTrue(); Container testAdditionalContainer2 = additionalContainer2.get(); assertThat(testAdditionalContainer2.getName()).as("Unexpected additional container name").isEqualTo("c2"); assertThat(testAdditionalContainer2.getImage()).as("Unexpected additional container image").isEqualTo("busybox:1.26.1"); List container2Commands = testAdditionalContainer2.getCommand(); assertThat(container2Commands).contains("sh", "-c", "echo hello2"); // Verifying the additional container passed from the root deployer properties Optional additionalContainer3 = containers.stream().filter(i -> i.getName().equals("container2")).findFirst(); assertThat(additionalContainer3.isPresent()).as("Additional container c2 not found").isTrue(); Container testAdditionalContainer3 = additionalContainer3.get(); assertThat(testAdditionalContainer3.getName()).as("Unexpected additional container name").isEqualTo("container2"); assertThat(testAdditionalContainer3.getImage()).as("Unexpected additional container image").isEqualTo("busybox:1.31.0"); List container3Commands = testAdditionalContainer3.getCommand(); assertThat(container3Commands).contains("sh", "-c", "echo hello-from-original-properties"); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); } @Nested class SecurityContextITs { @Test void podWithInitContainerAndAdditionalContainers() { Map deploymentProps = new HashMap<>(); deploymentProps.put("spring.cloud.deployer.kubernetes.initContainer", "{ containerName: 'init-container-5150', imageName: 'busybox:latest', commands: ['sh', '-c', 'echo hello']}"); deploymentProps.put("spring.cloud.deployer.kubernetes.additional-containers", "[" + "{ name: 'extra-container-5150', image: 'busybox:latest', command: ['sh', '-c'], args: [\"while true; do echo ‘hello 5150’ & sleep 2; done\"]}," + "{ name: 'extra-container-6160', image: 'busybox:latest', command: ['sh', '-c'], args: [\"while true; do echo ‘hello 6160’ & sleep 2; done\"]}" + "]"); deploymentProps.put("spring.cloud.deployer.kubernetes.podSecurityContext", "{ fsGroup: 65534" + ", fsGroupChangePolicy: Always" + ", runAsUser: 65534" + ", runAsGroup: 65534" + ", seLinuxOptions: { level: \"s0:c123,c456\" }" + "}"); deploymentProps.put("spring.cloud.deployer.kubernetes.containerSecurityContext", "{ allowPrivilegeEscalation: true" + ", runAsUser: 65534" + ", runAsGroup: 65534" + ", seLinuxOptions: { level: \"s0:c123,c777\" }" + "}"); AppDefinition definition = new AppDefinition(randomName(), null); AppDeploymentRequest request = new AppDeploymentRequest(definition, testApplication(), deploymentProps); KubernetesDeployerProperties deployProperties = new KubernetesDeployerProperties(); deployProperties.setCreateLoadBalancer(true); deployProperties.setMinutesToWaitForLoadBalancer(1); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(deployProperties); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); PodSecurityContext expectedPodSecurityContext = new PodSecurityContextBuilder() .withFsGroup(65534L) .withFsGroupChangePolicy("Always") .withRunAsUser(65534L) .withRunAsGroup(65534L) .withSeLinuxOptions(new SELinuxOptions("s0:c123,c456", null, null, null)) .build(); SecurityContext expectedContainerSecurityContext = new SecurityContextBuilder() .withAllowPrivilegeEscalation(true) .withRunAsUser(65534L) .withRunAsGroup(65534L) .withSeLinuxOptions(new SELinuxOptions("s0:c123,c777", null, null, null)) .build(); Deployment deployment = kubernetesClient.apps().deployments().withName(request.getDefinition().getName()).get(); assertThat(deployment.getSpec().getTemplate().getSpec().getSecurityContext()) .isEqualTo(expectedPodSecurityContext); assertThatContainerExistsWithSecurityContext(deployment.getSpec().getTemplate().getSpec().getInitContainers(), "init-container-5150", expectedContainerSecurityContext); assertThatContainerExistsWithSecurityContext(deployment.getSpec().getTemplate().getSpec().getContainers(), "extra-container-5150", expectedContainerSecurityContext); assertThatContainerExistsWithSecurityContext(deployment.getSpec().getTemplate().getSpec().getContainers(), "extra-container-6160", expectedContainerSecurityContext); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); } @Test void statefulSetWithInitContainerAndAdditionalContainers() throws IOException { Map deploymentProps = new HashMap<>(); deploymentProps.put(AppDeployer.COUNT_PROPERTY_KEY, "2"); deploymentProps.put(AppDeployer.INDEXED_PROPERTY_KEY, "true"); deploymentProps.put("spring.cloud.deployer.kubernetes.podSecurityContext", "{ fsGroup: 65534" + ", fsGroupChangePolicy: Always" + ", runAsUser: 65534" + ", runAsGroup: 65534" + ", seLinuxOptions: { level: \"s0:c123,c456\" }" + "}"); deploymentProps.put("spring.cloud.deployer.kubernetes.containerSecurityContext", "{ allowPrivilegeEscalation: true" + ", runAsUser: 65534" + ", runAsGroup: 65534" + ", seLinuxOptions: { level: \"s0:c123,c777\" }" + "}"); AppDefinition definition = new AppDefinition(randomName(), null); AppDeploymentRequest request = new AppDeploymentRequest(definition, testApplication(), deploymentProps); String imageName = testApplication().getURI().getSchemeSpecificPart(); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setStatefulSetInitContainerImageName(imageName); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(kubernetesDeployerProperties); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); PodSecurityContext expectedPodSecurityContext = new PodSecurityContextBuilder() .withFsGroup(65534L) .withFsGroupChangePolicy("Always") .withRunAsUser(65534L) .withRunAsGroup(65534L) .withSeLinuxOptions(new SELinuxOptions("s0:c123,c456", null, null, null)) .build(); SecurityContext expectedContainerSecurityContext = new SecurityContextBuilder() .withAllowPrivilegeEscalation(true) .withRunAsUser(65534L) .withRunAsGroup(65534L) .withSeLinuxOptions(new SELinuxOptions("s0:c123,c777", null, null, null)) .build(); Map selector = Collections.singletonMap(AbstractKubernetesDeployer.SPRING_APP_KEY, deploymentId); List statefulSets = kubernetesClient.apps().statefulSets().withLabels(selector).list().getItems(); assertThat(statefulSets).hasSize(1) .element(0, InstanceOfAssertFactories.type(StatefulSet.class)) .extracting("spec.template.spec", InstanceOfAssertFactories.type(PodSpec.class)) .satisfies((podSpec) -> { assertThat(podSpec.getSecurityContext()).isEqualTo(expectedPodSecurityContext); assertThat(podSpec.getInitContainers()) .hasSize(1) .element(0, InstanceOfAssertFactories.type(Container.class)) .extracting(Container::getSecurityContext).isEqualTo(expectedContainerSecurityContext); }); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); } } private void assertThatContainerExistsWithSecurityContext(List containers, String expectedName, SecurityContext expectedSecurityContext) { assertThat(containers .stream().filter(c -> c.getName().equals(expectedName)).findFirst()) .hasValueSatisfying((initContainer) -> assertThat(initContainer.getSecurityContext()).isEqualTo(expectedSecurityContext)); } @Test public void testUnknownStatusOnPendingResources() throws InterruptedException { log.info("Testing {}...", "UnknownStatusOnPendingResources"); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); Map props = new HashMap<>(); // requests.cpu mirrors limits.cpu when only limits is set avoiding need to set both here props.put("spring.cloud.deployer.kubernetes.limits.cpu", "5000"); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, props); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); while (kubernetesClient.pods().withLabel("spring-deployment-id", deploymentId).list().getItems().isEmpty()) { log.info("Waiting for deployed pod"); Thread.sleep(500); } Pod pod = kubernetesClient.pods().withLabel("spring-deployment-id", deploymentId).list().getItems().get(0); while (pod.getStatus().getConditions().isEmpty()) { log.info("Waiting for pod conditions to be set"); Thread.sleep(500); } while (!"Unschedulable".equals(pod.getStatus().getConditions().get(0).getReason())) { log.info("Waiting for deployed pod to become Unschedulable"); } Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); assertThatThrownBy(() -> kubernetesAppDeployer.undeploy(deploymentId)).isInstanceOf(IllegalStateException.class) .hasMessage("App '%s' is not deployed", deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); } @Test public void testSecretRef() throws InterruptedException { log.info("Testing {}...", "SecretRef"); Secret secret = randomSecret(); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setSecretRefs(Collections.singletonList(secret.getMetadata().getName())); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(kubernetesDeployerProperties); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); Container container = kubernetesClient.apps().deployments().withName(deploymentId).get().getSpec() .getTemplate().getSpec().getContainers().get(0); List envFromSources = container.getEnvFrom(); assertThat(envFromSources).hasSize(1); EnvFromSource envFromSource = envFromSources.get(0); assertThat(envFromSource.getSecretRef().getName()).isEqualTo(secret.getMetadata().getName()); String podEnvironment = getPodEnvironment(deploymentId); assertThat(secret.getData()).hasSize(2); for (Map.Entry secretData : secret.getData().entrySet()) { String decodedValue = new String(Base64.getDecoder().decode(secretData.getValue())); assertThat(podEnvironment).contains(secretData.getKey() + "=" + decodedValue); } log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); kubernetesClient.secrets().inNamespace("default").resource(secret).delete(); } @Test public void testSecretRefFromDeployerProperty() throws InterruptedException { log.info("Testing {}...", "SecretRefFromDeployerProperty"); Secret secret = randomSecret(); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.secretRefs", secret.getMetadata().getName()); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, props); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); Container container = kubernetesClient.apps().deployments().withName(deploymentId).get().getSpec() .getTemplate().getSpec().getContainers().get(0); List envFromSources = container.getEnvFrom(); assertThat(envFromSources).hasSize(1); EnvFromSource envFromSource = envFromSources.get(0); assertThat(secret.getMetadata().getName()).isEqualTo(envFromSource.getSecretRef().getName()); String podEnvironment = getPodEnvironment(deploymentId); assertThat(secret.getData()).hasSize(2); for (Map.Entry secretData : secret.getData().entrySet()) { String decodedValue = new String(Base64.getDecoder().decode(secretData.getValue())); assertThat(podEnvironment).contains(secretData.getKey() + "=" + decodedValue); } log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); kubernetesClient.secrets().inNamespace("default").resource(secret).delete(); } @Test public void testSecretRefFromDeployerPropertyOverride() throws IOException, InterruptedException { log.info("Testing {}...", "SecretRefFromDeployerPropertyOverride"); Secret propertySecret = randomSecret(); Secret deployerPropertySecret = randomSecret(); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setSecretRefs(Collections.singletonList(propertySecret.getMetadata().getName())); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(kubernetesDeployerProperties); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.secretRefs", deployerPropertySecret.getMetadata().getName()); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, props); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); Container container = kubernetesClient.apps().deployments().withName(deploymentId).get().getSpec() .getTemplate().getSpec().getContainers().get(0); List envFromSources = container.getEnvFrom(); assertThat(envFromSources).hasSize(1); EnvFromSource envFromSource = envFromSources.get(0); assertThat(envFromSource.getSecretRef().getName()).isEqualTo(deployerPropertySecret.getMetadata().getName()); String podEnvironment = getPodEnvironment(deploymentId); for (Map.Entry deployerPropertySecretData : deployerPropertySecret.getData().entrySet()) { String decodedValue = new String(Base64.getDecoder().decode(deployerPropertySecretData.getValue())); assertThat(podEnvironment).contains(deployerPropertySecretData.getKey() + "=" + decodedValue); } for (Map.Entry propertySecretData : propertySecret.getData().entrySet()) { String decodedValue = new String(Base64.getDecoder().decode(propertySecretData.getValue())); assertThat(podEnvironment).doesNotContain(propertySecretData.getKey() + "=" + decodedValue); } log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); kubernetesClient.secrets().inNamespace("default").resource(propertySecret).delete(); kubernetesClient.secrets().inNamespace("default").resource(deployerPropertySecret).delete(); } @Test public void testSecretRefFromPropertyMultiple() throws InterruptedException { log.info("Testing {}...", "SecretRefFromPropertyMultiple"); Secret secret1 = randomSecret(); Secret secret2 = randomSecret(); List secrets = new ArrayList<>(); secrets.add(secret1.getMetadata().getName()); secrets.add(secret2.getMetadata().getName()); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setSecretRefs(secrets); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(kubernetesDeployerProperties); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, null); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); Container container = kubernetesClient.apps().deployments().withName(deploymentId).get().getSpec() .getTemplate().getSpec().getContainers().get(0); List envFromSources = container.getEnvFrom(); assertThat(envFromSources).hasSize(2); String podEnvironment = getPodEnvironment(deploymentId); EnvFromSource envFromSource1 = envFromSources.get(0); assertThat(envFromSource1.getSecretRef().getName()).isEqualTo(secret1.getMetadata().getName()); EnvFromSource envFromSource2 = envFromSources.get(1); assertThat(envFromSource2.getSecretRef().getName()).isEqualTo(secret2.getMetadata().getName()); Map mergedSecretData = new HashMap<>(); mergedSecretData.putAll(secret1.getData()); mergedSecretData.putAll(secret2.getData()); assertThat(mergedSecretData).hasSize(4); for (Map.Entry secretData : secret1.getData().entrySet()) { String decodedValue = new String(Base64.getDecoder().decode(secretData.getValue())); assertThat(podEnvironment).contains(secretData.getKey() + "=" + decodedValue); } log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); kubernetesClient.secrets().inNamespace("default").resource(secret1).delete(); kubernetesClient.secrets().inNamespace("default").resource(secret2).delete(); } @Test public void testSecretRefFromDeploymentPropertyMultiple() throws InterruptedException { log.info("Testing {}...", "SecretRefFromDeploymentPropertyMultiple"); Secret secret1 = randomSecret(); Secret secret2 = randomSecret(); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.secretRefs", "[" + secret1.getMetadata().getName() + "," + secret2.getMetadata().getName() + "]"); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, props); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); Container container = kubernetesClient.apps().deployments().withName(deploymentId).get().getSpec() .getTemplate().getSpec().getContainers().get(0); List envFromSources = container.getEnvFrom(); assertThat(envFromSources).hasSize(2); String podEnvironment = getPodEnvironment(deploymentId); EnvFromSource envFromSource1 = envFromSources.get(0); assertThat(secret1.getMetadata().getName()).isEqualTo(envFromSource1.getSecretRef().getName()); EnvFromSource envFromSource2 = envFromSources.get(1); assertThat(secret2.getMetadata().getName()).isEqualTo(envFromSource2.getSecretRef().getName()); Map mergedSecretData = new HashMap<>(); mergedSecretData.putAll(secret1.getData()); mergedSecretData.putAll(secret2.getData()); assertThat(mergedSecretData).hasSize(4); for (Map.Entry secretData : secret1.getData().entrySet()) { String decodedValue = new String(Base64.getDecoder().decode(secretData.getValue())); assertThat(podEnvironment).contains(secretData.getKey() + "=" + decodedValue); } log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); kubernetesClient.secrets().inNamespace("default").resource(secret1).delete(); kubernetesClient.secrets().inNamespace("default").resource(secret2).delete(); } @Test public void testConfigMapRef() throws InterruptedException { log.info("Testing {}...", "ConfigMapRef"); ConfigMap configMap = randomConfigMap(); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setConfigMapRefs(Collections.singletonList(configMap.getMetadata().getName())); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(kubernetesDeployerProperties); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); Container container = kubernetesClient.apps().deployments().withName(deploymentId).get().getSpec() .getTemplate().getSpec().getContainers().get(0); List envFromSources = container.getEnvFrom(); assertThat(envFromSources).hasSize(1); EnvFromSource envFromSource = envFromSources.get(0); assertThat(envFromSource.getConfigMapRef().getName()).isEqualTo(configMap.getMetadata().getName()); String podEnvironment = getPodEnvironment(deploymentId); assertThat(configMap.getData()).hasSize(2); for (Map.Entry configMapData : configMap.getData().entrySet()) { assertThat(podEnvironment).contains(configMapData.getKey() + "=" + configMapData.getValue()); } log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); kubernetesClient.configMaps().inNamespace("default").resource(configMap).delete(); } @Test public void testConfigMapRefFromDeployerProperty() throws InterruptedException { log.info("Testing {}...", "ConfigMapRefFromDeployerProperty"); ConfigMap configMap = randomConfigMap(); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.config-map-refs", configMap.getMetadata().getName()); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, props); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); Container container = kubernetesClient.apps().deployments().withName(deploymentId).get().getSpec() .getTemplate().getSpec().getContainers().get(0); List envFromSources = container.getEnvFrom(); assertThat(envFromSources).hasSize(1); EnvFromSource envFromSource = envFromSources.get(0); assertThat(envFromSource.getConfigMapRef().getName()).isEqualTo(configMap.getMetadata().getName()); String podEnvironment = getPodEnvironment(deploymentId); assertThat(configMap.getData()).hasSize(2); for (Map.Entry configMapData : configMap.getData().entrySet()) { assertThat(podEnvironment).contains(configMapData.getKey() + "=" + configMapData.getValue()); } log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); kubernetesClient.configMaps().inNamespace("default").resource(configMap).delete(); } @Test public void testConfigMapRefFromDeployerPropertyOverride() throws IOException, InterruptedException { log.info("Testing {}...", "ConfigMapRefFromDeployerPropertyOverride"); ConfigMap propertyConfigMap = randomConfigMap(); ConfigMap deployerPropertyConfigMap = randomConfigMap(); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setConfigMapRefs(Collections.singletonList(propertyConfigMap.getMetadata().getName())); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(kubernetesDeployerProperties); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.configMapRefs", deployerPropertyConfigMap.getMetadata().getName()); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, props); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); Container container = kubernetesClient.apps().deployments().withName(deploymentId).get().getSpec() .getTemplate().getSpec().getContainers().get(0); List envFromSources = container.getEnvFrom(); assertThat(envFromSources).hasSize(1); EnvFromSource envFromSource = envFromSources.get(0); assertThat(deployerPropertyConfigMap.getMetadata().getName()).isEqualTo(envFromSource.getConfigMapRef().getName()); String podEnvironment = getPodEnvironment(deploymentId); for (Map.Entry deployerPropertyConfigMapData : deployerPropertyConfigMap.getData().entrySet()) { assertThat(podEnvironment) .contains(deployerPropertyConfigMapData.getKey() + "=" + deployerPropertyConfigMapData.getValue()); } for (Map.Entry propertyConfigMapData : propertyConfigMap.getData().entrySet()) { assertThat(podEnvironment).doesNotContain(propertyConfigMapData.getKey() + "=" + propertyConfigMapData.getValue()); } log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); kubernetesClient.configMaps().inNamespace("default").resource(propertyConfigMap).delete(); kubernetesClient.configMaps().inNamespace("default").resource(deployerPropertyConfigMap).delete(); } @Test public void testConfigMapRefFromPropertyMultiple() throws InterruptedException { log.info("Testing {}...", "ConfigMapRefFromPropertyMultiple"); ConfigMap configMap1 = randomConfigMap(); ConfigMap configMap2 = randomConfigMap(); List configMaps = new ArrayList<>(); configMaps.add(configMap1.getMetadata().getName()); configMaps.add(configMap2.getMetadata().getName()); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setConfigMapRefs(configMaps); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(kubernetesDeployerProperties); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, null); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); Container container = kubernetesClient.apps().deployments().withName(deploymentId).get().getSpec() .getTemplate().getSpec().getContainers().get(0); List envFromSources = container.getEnvFrom(); assertThat(envFromSources).hasSize(2); String podEnvironment = getPodEnvironment(deploymentId); EnvFromSource envFromSource1 = envFromSources.get(0); assertThat(configMap1.getMetadata().getName()).isEqualTo(envFromSource1.getConfigMapRef().getName()); EnvFromSource envFromSource2 = envFromSources.get(1); assertThat(configMap2.getMetadata().getName()).isEqualTo(envFromSource2.getConfigMapRef().getName()); Map mergedConfigMapData = new HashMap<>(); mergedConfigMapData.putAll(configMap1.getData()); mergedConfigMapData.putAll(configMap2.getData()); assertThat(mergedConfigMapData).hasSize(4); for (Map.Entry configMapData : configMap1.getData().entrySet()) { assertThat(podEnvironment).contains(configMapData.getKey() + "=" + configMapData.getValue()); } log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); kubernetesClient.configMaps().inNamespace("default").resource(configMap1).delete(); kubernetesClient.configMaps().inNamespace("default").resource(configMap2).delete(); } @Test public void testConfigMapRefFromDeploymentPropertyMultiple() throws InterruptedException { log.info("Testing {}...", "ConfigMapRefFromDeploymentPropertyMultiple"); ConfigMap configMap1 = randomConfigMap(); ConfigMap configMap2 = randomConfigMap(); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.configMapRefs", "[" + configMap1.getMetadata().getName() + "," + configMap2.getMetadata().getName() + "]"); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, props); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); Container container = kubernetesClient.apps().deployments().withName(deploymentId).get().getSpec() .getTemplate().getSpec().getContainers().get(0); List envFromSources = container.getEnvFrom(); assertThat(envFromSources).isNotNull(); assertThat(envFromSources).hasSize(2); String podEnvironment = getPodEnvironment(deploymentId); EnvFromSource envFromSource1 = envFromSources.get(0); assertThat(envFromSource1.getConfigMapRef().getName()).isEqualTo(configMap1.getMetadata().getName()); EnvFromSource envFromSource2 = envFromSources.get(1); assertThat(envFromSource2.getConfigMapRef().getName()).isEqualTo(configMap2.getMetadata().getName()); Map mergedConfigMapData = new HashMap<>(); mergedConfigMapData.putAll(configMap1.getData()); mergedConfigMapData.putAll(configMap2.getData()); assertThat(mergedConfigMapData).hasSize(4); for (Map.Entry configMapData : configMap1.getData().entrySet()) { assertThat(podEnvironment).contains(configMapData.getKey() + "=" + configMapData.getValue()); } log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); kubernetesClient.configMaps().inNamespace("default").resource(configMap1).delete(); kubernetesClient.configMaps().inNamespace("default").resource(configMap2).delete(); } @Override protected String randomName() { // Kubernetes service names must start with a letter and can only be 24 characters long return "app-" + UUID.randomUUID().toString().substring(0, 18); } @Override protected Timeout deploymentTimeout() { return new Timeout(300, 2000); } @Override protected Resource testApplication() { return new DockerResource("springcloud/spring-cloud-deployer-spi-test-app:latest"); } private String getPodEnvironment(String deploymentId) throws InterruptedException { String podName = kubernetesClient.pods().withLabel("spring-deployment-id", deploymentId).list().getItems() .get(0).getMetadata().getName(); final CountDownLatch countDownLatch = new CountDownLatch(1); ByteArrayOutputStream execOutputStream = new ByteArrayOutputStream(); ExecWatch watch = kubernetesClient.pods().withName(podName).inContainer(deploymentId) .writingOutput(execOutputStream) .usingListener(new ExecListener() { @Override public void onFailure(Throwable throwable, Response response) { countDownLatch.countDown(); } @Override public void onClose(int code, String reason) { countDownLatch.countDown(); } }).exec("printenv"); countDownLatch.await(); watch.close(); return execOutputStream.toString(); } // Creates a Secret with a name will be generated prefixed by "secret-" followed by random numbers. // // Two data keys are present, both prefixed by "d-" followed by random numbers. This allows for cases where // multiple Secrets may be read into environment variables avoiding variable name clashes. // // Data values are Base64 encoded strings of value1 and value2 private Secret randomSecret() { Map secretData = new HashMap<>(); secretData.put("d-" + UUID.randomUUID().toString().substring(0, 5), "dmFsdWUx"); secretData.put("d-" + UUID.randomUUID().toString().substring(0, 5), "dmFsdWUy"); Secret secret = new Secret(); secret.setData(secretData); ObjectMeta objectMeta = new ObjectMeta(); objectMeta.setName("secret-" + UUID.randomUUID().toString().substring(0, 5)); secret.setMetadata(objectMeta); return kubernetesClient.secrets().inNamespace("default").resource(secret).create(); } // Creates a ConfigMap with a name will be generated prefixed by "cm-" followed by random numbers. // // Two data keys are present, both prefixed by "d-" followed by random numbers. This allows for cases where // multiple ConfigMaps may be read into environment variables avoiding variable name clashes. // // Data values are strings of value1 and value private ConfigMap randomConfigMap() { Map configMapData = new HashMap<>(); configMapData.put("d-" + UUID.randomUUID().toString().substring(0, 5), "value1"); configMapData.put("d-" + UUID.randomUUID().toString().substring(0, 5), "value2"); ConfigMap configMap = new ConfigMap(); configMap.setData(configMapData); ObjectMeta objectMeta = new ObjectMeta(); objectMeta.setName("cm-" + UUID.randomUUID().toString().substring(0, 5)); configMap.setMetadata(objectMeta); return kubernetesClient.configMaps().inNamespace("default").resource(configMap).create(); } private KubernetesAppDeployer kubernetesAppDeployer() { return kubernetesAppDeployer(new KubernetesDeployerProperties()); } private KubernetesAppDeployer kubernetesAppDeployer(KubernetesDeployerProperties kubernetesDeployerProperties) { return new KubernetesAppDeployer(kubernetesDeployerProperties, this.kubernetesClient, new DefaultContainerFactory(kubernetesDeployerProperties)); } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/test/java/org/springframework/cloud/deployer/spi/kubernetes/KubernetesAppDeployerTests.java ================================================ /* * Copyright 2015-2023 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.cloud.deployer.spi.kubernetes; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.stream.Collectors; import io.fabric8.kubernetes.api.model.AffinityBuilder; import io.fabric8.kubernetes.api.model.Capabilities; import io.fabric8.kubernetes.api.model.ConfigMapEnvSourceBuilder; import io.fabric8.kubernetes.api.model.ConfigMapKeySelector; import io.fabric8.kubernetes.api.model.ConfigMapVolumeSource; import io.fabric8.kubernetes.api.model.ConfigMapVolumeSourceBuilder; import io.fabric8.kubernetes.api.model.Container; import io.fabric8.kubernetes.api.model.EnvFromSource; import io.fabric8.kubernetes.api.model.EnvVar; import io.fabric8.kubernetes.api.model.EnvVarSourceBuilder; import io.fabric8.kubernetes.api.model.HostPathVolumeSource; import io.fabric8.kubernetes.api.model.HostPathVolumeSourceBuilder; import io.fabric8.kubernetes.api.model.KeyToPath; import io.fabric8.kubernetes.api.model.LabelSelector; import io.fabric8.kubernetes.api.model.LabelSelectorRequirementBuilder; import io.fabric8.kubernetes.api.model.NodeAffinity; import io.fabric8.kubernetes.api.model.NodeSelectorRequirementBuilder; import io.fabric8.kubernetes.api.model.NodeSelectorTerm; import io.fabric8.kubernetes.api.model.ObjectFieldSelectorBuilder; import io.fabric8.kubernetes.api.model.PodAffinity; import io.fabric8.kubernetes.api.model.PodAffinityTerm; import io.fabric8.kubernetes.api.model.PodAntiAffinity; import io.fabric8.kubernetes.api.model.PodSecurityContext; import io.fabric8.kubernetes.api.model.PodSecurityContextBuilder; import io.fabric8.kubernetes.api.model.PodSpec; import io.fabric8.kubernetes.api.model.PreferredSchedulingTerm; import io.fabric8.kubernetes.api.model.SELinuxOptions; import io.fabric8.kubernetes.api.model.SeccompProfile; import io.fabric8.kubernetes.api.model.SecretEnvSourceBuilder; import io.fabric8.kubernetes.api.model.SecretKeySelector; import io.fabric8.kubernetes.api.model.SecurityContext; import io.fabric8.kubernetes.api.model.SecurityContextBuilder; import io.fabric8.kubernetes.api.model.Sysctl; import io.fabric8.kubernetes.api.model.Toleration; import io.fabric8.kubernetes.api.model.Volume; import io.fabric8.kubernetes.api.model.VolumeBuilder; import io.fabric8.kubernetes.api.model.WeightedPodAffinityTerm; import io.fabric8.kubernetes.api.model.WindowsSecurityContextOptions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.config.YamlPropertiesFactoryBean; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; import org.springframework.cloud.deployer.resource.docker.DockerResource; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.entry; /** * Unit tests for {@link KubernetesAppDeployer} * * @author Donovan Muller * @author David Turanski * @author Ilayaperumal Gopinathan * @author Chris Schaefer * @author Enrique Medina Montenegro * @author Chris Bono * @author Corneil du Plessis */ @DisplayName("KubernetesAppDeployer") public class KubernetesAppDeployerTests { private KubernetesAppDeployer deployer; private DeploymentPropertiesResolver deploymentPropertiesResolver = new DeploymentPropertiesResolver( KubernetesDeployerProperties.KUBERNETES_DEPLOYER_PROPERTIES_PREFIX, new KubernetesDeployerProperties()); @Test public void deployWithVolumesOnly() throws Exception { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), new HashMap<>()); deployer = k8sAppDeployer(bindDeployerProperties()); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getVolumes()).isEmpty(); } @Test public void deployWithVolumesAndVolumeMounts() throws Exception { AppDefinition definition = new AppDefinition("app-test", null); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.volumeMounts", "[" + "{name: 'testpvc', mountPath: '/test/pvc'}, " + "{name: 'testnfs', mountPath: '/test/nfs', readOnly: 'true'}" + "]"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getVolumes()).containsOnly( // volume 'testhostpath' defined in dataflow-server.yml should not be added // as there is no corresponding volume mount new VolumeBuilder().withName("testpvc").withNewPersistentVolumeClaim("testClaim", true).build(), new VolumeBuilder().withName("testnfs").withNewNfs("/test/nfs", null, "10.0.0.1:111").build()); props.clear(); props.put("spring.cloud.deployer.kubernetes.volumes", "[" + "{name: testhostpath, hostPath: { path: '/test/override/hostPath' }}," + "{name: 'testnfs', nfs: { server: '192.168.1.1:111', path: '/test/override/nfs' }} " + "]"); props.put("spring.cloud.deployer.kubernetes.volumeMounts", "[" + "{name: 'testhostpath', mountPath: '/test/hostPath'}, " + "{name: 'testpvc', mountPath: '/test/pvc'}, " + "{name: 'testnfs', mountPath: '/test/nfs', readOnly: 'true'}" + "]"); appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(); podSpec = deployer.createPodSpec(appDeploymentRequest); HostPathVolumeSource hostPathVolumeSource = new HostPathVolumeSourceBuilder() .withPath("/test/override/hostPath").build(); assertThat(podSpec.getVolumes()).containsOnly( new VolumeBuilder().withName("testhostpath").withHostPath(hostPathVolumeSource).build(), new VolumeBuilder().withName("testpvc").withNewPersistentVolumeClaim("testClaim", true).build(), new VolumeBuilder().withName("testnfs").withNewNfs("/test/override/nfs", null, "192.168.1.1:111").build()); } @Test public void deployWithVolumesAndVolumeMountsOnAdditionalContainer() throws Exception { AppDefinition definition = new AppDefinition("app-test", null); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.volumes", "[{name: 'config', configMap: {name: promtail-config, items: [{key: promtail.yaml, path: promtail.yaml}]}},{name: 'config2', configMap: {name: promtail-config, items: [{key: promtail.yaml, path: promtail.yaml}]}}]"); props.put("spring.cloud.deployer.kubernetes.additional-containers", "[{name: 'promtail',image: image-path-of-promtail, ports:[{protocol: TCP,containerPort: 8080}],args: [\"-config.file=/home/conf/promtail.yaml\"],volumeMounts: [{name: 'config', mountPath: '/home/conf'}]},{name: 'promtail2',image: image-path-of-promtail, ports:[{protocol: TCP,containerPort: 8080}],args: [\"-config.file=/home/conf/promtail.yaml\"],volumeMounts: [{name: 'config2', mountPath: '/home/conf'}]}]"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); ConfigMapVolumeSource configMapVolumeSource = new ConfigMapVolumeSourceBuilder() .withName("promtail-config") .withItems(new KeyToPath("promtail.yaml", null, "promtail.yaml")) .build(); Volume volume = new VolumeBuilder().withName("config").withNewConfigMapLike(configMapVolumeSource).endConfigMap().build(); Volume volume2 = new VolumeBuilder().withName("config2").withNewConfigMapLike(configMapVolumeSource).endConfigMap().build(); assertThat(podSpec.getVolumes()).containsExactly(volume, volume2); } @Test public void deployWithVolumesAndVolumeMountsOnAdditionalContainerAbsentVolumeMount() throws Exception { AppDefinition definition = new AppDefinition("app-test", null); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.volumes", "[{name: 'config', configMap: {name: promtail-config, items: [{key: promtail.yaml, path: promtail.yaml}]}},{name: 'config2', configMap: {name: promtail-config, items: [{key: promtail.yaml, path: promtail.yaml}]}}]"); // both containers reference config. props.put("spring.cloud.deployer.kubernetes.additional-containers", "[{name: 'promtail',image: image-path-of-promtail, ports:[{protocol: TCP,containerPort: 8080}],args: [\"-config.file=/home/conf/promtail.yaml\"],volumeMounts: [{name: 'config', mountPath: '/home/conf'}]},{name: 'promtail2',image: image-path-of-promtail, ports:[{protocol: TCP,containerPort: 8080}],args: [\"-config.file=/home/conf/promtail.yaml\"],volumeMounts: [{name: 'config', mountPath: '/home/conf'}]}]"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); ConfigMapVolumeSource configMapVolumeSource = new ConfigMapVolumeSourceBuilder() .withName("promtail-config") .withItems(new KeyToPath("promtail.yaml", null, "promtail.yaml")) .build(); Set volumeNames = podSpec.getVolumes().stream().map(v -> v.getName()).collect(Collectors.toSet()); assertThat(volumeNames).doesNotContain("config2"); Volume volume = new VolumeBuilder().withName("config").withNewConfigMapLike(configMapVolumeSource).endConfigMap().build(); assertThat(podSpec.getVolumes()).containsExactly(volume); } @Test public void deployWithNodeSelectorGlobalProperty() throws Exception { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), null); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setNodeSelector("disktype:ssd, os:qnx"); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getNodeSelector()).containsOnly(entry("disktype", "ssd"), entry("os", "qnx")); } @Test public void deployWithNodeSelectorDeploymentProperty() throws Exception { AppDefinition definition = new AppDefinition("app-test", null); Map props = new HashMap<>(); props.put(KubernetesDeployerProperties.KUBERNETES_DEPLOYMENT_NODE_SELECTOR, "disktype:ssd, os: linux"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getNodeSelector()).containsOnly(entry("disktype", "ssd"), entry("os", "linux")); } @Test public void deployWithNodeSelectorTrainTruckCaseProperty() throws Exception { AppDefinition definition = new AppDefinition("app-test", null); Map props = new HashMap<>(); props.put(KubernetesDeployerProperties.KUBERNETES_DEPLOYER_PROPERTIES_PREFIX + ".node-selector", "os: linux"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getNodeSelector()).containsOnly(entry("os", "linux")); } @Test public void deployWithNodeSelectorDeploymentPropertyGlobalOverride() throws Exception { AppDefinition definition = new AppDefinition("app-test", null); Map props = new HashMap<>(); props.put(KubernetesDeployerProperties.KUBERNETES_DEPLOYMENT_NODE_SELECTOR, "disktype:ssd, os: openbsd"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setNodeSelector("disktype:ssd, os:qnx"); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getNodeSelector()).containsOnly(entry("disktype", "ssd"), entry("os", "openbsd")); } @Test public void deployWithEnvironmentWithCommaDelimitedValue() throws Exception { AppDefinition definition = new AppDefinition("app-test", null); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.environmentVariables", "JAVA_TOOL_OPTIONS='thing1,thing2',foo='bar,baz',car=caz,boo='zoo,gnu',doo=dar,OPTS='thing1'"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getContainers().get(0).getEnv()) .contains( new EnvVar("foo", "bar,baz", null), new EnvVar("car", "caz", null), new EnvVar("boo", "zoo,gnu", null), new EnvVar("doo", "dar", null), new EnvVar("JAVA_TOOL_OPTIONS", "thing1,thing2", null), new EnvVar("OPTS", "thing1", null)); } @Test public void deployWithEnvironmentWithSingleCommaDelimitedValue() throws Exception { AppDefinition definition = new AppDefinition("app-test", null); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.environmentVariables", "JAVA_TOOL_OPTIONS='thing1,thing2'"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getContainers().get(0).getEnv()) .contains(new EnvVar("JAVA_TOOL_OPTIONS", "thing1,thing2", null)); } @Test public void deployWithEnvironmentWithMultipleCommaDelimitedValue() throws Exception { AppDefinition definition = new AppDefinition("app-test", null); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.environmentVariables", "JAVA_TOOL_OPTIONS='thing1,thing2',OPTS='thing3, thing4'"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getContainers().get(0).getEnv()) .contains( new EnvVar("JAVA_TOOL_OPTIONS", "thing1,thing2", null), new EnvVar("OPTS", "thing3, thing4", null)); } @Test public void deployWithImagePullSecretDeploymentProperty() { AppDefinition definition = new AppDefinition("app-test", null); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.imagePullSecret", "regcred"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(new KubernetesDeployerProperties()); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getImagePullSecrets().size()).isEqualTo(1); assertThat(podSpec.getImagePullSecrets().get(0).getName()).isEqualTo("regcred"); } @Test public void deployWithImagePullSecretDeployerProperty() { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), null); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setImagePullSecret("regcred"); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getImagePullSecrets().size()).isEqualTo(1); assertThat(podSpec.getImagePullSecrets().get(0).getName()).isEqualTo("regcred"); } @Test public void deployWithImagePullSecretsDeploymentProperty() { AppDefinition definition = new AppDefinition("app-test", null); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.imagePullSecrets", "['regcredone','regcredtwo']"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(new KubernetesDeployerProperties()); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getImagePullSecrets().size()).isEqualTo(2); assertThat(podSpec.getImagePullSecrets().get(0).getName()).isEqualTo("regcredone"); assertThat(podSpec.getImagePullSecrets().get(1).getName()).isEqualTo("regcredtwo"); } @Test public void deployWithImagePullSecretsDeployerProperty() { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), null); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setImagePullSecrets(Arrays.asList("regcredone", "regcredtwo")); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getImagePullSecrets().size()).isEqualTo(2); assertThat(podSpec.getImagePullSecrets().get(0).getName()).isEqualTo("regcredone"); assertThat(podSpec.getImagePullSecrets().get(1).getName()).isEqualTo("regcredtwo"); } @Test public void deployWithDeploymentServiceAccountNameDeploymentProperties() { AppDefinition definition = new AppDefinition("app-test", null); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.deploymentServiceAccountName", "myserviceaccount"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(new KubernetesDeployerProperties()); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getServiceAccountName()).isNotNull(); assertThat(podSpec.getServiceAccountName().equals("myserviceaccount")); } @Test public void deployWithDeploymentServiceAccountNameDeployerProperty() { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), null); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setDeploymentServiceAccountName("myserviceaccount"); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getServiceAccountName()).isNotNull(); assertThat(podSpec.getServiceAccountName().equals("myserviceaccount")); } @Test public void deployWithDeploymentServiceAccountNameDeploymentPropertyOverride() { AppDefinition definition = new AppDefinition("app-test", null); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.deploymentServiceAccountName", "overridesan"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setDeploymentServiceAccountName("defaultsan"); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getServiceAccountName()).isNotNull(); assertThat(podSpec.getServiceAccountName().equals("overridesan")); } @Test public void deployWithTolerations() throws Exception { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), new HashMap<>()); deployer = k8sAppDeployer(); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getTolerations()).isNotEmpty(); } @Test public void deployWithGlobalTolerations() { AppDefinition definition = new AppDefinition("app-test", null); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.tolerations", "[{key: 'test', value: 'true', operator: 'Equal', effect: 'NoSchedule', tolerationSeconds: 5}, " + "{key: 'test2', value: 'false', operator: 'Equal', effect: 'NoSchedule', tolerationSeconds: 5}]"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(new KubernetesDeployerProperties()); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getTolerations()).isNotNull(); assertThat(podSpec.getTolerations().size() == 2); assertThat(podSpec.getTolerations().contains(new Toleration("NoSchedule", "test", "Equal", 5L, "true"))); assertThat(podSpec.getTolerations().contains(new Toleration("NoSchedule", "test2", "Equal", 5L, "false"))); } @Test public void deployWithTolerationPropertyOverride() { AppDefinition definition = new AppDefinition("app-test", null); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.tolerations", "[{key: 'test', value: 'true', operator: 'Equal', effect: 'NoSchedule', tolerationSeconds: 5}, " + "{key: 'test2', value: 'false', operator: 'Equal', effect: 'NoSchedule', tolerationSeconds: 5}]"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); KubernetesDeployerProperties.Toleration toleration = new KubernetesDeployerProperties.Toleration(); toleration.setEffect("NoSchedule"); toleration.setKey("test"); toleration.setOperator("Equal"); toleration.setTolerationSeconds(5L); toleration.setValue("false"); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.getTolerations().add(toleration); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getTolerations()).isNotNull(); assertThat(podSpec.getTolerations().size() == 2); assertThat(podSpec.getTolerations().contains(new Toleration("NoSchedule", "test", "Equal", 5L, "true"))); assertThat(podSpec.getTolerations().contains(new Toleration("NoSchedule", "test2", "Equal", 5L, "false"))); } @Test public void deployWithDuplicateTolerationKeyPropertyOverride() { AppDefinition definition = new AppDefinition("app-test", null); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.tolerations", "[{key: 'test', value: 'true', operator: 'Equal', effect: 'NoSchedule', tolerationSeconds: 5}]"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); KubernetesDeployerProperties.Toleration toleration = new KubernetesDeployerProperties.Toleration(); toleration.setEffect("NoSchedule"); toleration.setKey("test"); toleration.setOperator("Equal"); toleration.setTolerationSeconds(5L); toleration.setValue("false"); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.getTolerations().add(toleration); kubernetesDeployerProperties.setStartupHttpProbePort(kubernetesDeployerProperties.getLivenessHttpProbePort()); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getTolerations()).isNotNull(); assertThat(podSpec.getTolerations().size() == 1); assertThat(podSpec.getTolerations().contains(new Toleration("NoSchedule", "test2", "Equal", 5L, "false"))); } @Test public void deployWithDuplicateGlobalToleration() { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), null); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); KubernetesDeployerProperties.Toleration toleration1 = new KubernetesDeployerProperties.Toleration(); toleration1.setEffect("NoSchedule"); toleration1.setKey("test"); toleration1.setOperator("Equal"); toleration1.setTolerationSeconds(5L); toleration1.setValue("false"); kubernetesDeployerProperties.getTolerations().add(toleration1); KubernetesDeployerProperties.Toleration toleration2 = new KubernetesDeployerProperties.Toleration(); toleration2.setEffect("NoSchedule"); toleration2.setKey("test"); toleration2.setOperator("Equal"); toleration2.setTolerationSeconds(5L); toleration2.setValue("true"); kubernetesDeployerProperties.getTolerations().add(toleration2); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getTolerations()).isNotNull(); assertThat(podSpec.getTolerations().size() == 1); assertThat(podSpec.getTolerations().contains(new Toleration("NoSchedule", "test2", "Equal", 5L, "true"))); } @Test public void testInvalidDeploymentLabelDelimiter() { Map props = Collections.singletonMap("spring.cloud.deployer.kubernetes.deploymentLabels", "label1|value1"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); assertThatThrownBy(() -> { this.deploymentPropertiesResolver.getDeploymentLabels(appDeploymentRequest.getDeploymentProperties()); }).isInstanceOf(IllegalArgumentException.class); } @Test public void testInvalidMultipleDeploymentLabelDelimiter() { Map props = Collections.singletonMap("spring.cloud.deployer.kubernetes.deploymentLabels", "label1:value1 label2:value2"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); assertThatThrownBy(() -> { this.deploymentPropertiesResolver.getDeploymentLabels(appDeploymentRequest.getDeploymentProperties()); }).isInstanceOf(IllegalArgumentException.class); } @Test public void testDeploymentLabels() { Map props = Collections.singletonMap("spring.cloud.deployer.kubernetes.deploymentLabels", "label1:value1,label2:value2"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); Map deploymentLabels = this.deploymentPropertiesResolver.getDeploymentLabels(appDeploymentRequest.getDeploymentProperties()); assertThat(deploymentLabels).isNotEmpty(); assertThat(deploymentLabels.size()).as("Invalid number of labels").isEqualTo(2); assertThat(deploymentLabels).containsKey("label1"); assertThat(deploymentLabels.get("label1")).as("Invalid value for 'label1'").isEqualTo("value1"); assertThat(deploymentLabels).containsKey("label2"); assertThat(deploymentLabels.get("label2")).as("Invalid value for 'label2'").isEqualTo("value2"); } @Test public void testSecretKeyRef() { Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.secretKeyRefs", "[{envVarName: 'SECRET_PASSWORD', secretName: 'mySecret', dataKey: 'password'}]"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(new KubernetesDeployerProperties()); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); List envVars = podSpec.getContainers().get(0).getEnv(); assertThat(envVars.size()).as("Invalid number of env vars").isEqualTo(2); EnvVar secretKeyRefEnvVar = envVars.get(0); assertThat(secretKeyRefEnvVar.getName()).as("Unexpected env var name").isEqualTo("SECRET_PASSWORD"); SecretKeySelector secretKeySelector = secretKeyRefEnvVar.getValueFrom().getSecretKeyRef(); assertThat(secretKeySelector.getName()).as("Unexpected secret name").isEqualTo("mySecret"); assertThat(secretKeySelector.getKey()).as("Unexpected secret data key").isEqualTo("password"); } @Test public void testSecretKeyRefMultiple() { Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.secretKeyRefs", "[{envVarName: 'SECRET_PASSWORD', secretName: 'mySecret', dataKey: 'password'}," + "{envVarName: 'SECRET_USERNAME', secretName: 'mySecret2', dataKey: 'username'}]"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(new KubernetesDeployerProperties()); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); List envVars = podSpec.getContainers().get(0).getEnv(); assertThat(envVars.size()).as("Invalid number of env vars").isEqualTo(3); EnvVar secretKeyRefEnvVar = envVars.get(0); assertThat(secretKeyRefEnvVar.getName()).as("Unexpected env var name").isEqualTo("SECRET_PASSWORD"); SecretKeySelector secretKeySelector = secretKeyRefEnvVar.getValueFrom().getSecretKeyRef(); assertThat(secretKeySelector.getName()).as("Unexpected secret name").isEqualTo("mySecret"); assertThat(secretKeySelector.getKey()).as("Unexpected secret data key").isEqualTo("password"); secretKeyRefEnvVar = envVars.get(1); assertThat(secretKeyRefEnvVar.getName()).as("Unexpected env var name").isEqualTo("SECRET_USERNAME"); secretKeySelector = secretKeyRefEnvVar.getValueFrom().getSecretKeyRef(); assertThat(secretKeySelector.getName()).as("Unexpected secret name").isEqualTo("mySecret2"); assertThat(secretKeySelector.getKey()).as("Unexpected secret data key").isEqualTo("username"); } @Test public void testSecretKeyRefGlobal() { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), null); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); KubernetesDeployerProperties.SecretKeyRef secretKeyRef = new KubernetesDeployerProperties.SecretKeyRef(); secretKeyRef.setEnvVarName("SECRET_PASSWORD_GLOBAL"); secretKeyRef.setSecretName("mySecretGlobal"); secretKeyRef.setDataKey("passwordGlobal"); kubernetesDeployerProperties.setSecretKeyRefs(Collections.singletonList(secretKeyRef)); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); List envVars = podSpec.getContainers().get(0).getEnv(); assertThat(envVars.size()).as("Invalid number of env vars").isEqualTo(2); EnvVar secretKeyRefEnvVar = envVars.get(0); assertThat(secretKeyRefEnvVar.getName()).as("Unexpected env var name").isEqualTo("SECRET_PASSWORD_GLOBAL"); SecretKeySelector secretKeySelector = secretKeyRefEnvVar.getValueFrom().getSecretKeyRef(); assertThat(secretKeySelector.getName()).as("Unexpected secret name").isEqualTo("mySecretGlobal"); assertThat(secretKeySelector.getKey()).as("Unexpected secret data key").isEqualTo("passwordGlobal"); } @Test public void testSecretKeyRefPropertyOverride() { Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.secretKeyRefs", "[{envVarName: 'SECRET_PASSWORD_GLOBAL', secretName: 'mySecret', dataKey: 'password'}," + "{envVarName: 'SECRET_USERNAME', secretName: 'mySecret2', dataKey: 'username'}]"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); List globalSecretKeyRefs = new ArrayList<>(); KubernetesDeployerProperties.SecretKeyRef globalSecretKeyRef1 = new KubernetesDeployerProperties.SecretKeyRef(); globalSecretKeyRef1.setEnvVarName("SECRET_PASSWORD_GLOBAL"); globalSecretKeyRef1.setSecretName("mySecretGlobal"); globalSecretKeyRef1.setDataKey("passwordGlobal"); KubernetesDeployerProperties.SecretKeyRef globalSecretKeyRef2 = new KubernetesDeployerProperties.SecretKeyRef(); globalSecretKeyRef2.setEnvVarName("SECRET_USERNAME_GLOBAL"); globalSecretKeyRef2.setSecretName("mySecretGlobal"); globalSecretKeyRef2.setDataKey("usernameGlobal"); globalSecretKeyRefs.add(globalSecretKeyRef1); globalSecretKeyRefs.add(globalSecretKeyRef2); kubernetesDeployerProperties.setSecretKeyRefs(globalSecretKeyRefs); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); List envVars = podSpec.getContainers().get(0).getEnv(); assertThat(envVars.size()).as("Invalid number of env vars").isEqualTo(4); // deploy prop overrides global EnvVar secretKeyRefEnvVar = envVars.get(0); assertThat(secretKeyRefEnvVar.getName()).as("Unexpected env var name").isEqualTo("SECRET_PASSWORD_GLOBAL"); SecretKeySelector secretKeySelector = secretKeyRefEnvVar.getValueFrom().getSecretKeyRef(); assertThat(secretKeySelector.getName()).as("Unexpected secret name").isEqualTo("mySecret"); assertThat(secretKeySelector.getKey()).as("Unexpected secret data key").isEqualTo("password"); // unique deploy prop secretKeyRefEnvVar = envVars.get(1); assertThat(secretKeyRefEnvVar.getName()).as("Unexpected env var name").isEqualTo("SECRET_USERNAME"); secretKeySelector = secretKeyRefEnvVar.getValueFrom().getSecretKeyRef(); assertThat(secretKeySelector.getName()).as("Unexpected secret name").isEqualTo("mySecret2"); assertThat(secretKeySelector.getKey()).as("Unexpected secret data key").isEqualTo("username"); // unique, non-overridden global prop secretKeyRefEnvVar = envVars.get(2); assertThat(secretKeyRefEnvVar.getName()).as("Unexpected env var name").isEqualTo("SECRET_USERNAME_GLOBAL"); secretKeySelector = secretKeyRefEnvVar.getValueFrom().getSecretKeyRef(); assertThat(secretKeySelector.getName()).as("Unexpected secret name").isEqualTo("mySecretGlobal"); assertThat(secretKeySelector.getKey()).as("Unexpected secret data key").isEqualTo("usernameGlobal"); } @Test public void testSecretKeyRefGlobalFromYaml() throws Exception { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), null); deployer = k8sAppDeployer(); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); List envVars = podSpec.getContainers().get(0).getEnv(); assertThat(envVars.size()).as("Invalid number of env vars").isEqualTo(3); EnvVar secretKeyRefEnvVar = envVars.get(0); assertThat(secretKeyRefEnvVar.getName()).as("Unexpected env var name").isEqualTo("SECRET_PASSWORD"); SecretKeySelector secretKeySelector = secretKeyRefEnvVar.getValueFrom().getSecretKeyRef(); assertThat(secretKeySelector.getName()).as("Unexpected secret name").isEqualTo("mySecret"); assertThat(secretKeySelector.getKey()).as("Unexpected secret data key").isEqualTo("myPassword"); } @Test public void testConfigMapKeyRef() { Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.configMapKeyRefs", "[{envVarName: 'MY_ENV', configMapName: 'myConfigMap', dataKey: 'envName'}]"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(new KubernetesDeployerProperties()); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); List envVars = podSpec.getContainers().get(0).getEnv(); assertThat(envVars.size()).as("Invalid number of env vars").isEqualTo(2); EnvVar configMapKeyRefEnvVar = envVars.get(0); assertThat(configMapKeyRefEnvVar.getName()).as("Unexpected env var name").isEqualTo("MY_ENV"); ConfigMapKeySelector configMapKeySelector = configMapKeyRefEnvVar.getValueFrom().getConfigMapKeyRef(); assertThat(configMapKeySelector.getName()).as("Unexpected config map name").isEqualTo("myConfigMap"); assertThat(configMapKeySelector.getKey()).as("Unexpected config map data key").isEqualTo("envName"); } @Test public void testConfigMapKeyRefMultiple() { Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.configMapKeyRefs", "[{envVarName: 'MY_ENV', configMapName: 'myConfigMap', dataKey: 'envName'}," + "{envVarName: 'ENV_VALUES', configMapName: 'myOtherConfigMap', dataKey: 'diskType'}]"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(new KubernetesDeployerProperties()); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); List envVars = podSpec.getContainers().get(0).getEnv(); assertThat(envVars.size()).as("Invalid number of env vars").isEqualTo(3); EnvVar configMapKeyRefEnvVar = envVars.get(0); assertThat(configMapKeyRefEnvVar.getName()).as("Unexpected env var name").isEqualTo("MY_ENV"); ConfigMapKeySelector configMapKeySelector = configMapKeyRefEnvVar.getValueFrom().getConfigMapKeyRef(); assertThat(configMapKeySelector.getName()).as("Unexpected config map name").isEqualTo("myConfigMap"); assertThat(configMapKeySelector.getKey()).as("Unexpected config map data key").isEqualTo("envName"); configMapKeyRefEnvVar = envVars.get(1); assertThat(configMapKeyRefEnvVar.getName()).as("Unexpected env var name").isEqualTo("ENV_VALUES"); configMapKeySelector = configMapKeyRefEnvVar.getValueFrom().getConfigMapKeyRef(); assertThat(configMapKeySelector.getName()).as("Unexpected config map name").isEqualTo("myOtherConfigMap"); assertThat(configMapKeySelector.getKey()).as("Unexpected config map data key").isEqualTo("diskType"); } @Test public void testConfigMapKeyRefGlobal() { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), null); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); KubernetesDeployerProperties.ConfigMapKeyRef configMapKeyRef = new KubernetesDeployerProperties.ConfigMapKeyRef(); configMapKeyRef.setEnvVarName("MY_ENV_GLOBAL"); configMapKeyRef.setConfigMapName("myConfigMapGlobal"); configMapKeyRef.setDataKey("envGlobal"); kubernetesDeployerProperties.setConfigMapKeyRefs(Collections.singletonList(configMapKeyRef)); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); List envVars = podSpec.getContainers().get(0).getEnv(); assertThat(envVars.size()).as("Invalid number of env vars").isEqualTo(2); EnvVar configMapKeyRefEnvVar = envVars.get(0); assertThat(configMapKeyRefEnvVar.getName()).as("Unexpected env var name").isEqualTo("MY_ENV_GLOBAL"); ConfigMapKeySelector configMapKeySelector = configMapKeyRefEnvVar.getValueFrom().getConfigMapKeyRef(); assertThat(configMapKeySelector.getName()).as("Unexpected config map name").isEqualTo("myConfigMapGlobal"); assertThat(configMapKeySelector.getKey()).as("Unexpected config data key").isEqualTo("envGlobal"); } @Test public void testConfigMapKeyRefPropertyOverride() { Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.configMapKeyRefs", "[{envVarName: 'MY_ENV', configMapName: 'myConfigMap', dataKey: 'envName'}," + "{envVarName: 'ENV_VALUES', configMapName: 'myOtherConfigMap', dataKey: 'diskType'}]"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); List globalConfigMapKeyRefs = new ArrayList<>(); KubernetesDeployerProperties.ConfigMapKeyRef globalConfigMapKeyRef1 = new KubernetesDeployerProperties.ConfigMapKeyRef(); globalConfigMapKeyRef1.setEnvVarName("MY_ENV"); globalConfigMapKeyRef1.setConfigMapName("myEnvGlobal"); globalConfigMapKeyRef1.setDataKey("envGlobal"); KubernetesDeployerProperties.ConfigMapKeyRef globalConfigMapKeyRef2 = new KubernetesDeployerProperties.ConfigMapKeyRef(); globalConfigMapKeyRef2.setEnvVarName("MY_VALS_GLOBAL"); globalConfigMapKeyRef2.setConfigMapName("myValsGlobal"); globalConfigMapKeyRef2.setDataKey("valsGlobal"); globalConfigMapKeyRefs.add(globalConfigMapKeyRef1); globalConfigMapKeyRefs.add(globalConfigMapKeyRef2); kubernetesDeployerProperties.setConfigMapKeyRefs(globalConfigMapKeyRefs); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); List envVars = podSpec.getContainers().get(0).getEnv(); assertThat(envVars.size()).as("Invalid number of env vars").isEqualTo(4); // deploy prop overrides global EnvVar configMapKeyRefEnvVar = envVars.get(0); assertThat(configMapKeyRefEnvVar.getName()).as("Unexpected env var name").isEqualTo("MY_ENV"); ConfigMapKeySelector configMapKeySelector = configMapKeyRefEnvVar.getValueFrom().getConfigMapKeyRef(); assertThat(configMapKeySelector.getName()).as("Unexpected config map name").isEqualTo("myConfigMap"); assertThat(configMapKeySelector.getKey()).as("Unexpected config map data key").isEqualTo("envName"); // unique deploy prop configMapKeyRefEnvVar = envVars.get(1); assertThat(configMapKeyRefEnvVar.getName()).as("Unexpected env var name").isEqualTo("ENV_VALUES"); configMapKeySelector = configMapKeyRefEnvVar.getValueFrom().getConfigMapKeyRef(); assertThat(configMapKeySelector.getName()).as("Unexpected config map name").isEqualTo("myOtherConfigMap"); assertThat(configMapKeySelector.getKey()).as("Unexpected config map data key").isEqualTo("diskType"); // unique, non-overridden global prop configMapKeyRefEnvVar = envVars.get(2); assertThat(configMapKeyRefEnvVar.getName()).as("Unexpected env var name").isEqualTo("MY_VALS_GLOBAL"); configMapKeySelector = configMapKeyRefEnvVar.getValueFrom().getConfigMapKeyRef(); assertThat(configMapKeySelector.getName()).as("Unexpected config map name").isEqualTo("myValsGlobal"); assertThat(configMapKeySelector.getKey()).as("Unexpected config map data key").isEqualTo("valsGlobal"); } @Test public void testConfigMapKeyRefGlobalFromYaml() throws Exception { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), null); deployer = k8sAppDeployer(); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); List envVars = podSpec.getContainers().get(0).getEnv(); assertThat(envVars.size()).as("Invalid number of env vars").isEqualTo(3); EnvVar configMapKeyRefEnvVar = envVars.get(1); assertThat(configMapKeyRefEnvVar.getName()).as("Unexpected env var name").isEqualTo("MY_ENV"); ConfigMapKeySelector configMapKeySelector = configMapKeyRefEnvVar.getValueFrom().getConfigMapKeyRef(); assertThat(configMapKeySelector.getName()).as("Unexpected config map name").isEqualTo("myConfigMap"); assertThat(configMapKeySelector.getKey()).as("Unexpected config map data key").isEqualTo("envName"); } @Test public void testInitContainerProperties() { Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.initContainer", "{ \"imageName\": \"busybox:1\", \"containerName\": \"bb_s1\", \"commands\": [\"sh\", \"-c\", \"script1.sh\"] }"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(new KubernetesDeployerProperties()); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getInitContainers()).isNotEmpty(); Container container = podSpec.getInitContainers().get(0); assertThat(container.getImage()).isEqualTo("busybox:1"); assertThat(container.getName()).isEqualTo("bb_s1"); assertThat(container.getCommand()).containsExactly("sh", "-c", "script1.sh"); } @Test public void testInitContainerJsonArrayProperties() { Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.init-containers", "[{ \"imageName\": \"busybox:1\", \"containerName\": \"bb_s1\", \"commands\": [\"sh\", \"-c\", \"script1.sh\"] }]"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(new KubernetesDeployerProperties()); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getInitContainers()).isNotEmpty(); Container container = podSpec.getInitContainers().get(0); assertThat(container.getImage()).isEqualTo("busybox:1"); assertThat(container.getName()).isEqualTo("bb_s1"); assertThat(container.getCommand()).containsExactly("sh", "-c", "script1.sh"); } @Test public void testInitContainerEnvironmentVariables() { Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.initContainers[0]", "{ \"imageName\": \"busybox:1\", \"environmentVariablesFromFieldRefs\": [\"POD_UID=metadata.uid\"] }"); props.put("spring.cloud.deployer.kubernetes.initContainers[1]", "{ \"imageName\": \"busybox:2\", \"configMapRefEnvVars\": [\"myConfigMap\",\"theirMap\"], \"secretRefEnvVars\": [\"mySecret\"] }"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(new KubernetesDeployerProperties()); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getInitContainers()).isNotEmpty(); assertThat(podSpec.getInitContainers().size()).isEqualTo(2); Container container0 = podSpec.getInitContainers().get(0); assertThat(container0.getImage()).isEqualTo("busybox:1"); assertThat(container0.getEnv().get(0).getName()).isEqualTo("POD_UID"); assertThat(container0.getEnv().get(0).getValueFrom().getFieldRef().getFieldPath()).isEqualTo("metadata.uid"); Container container1 = podSpec.getInitContainers().get(1); assertThat(container1.getImage()).isEqualTo("busybox:2"); assertThat(container1.getEnvFrom().get(0).getConfigMapRef().getName()).isEqualTo("myConfigMap"); assertThat(container1.getEnvFrom().get(1).getConfigMapRef().getName()).isEqualTo("theirMap"); assertThat(container1.getEnvFrom().get(2).getSecretRef().getName()).isEqualTo("mySecret"); } @Test public void testMultipleInitContainerProperties() { Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.initContainers[0]", "{ \"imageName\": \"busybox:1\", \"containerName\": \"bb_s1\", \"commands\": [\"sh\", \"-c\", \"script1.sh\"] }"); props.put("spring.cloud.deployer.kubernetes.initContainers[1].imageName", "busybox:2"); props.put("spring.cloud.deployer.kubernetes.initContainers[1].containerName", "bb_s2"); props.put("spring.cloud.deployer.kubernetes.initContainers[1].commands", "sh,-c,script2.sh"); props.put("spring.cloud.deployer.kubernetes.initContainers[1].environmentVariables", "foo=baz"); props.put("spring.cloud.deployer.kubernetes.initContainers[2]", "{ \"image\": \"busybox:3\", \"name\": \"bb_s3\", \"args\": [\"-c\", \"script3.sh\"], \"volumeMounts\": [{\"mountPath\": \"/data\", \"name\": \"s3vol\", \"readOnly\": true}] }"); props.put("spring.cloud.deployer.kubernetes.initContainers[3].image", "busybox:2"); props.put("spring.cloud.deployer.kubernetes.initContainers[3].name", "bb_s4"); props.put("spring.cloud.deployer.kubernetes.initContainers[3].command", "sh,-c,script4.sh"); props.put("spring.cloud.deployer.kubernetes.initContainers[3].env", "foo=bar"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(new KubernetesDeployerProperties()); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getInitContainers()).isNotEmpty(); assertThat(podSpec.getInitContainers().size()).isEqualTo(4); Container container0 = podSpec.getInitContainers().get(0); assertThat(container0.getImage()).isEqualTo("busybox:1"); assertThat(container0.getName()).isEqualTo("bb_s1"); assertThat(container0.getCommand()).containsExactly("sh", "-c", "script1.sh"); Container container1 = podSpec.getInitContainers().get(1); assertThat(container1.getImage()).isEqualTo("busybox:2"); assertThat(container1.getName()).isEqualTo("bb_s2"); assertThat(container1.getCommand()).containsExactly("sh", "-c", "script2.sh"); assertThat(container1.getEnv()).isEqualTo(Collections.singletonList(new EnvVar("foo","baz", null))); Container container2 = podSpec.getInitContainers().get(2); assertThat(container2.getImage()).isEqualTo("busybox:3"); assertThat(container2.getName()).isEqualTo("bb_s3"); assertThat(container2.getArgs()).containsExactly("-c", "script3.sh"); assertThat(container2.getVolumeMounts()).isNotEmpty(); assertThat(container2.getVolumeMounts().get(0).getName()).isEqualTo("s3vol"); assertThat(container2.getVolumeMounts().get(0).getMountPath()).isEqualTo("/data"); assertThat(container2.getVolumeMounts().get(0).getReadOnly()).isTrue(); Container container3 = podSpec.getInitContainers().get(3); assertThat(container3.getImage()).isEqualTo("busybox:2"); assertThat(container3.getName()).isEqualTo("bb_s4"); assertThat(container3.getCommand()).containsExactly("sh", "-c", "script4.sh"); assertThat(container3.getEnv()).isEqualTo(Collections.singletonList(new EnvVar("foo","bar", null))); } @Test public void testNodeAffinityProperty() { Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.affinity.nodeAffinity", "{ requiredDuringSchedulingIgnoredDuringExecution:" + " { nodeSelectorTerms:" + " [ { matchExpressions:" + " [ { key: 'kubernetes.io/e2e-az-name', " + " operator: 'In'," + " values:" + " [ 'e2e-az1', 'e2e-az2']}]}]}, " + " preferredDuringSchedulingIgnoredDuringExecution:" + " [ { weight: 1," + " preference:" + " { matchExpressions:" + " [ { key: 'another-node-label-key'," + " operator: 'In'," + " values:" + " [ 'another-node-label-value' ]}]}}]}"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(new KubernetesDeployerProperties()); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); NodeAffinity nodeAffinity = podSpec.getAffinity().getNodeAffinity(); assertThat(nodeAffinity).as("Node affinity should not be null").isNotNull(); assertThat(nodeAffinity.getRequiredDuringSchedulingIgnoredDuringExecution()).as("RequiredDuringSchedulingIgnoredDuringExecution should not be null").isNotNull(); assertThat(nodeAffinity.getPreferredDuringSchedulingIgnoredDuringExecution().size()).as("PreferredDuringSchedulingIgnoredDuringExecution should have one element").isEqualTo(1); } @Test public void testPodAffinityProperty() { Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.affinity.podAffinity", "{ requiredDuringSchedulingIgnoredDuringExecution:" + " { labelSelector:" + " [ { matchExpressions:" + " [ { key: 'app', " + " operator: 'In'," + " values:" + " [ 'store']}]}], " + " topologyKey: 'kubernetes.io/hostname'}, " + " preferredDuringSchedulingIgnoredDuringExecution:" + " [ { weight: 1," + " podAffinityTerm:" + " { labelSelector:" + " { matchExpressions:" + " [ { key: 'security'," + " operator: 'In'," + " values:" + " [ 'S2' ]}]}, " + " topologyKey: 'failure-domain.beta.kubernetes.io/zone'}}]}"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(new KubernetesDeployerProperties()); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); PodAffinity podAffinity = podSpec.getAffinity().getPodAffinity(); assertThat(podAffinity).as("Pod affinity should not be null").isNotNull(); assertThat(podAffinity.getRequiredDuringSchedulingIgnoredDuringExecution()).as("RequiredDuringSchedulingIgnoredDuringExecution should not be null").isNotNull(); assertThat(podAffinity.getPreferredDuringSchedulingIgnoredDuringExecution().size()).as("PreferredDuringSchedulingIgnoredDuringExecution should have one element").isEqualTo(1); } @Test public void testPodAntiAffinityProperty() { Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.affinity.podAntiAffinity", "{ requiredDuringSchedulingIgnoredDuringExecution:" + " { labelSelector:" + " [ { matchExpressions:" + " [ { key: 'app', " + " operator: 'In'," + " values:" + " [ 'store']}]}], " + " topologyKey: 'kubernetes.io/hostname'}, " + " preferredDuringSchedulingIgnoredDuringExecution:" + " [ { weight: 1," + " podAffinityTerm:" + " { labelSelector:" + " { matchExpressions:" + " [ { key: 'security'," + " operator: 'In'," + " values:" + " [ 'S2' ]}]}, " + " topologyKey: 'failure-domain.beta.kubernetes.io/zone'}}]}"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(new KubernetesDeployerProperties()); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); PodAntiAffinity podAntiAffinity = podSpec.getAffinity().getPodAntiAffinity(); assertThat(podAntiAffinity).as("Pod anti-affinity should not be null").isNotNull(); assertThat(podAntiAffinity.getRequiredDuringSchedulingIgnoredDuringExecution()).as("RequiredDuringSchedulingIgnoredDuringExecution should not be null").isNotNull(); assertThat(podAntiAffinity.getPreferredDuringSchedulingIgnoredDuringExecution().size()).as("PreferredDuringSchedulingIgnoredDuringExecution should have one element").isEqualTo(1); } @Test public void testNodeAffinityGlobalProperty() { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), null); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); NodeSelectorTerm nodeSelectorTerm = new NodeSelectorTerm(); nodeSelectorTerm.setMatchExpressions(Arrays.asList(new NodeSelectorRequirementBuilder() .withKey("kubernetes.io/e2e-az-name") .withOperator("In") .withValues("e2e-az1", "e2e-az2") .build())); NodeSelectorTerm nodeSelectorTerm2 = new NodeSelectorTerm(); nodeSelectorTerm2.setMatchExpressions(Arrays.asList(new NodeSelectorRequirementBuilder() .withKey("another-node-label-key") .withOperator("In") .withValues("another-node-label-value2") .build())); PreferredSchedulingTerm preferredSchedulingTerm = new PreferredSchedulingTerm(nodeSelectorTerm2, 1); NodeAffinity nodeAffinity = new AffinityBuilder() .withNewNodeAffinity() .withNewRequiredDuringSchedulingIgnoredDuringExecution() .withNodeSelectorTerms(nodeSelectorTerm) .endRequiredDuringSchedulingIgnoredDuringExecution() .withPreferredDuringSchedulingIgnoredDuringExecution(preferredSchedulingTerm) .endNodeAffinity() .buildNodeAffinity(); kubernetesDeployerProperties.setNodeAffinity(nodeAffinity); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); NodeAffinity nodeAffinityTest = podSpec.getAffinity().getNodeAffinity(); assertThat(nodeAffinityTest).as("Node affinity should not be null").isNotNull(); assertThat(nodeAffinityTest.getRequiredDuringSchedulingIgnoredDuringExecution()).as("RequiredDuringSchedulingIgnoredDuringExecution should not be null").isNotNull(); assertThat(nodeAffinityTest.getPreferredDuringSchedulingIgnoredDuringExecution().size()).as("PreferredDuringSchedulingIgnoredDuringExecution should have one element").isEqualTo(1); } @Test public void testPodAffinityGlobalProperty() { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), null); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); LabelSelector labelSelector = new LabelSelector(); labelSelector.setMatchExpressions(Arrays.asList(new LabelSelectorRequirementBuilder() .withKey("security") .withOperator("In") .withValues("S1") .build())); PodAffinityTerm podAffinityTerm = new PodAffinityTerm(labelSelector, null, null, null, null, "failure-domain.beta.kubernetes.io/zone"); LabelSelector labelSelector2 = new LabelSelector(); labelSelector2.setMatchExpressions(Arrays.asList(new LabelSelectorRequirementBuilder() .withKey("security") .withOperator("In") .withValues("s2") .build())); PodAffinityTerm podAffinityTerm2 = new PodAffinityTerm(labelSelector2, null, null, null, null, "failure-domain.beta.kubernetes.io/zone"); WeightedPodAffinityTerm weightedPodAffinityTerm = new WeightedPodAffinityTerm(podAffinityTerm2, 100); PodAffinity podAffinity = new AffinityBuilder() .withNewPodAffinity() .withRequiredDuringSchedulingIgnoredDuringExecution(podAffinityTerm) .withPreferredDuringSchedulingIgnoredDuringExecution(weightedPodAffinityTerm) .endPodAffinity() .buildPodAffinity(); kubernetesDeployerProperties.setPodAffinity(podAffinity); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); PodAffinity podAffinityTest = podSpec.getAffinity().getPodAffinity(); assertThat(podAffinityTest).as("Pod affinity should not be null").isNotNull(); assertThat(podAffinityTest.getRequiredDuringSchedulingIgnoredDuringExecution()).as("RequiredDuringSchedulingIgnoredDuringExecution should not be null").isNotNull(); assertThat(podAffinityTest.getPreferredDuringSchedulingIgnoredDuringExecution().size()).as("PreferredDuringSchedulingIgnoredDuringExecution should have one element").isEqualTo(1); } @Test public void testPodAntiAffinityGlobalProperty() { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), null); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setStartupHttpProbePort(kubernetesDeployerProperties.getLivenessHttpProbePort()); LabelSelector labelSelector = new LabelSelector(); labelSelector.setMatchExpressions(Arrays.asList(new LabelSelectorRequirementBuilder() .withKey("app") .withOperator("In") .withValues("store") .build())); PodAffinityTerm podAffinityTerm = new PodAffinityTerm(labelSelector, null, null, null, null, "kubernetes.io/hostname"); LabelSelector labelSelector2 = new LabelSelector(); labelSelector2.setMatchExpressions(Arrays.asList(new LabelSelectorRequirementBuilder() .withKey("security") .withOperator("In") .withValues("s2") .build())); PodAffinityTerm podAffinityTerm2 = new PodAffinityTerm(labelSelector2, null, null, null, null, "failure-domain.beta.kubernetes.io/zone"); WeightedPodAffinityTerm weightedPodAffinityTerm = new WeightedPodAffinityTerm(podAffinityTerm2, 100); PodAntiAffinity podAntiAffinity = new AffinityBuilder() .withNewPodAntiAffinity() .withRequiredDuringSchedulingIgnoredDuringExecution(podAffinityTerm) .withPreferredDuringSchedulingIgnoredDuringExecution(weightedPodAffinityTerm) .endPodAntiAffinity() .buildPodAntiAffinity(); kubernetesDeployerProperties.setPodAntiAffinity(podAntiAffinity); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); PodAntiAffinity podAntiAffinityTest = podSpec.getAffinity().getPodAntiAffinity(); assertThat(podAntiAffinityTest).as("Pod anti-affinity should not be null").isNotNull(); assertThat(podAntiAffinityTest.getRequiredDuringSchedulingIgnoredDuringExecution()).as("RequiredDuringSchedulingIgnoredDuringExecution should not be null").isNotNull(); assertThat(podAntiAffinityTest.getPreferredDuringSchedulingIgnoredDuringExecution().size()).as("PreferredDuringSchedulingIgnoredDuringExecution should have one element").isEqualTo(1); } @Test public void testNodeAffinityFromYaml() throws Exception { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), null); deployer = k8sAppDeployer(); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); NodeAffinity nodeAffinity = podSpec.getAffinity().getNodeAffinity(); assertThat(nodeAffinity).as("Node affinity should not be null").isNotNull(); assertThat(nodeAffinity.getRequiredDuringSchedulingIgnoredDuringExecution()).as("RequiredDuringSchedulingIgnoredDuringExecution should not be null").isNotNull(); assertThat(nodeAffinity.getPreferredDuringSchedulingIgnoredDuringExecution().size()).as("PreferredDuringSchedulingIgnoredDuringExecution should have one element").isEqualTo(1); } @Test public void testPodAffinityFromYaml() throws Exception { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), null); deployer = k8sAppDeployer(); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); PodAffinity podAffinity = podSpec.getAffinity().getPodAffinity(); assertThat(podAffinity).as("Pod affinity should not be null").isNotNull(); assertThat(podAffinity.getRequiredDuringSchedulingIgnoredDuringExecution()).as("RequiredDuringSchedulingIgnoredDuringExecution should not be null").isNotNull(); assertThat(podAffinity.getPreferredDuringSchedulingIgnoredDuringExecution().size()).as("PreferredDuringSchedulingIgnoredDuringExecution should have one element").isEqualTo(1); } @Test public void testPodAntiAffinityFromYaml() throws Exception { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), null); deployer = k8sAppDeployer(); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); PodAntiAffinity podAntiAffinity = podSpec.getAffinity().getPodAntiAffinity(); assertThat(podAntiAffinity).as("Pod anti-affinity should not be null").isNotNull(); assertThat(podAntiAffinity.getRequiredDuringSchedulingIgnoredDuringExecution()).as("RequiredDuringSchedulingIgnoredDuringExecution should not be null").isNotNull(); assertThat(podAntiAffinity.getPreferredDuringSchedulingIgnoredDuringExecution().size()).as("PreferredDuringSchedulingIgnoredDuringExecution should have one element").isEqualTo(1); } @Test public void testNodeAffinityPropertyOverrideGlobal() { Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.affinity.nodeAffinity", "{ requiredDuringSchedulingIgnoredDuringExecution:" + " { nodeSelectorTerms:" + " [ { matchExpressions:" + " [ { key: 'kubernetes.io/e2e-az-name', " + " operator: 'In'," + " values:" + " [ 'e2e-az1', 'e2e-az2']}]}]}, " + " preferredDuringSchedulingIgnoredDuringExecution:" + " [ { weight: 1," + " preference:" + " { matchExpressions:" + " [ { key: 'another-node-label-key'," + " operator: 'In'," + " values:" + " [ 'another-node-label-value' ]}]}}]}"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); NodeSelectorTerm nodeSelectorTerm = new NodeSelectorTerm(); nodeSelectorTerm.setMatchExpressions(Arrays.asList(new NodeSelectorRequirementBuilder() .withKey("kubernetes.io/e2e-az-name") .withOperator("In") .withValues("e2e-az1", "e2e-az2") .build())); NodeAffinity nodeAffinity = new AffinityBuilder() .withNewNodeAffinity() .withNewRequiredDuringSchedulingIgnoredDuringExecution() .withNodeSelectorTerms(nodeSelectorTerm) .endRequiredDuringSchedulingIgnoredDuringExecution() .endNodeAffinity() .buildNodeAffinity(); kubernetesDeployerProperties.setNodeAffinity(nodeAffinity); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); NodeAffinity nodeAffinityTest = podSpec.getAffinity().getNodeAffinity(); assertThat(nodeAffinityTest).as("Node affinity should not be null").isNotNull(); assertThat(nodeAffinityTest.getRequiredDuringSchedulingIgnoredDuringExecution()).as("RequiredDuringSchedulingIgnoredDuringExecution should not be null").isNotNull(); assertThat(nodeAffinityTest.getPreferredDuringSchedulingIgnoredDuringExecution().size()).as("PreferredDuringSchedulingIgnoredDuringExecution should have one element").isEqualTo(1); } @Test public void testPodAffinityPropertyOverrideGlobal() { Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.affinity.podAffinity", "{ requiredDuringSchedulingIgnoredDuringExecution:" + " { labelSelector:" + " [ { matchExpressions:" + " [ { key: 'security', " + " operator: 'In'," + " values:" + " [ 'S1']}]}], " + " topologyKey: 'failure-domain.beta.kubernetes.io/zone'}, " + " preferredDuringSchedulingIgnoredDuringExecution:" + " [ { weight: 1," + " podAffinityTerm:" + " { labelSelector:" + " { matchExpressions:" + " [ { key: 'security'," + " operator: 'In'," + " values:" + " [ 'S2' ]}]}, " + " topologyKey: 'failure-domain.beta.kubernetes.io/zone'}}]}"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); LabelSelector labelSelector = new LabelSelector(); labelSelector.setMatchExpressions(Arrays.asList(new LabelSelectorRequirementBuilder() .withKey("tolerance") .withOperator("In") .withValues("Reliable") .build())); PodAffinityTerm podAffinityTerm = new PodAffinityTerm(labelSelector, null, null, null, null, "failure-domain.beta.kubernetes.io/zone"); PodAffinity podAffinity = new AffinityBuilder() .withNewPodAffinity() .withRequiredDuringSchedulingIgnoredDuringExecution(podAffinityTerm) .endPodAffinity() .buildPodAffinity(); kubernetesDeployerProperties.setPodAffinity(podAffinity); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); PodAffinity podAffinityTest = podSpec.getAffinity().getPodAffinity(); assertThat(podAffinityTest).as("Pod affinity should not be null").isNotNull(); assertThat(podAffinityTest.getRequiredDuringSchedulingIgnoredDuringExecution()).as("RequiredDuringSchedulingIgnoredDuringExecution should not be null").isNotNull(); assertThat(podAffinityTest.getPreferredDuringSchedulingIgnoredDuringExecution().size()).as("PreferredDuringSchedulingIgnoredDuringExecution should have one element").isEqualTo(1); } @Test public void testPodAntiAffinityPropertyOverrideGlobal() { Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.affinity.podAntiAffinity", "{ requiredDuringSchedulingIgnoredDuringExecution:" + " { labelSelector:" + " [ { matchExpressions:" + " [ { key: 'app', " + " operator: 'In'," + " values:" + " [ 'store']}]}], " + " topologyKey: 'kubernetes.io/hostnam'}, " + " preferredDuringSchedulingIgnoredDuringExecution:" + " [ { weight: 1," + " podAffinityTerm:" + " { labelSelector:" + " { matchExpressions:" + " [ { key: 'security'," + " operator: 'In'," + " values:" + " [ 'S2' ]}]}, " + " topologyKey: 'failure-domain.beta.kubernetes.io/zone'}}]}"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); LabelSelector labelSelector = new LabelSelector(); labelSelector.setMatchExpressions(Arrays.asList(new LabelSelectorRequirementBuilder() .withKey("version") .withOperator("Equals") .withValues("v1") .build())); PodAffinityTerm podAffinityTerm = new PodAffinityTerm(labelSelector, null, null, null, null, "kubernetes.io/hostnam"); PodAntiAffinity podAntiAffinity = new AffinityBuilder() .withNewPodAntiAffinity() .withRequiredDuringSchedulingIgnoredDuringExecution(podAffinityTerm) .endPodAntiAffinity() .buildPodAntiAffinity(); kubernetesDeployerProperties.setPodAntiAffinity(podAntiAffinity); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); PodAntiAffinity podAntiAffinityTest = podSpec.getAffinity().getPodAntiAffinity(); assertThat(podAntiAffinityTest).as("Pod anti-affinity should not be null").isNotNull(); assertThat(podAntiAffinityTest.getRequiredDuringSchedulingIgnoredDuringExecution()).as("RequiredDuringSchedulingIgnoredDuringExecution should not be null").isNotNull(); assertThat(podAntiAffinityTest.getPreferredDuringSchedulingIgnoredDuringExecution().size()).as("PreferredDuringSchedulingIgnoredDuringExecution should have one element").isEqualTo(1); } @Nested @DisplayName("creates pod spec with pod security context") class CreatePodSpecWithPodSecurityContext { @Test @DisplayName("created from deployment property with all fields") void createdFromDeploymentPropertyWithAllFields() { KubernetesDeployerProperties globalDeployerProps = new KubernetesDeployerProperties(); Map deploymentProps = new HashMap<>(); deploymentProps.put("spring.cloud.deployer.kubernetes.podSecurityContext", "{" + " fsGroup: 65534" + ", fsGroupChangePolicy: Always" + ", runAsUser: 65534" + ", runAsGroup: 65534" + ", runAsNonRoot: true" + ", seLinuxOptions: { level: \"s0:c123,c456\" }" + ", seccompProfile: { type: Localhost, localhostProfile: my-profiles/profile-allow.json }" + ", supplementalGroups: [65534, 65535]" + ", sysctls: [{name: \"kernel.shm_rmid_forced\", value: 0}, {name: \"net.core.somaxconn\", value: 1024}]" + ", windowsOptions: { gmsaCredentialSpec: \"specA\", gmsaCredentialSpecName: \"specA-name\", hostProcess: true, runAsUserName: \"userA\" }" + "}"); PodSecurityContext expectedPodSecurityContext = new PodSecurityContextBuilder() .withFsGroup(65534L) .withFsGroupChangePolicy("Always") .withRunAsUser(65534L) .withRunAsGroup(65534L) .withRunAsNonRoot(true) .withSeLinuxOptions(new SELinuxOptions("s0:c123,c456", null, null, null)) .withSeccompProfile(new SeccompProfile("my-profiles/profile-allow.json", "Localhost")) .withSupplementalGroups(65534L, 65535L) .withSysctls(new Sysctl("kernel.shm_rmid_forced", "0"), new Sysctl("net.core.somaxconn", "1024")) .withWindowsOptions(new WindowsSecurityContextOptions("specA", "specA-name", true, "userA")) .build(); assertThatDeployerCreatesPodSpecWithPodSecurityContext(globalDeployerProps, deploymentProps, expectedPodSecurityContext); } @Test @DisplayName("created from deployment property with runAsUser only") void createdFromDeploymentPropertyWithRunAsUserOnly() { KubernetesDeployerProperties globalDeployerProps = new KubernetesDeployerProperties(); Map deploymentProps = new HashMap<>(); deploymentProps.put("spring.cloud.deployer.kubernetes.podSecurityContext", "{runAsUser: 65534}"); PodSecurityContext expectedPodSecurityContext = new PodSecurityContextBuilder() .withRunAsUser(65534L) .build(); assertThatDeployerCreatesPodSpecWithPodSecurityContext(globalDeployerProps, deploymentProps, expectedPodSecurityContext); } @Test @DisplayName("created from deployment property with fsGroup only") void createdFromDeploymentPropertyWithFsGroupOnly() { KubernetesDeployerProperties globalDeployerProps = new KubernetesDeployerProperties(); Map deploymentProps = new HashMap<>(); deploymentProps.put("spring.cloud.deployer.kubernetes.podSecurityContext", "{fsGroup: 65534}"); PodSecurityContext expectedPodSecurityContext = new PodSecurityContextBuilder() .withFsGroup(65534L) .build(); assertThatDeployerCreatesPodSpecWithPodSecurityContext(globalDeployerProps, deploymentProps, expectedPodSecurityContext); } @Test @DisplayName("created from deployment property with supplementalGroups only") void createdFromDeploymentPropertyWithSupplementalGroupsOnly() { KubernetesDeployerProperties globalDeployerProps = new KubernetesDeployerProperties(); Map deploymentProps = new HashMap<>(); deploymentProps.put("spring.cloud.deployer.kubernetes.podSecurityContext", "{supplementalGroups: [65534,65535]}"); PodSecurityContext expectedPodSecurityContext = new PodSecurityContextBuilder() .withSupplementalGroups(65534L, 65535L) .build(); assertThatDeployerCreatesPodSpecWithPodSecurityContext(globalDeployerProps, deploymentProps, expectedPodSecurityContext); } @Test @DisplayName("created from deployment property with seccompProfile only") void createdFromDeploymentPropertyWithSeccompProfileOnly() { KubernetesDeployerProperties globalDeployerProps = new KubernetesDeployerProperties(); Map deploymentProps = new HashMap<>(); deploymentProps.put("spring.cloud.deployer.kubernetes.podSecurityContext", "{seccompProfile: { type: RuntimeDefault}}"); PodSecurityContext expectedPodSecurityContext = new PodSecurityContextBuilder() .withSeccompProfile(new SeccompProfile(null, "RuntimeDefault")) .build(); assertThatDeployerCreatesPodSpecWithPodSecurityContext(globalDeployerProps, deploymentProps, expectedPodSecurityContext); } @Test @DisplayName("created from global deployer property sourced from yaml") void createdFromGlobalDeployerPropertySourcedFromYaml() throws Exception { KubernetesDeployerProperties globalDeployerProps = bindDeployerProperties(); Map deploymentProps = new HashMap<>(); PodSecurityContext expectedPodSecurityContext = new PodSecurityContextBuilder() .withFsGroup(65534L) .withFsGroupChangePolicy("Always") .withRunAsUser(65534L) .withRunAsGroup(65534L) .withRunAsNonRoot(true) .withSeLinuxOptions(new SELinuxOptions("s0:c123,c456", null, null, null)) .withSeccompProfile(new SeccompProfile("my-profiles/profile-allow.json", "Localhost")) .withSupplementalGroups(65534L, 65535L) .withSysctls(new Sysctl("kernel.shm_rmid_forced", "0"), new Sysctl("net.core.somaxconn", "1024")) .withWindowsOptions(new WindowsSecurityContextOptions("specA", "specA-name", true, "userA")) .build(); assertThatDeployerCreatesPodSpecWithPodSecurityContext(globalDeployerProps, deploymentProps, expectedPodSecurityContext); } @Test @DisplayName("created from global deployer property") void createdFromGlobalDeployerProperty() { KubernetesDeployerProperties globalDeployerProps = new KubernetesDeployerProperties(); KubernetesDeployerProperties.PodSecurityContext securityContext = new KubernetesDeployerProperties.PodSecurityContext(); securityContext.setFsGroup(65534L); securityContext.setRunAsUser(65534L); securityContext.setSupplementalGroups(new Long[]{65534L}); KubernetesDeployerProperties.SeccompProfile seccompProfile = new KubernetesDeployerProperties.SeccompProfile(); seccompProfile.setType("Localhost"); seccompProfile.setLocalhostProfile("profile.json"); securityContext.setSeccompProfile(seccompProfile); globalDeployerProps.setPodSecurityContext(securityContext); Map deploymentProps = Collections.emptyMap(); PodSecurityContext expectedPodSecurityContext = new PodSecurityContextBuilder() .withRunAsUser(65534L) .withFsGroup(65534L) .withSupplementalGroups(65534L) .withSeccompProfile(new SeccompProfile("profile.json", "Localhost")) .build(); assertThatDeployerCreatesPodSpecWithPodSecurityContext(globalDeployerProps, deploymentProps, expectedPodSecurityContext); } @Test @DisplayName("created from deployment property overrriding global deployer property") void createdFromDeploymentPropertyOverridingGlobalDeployerProperty() { KubernetesDeployerProperties globalDeployerProps = new KubernetesDeployerProperties(); KubernetesDeployerProperties.PodSecurityContext securityContext = new KubernetesDeployerProperties.PodSecurityContext(); securityContext.setFsGroup(1000L); securityContext.setRunAsUser(1000L); securityContext.setSupplementalGroups(new Long[]{1000L}); KubernetesDeployerProperties.SeccompProfile seccompProfile = new KubernetesDeployerProperties.SeccompProfile(); seccompProfile.setType("Localhost"); seccompProfile.setLocalhostProfile("sec/default-allow.json"); securityContext.setSeccompProfile(seccompProfile); globalDeployerProps.setPodSecurityContext(securityContext); Map deploymentProps = new HashMap<>(); deploymentProps.put("spring.cloud.deployer.kubernetes.podSecurityContext", "{runAsUser: 65534, fsGroup: 65534, supplementalGroups: [65534,65535], seccompProfile: { type: Localhost, localhostProfile: sec/custom-allow.json } }"); PodSecurityContext expectedPodSecurityContext = new PodSecurityContextBuilder() .withRunAsUser(65534L) .withFsGroup(65534L) .withSupplementalGroups(65534L, 65535L) .withSeccompProfile(new SeccompProfile("sec/custom-allow.json", "Localhost")) .build(); assertThatDeployerCreatesPodSpecWithPodSecurityContext(globalDeployerProps, deploymentProps, expectedPodSecurityContext); } private void assertThatDeployerCreatesPodSpecWithPodSecurityContext( KubernetesDeployerProperties globalDeployerProps, Map deploymentProps, PodSecurityContext expectedPodSecurityContext ) { PodSpec podSpec = deployerCreatesPodSpec(globalDeployerProps, deploymentProps); PodSecurityContext actualPodSecurityContext = podSpec.getSecurityContext(); assertThat(actualPodSecurityContext) .isNotNull() .isEqualTo(expectedPodSecurityContext); } } @Nested @DisplayName("creates pod spec with container security context") class CreatePodSpecWithContainerSecurityContext { @Test @DisplayName("created from deployment property with all fields") void createdFromDeploymentPropertyWithAllFields() { KubernetesDeployerProperties globalDeployerProps = new KubernetesDeployerProperties(); Map deploymentProps = new HashMap<>(); deploymentProps.put("spring.cloud.deployer.kubernetes.containerSecurityContext", "{" + " allowPrivilegeEscalation: true" + ", capabilities: { add: [ \"a\", \"b\" ], drop: [ \"c\" ] }" + ", privileged: true" + ", procMount: DefaultProcMount" + ", readOnlyRootFilesystem: true" + ", runAsUser: 65534" + ", runAsGroup: 65534" + ", runAsNonRoot: true" + ", seLinuxOptions: { level: \"s0:c123,c456\" }" + ", seccompProfile: { type: Localhost, localhostProfile: my-profiles/profile-allow.json }" + ", windowsOptions: { gmsaCredentialSpec: \"specA\", gmsaCredentialSpecName: \"specA-name\", hostProcess: true, runAsUserName: \"userA\" }" + "}"); SecurityContext expectedContainerSecurityContext = new SecurityContextBuilder() .withAllowPrivilegeEscalation(true) .withCapabilities(new Capabilities(Arrays.asList("a", "b"), Arrays.asList("c"))) .withPrivileged(true) .withProcMount("DefaultProcMount") .withReadOnlyRootFilesystem(true) .withRunAsUser(65534L) .withRunAsGroup(65534L) .withRunAsNonRoot(true) .withSeLinuxOptions(new SELinuxOptions("s0:c123,c456", null, null, null)) .withSeccompProfile(new SeccompProfile("my-profiles/profile-allow.json", "Localhost")) .withWindowsOptions(new WindowsSecurityContextOptions("specA", "specA-name", true, "userA")) .build(); assertThatDeployerCreatesPodSpecWithContainerSecurityContext(globalDeployerProps, deploymentProps, expectedContainerSecurityContext); } @Test @DisplayName("created from deployment property with allowPrivilegeEscalation only") void createdFromDeploymentPropertyWithAllowPrivilegeEscalationOnly() { KubernetesDeployerProperties globalDeployerProps = new KubernetesDeployerProperties(); Map deploymentProps = Collections.singletonMap("spring.cloud.deployer.kubernetes.containerSecurityContext", "{allowPrivilegeEscalation: true}"); SecurityContext expectedContainerSecurityContext = new SecurityContextBuilder() .withAllowPrivilegeEscalation(true) .build(); assertThatDeployerCreatesPodSpecWithContainerSecurityContext(globalDeployerProps, deploymentProps, expectedContainerSecurityContext); } @Test @DisplayName("created from deployment property with readOnlyRootFilesystem only") void createdFromDeploymentPropertyWithReadOnlyRootFilesystemOnly() { KubernetesDeployerProperties globalDeployerProps = new KubernetesDeployerProperties(); Map deploymentProps = Collections.singletonMap("spring.cloud.deployer.kubernetes.containerSecurityContext", "{readOnlyRootFilesystem: true}"); SecurityContext expectedContainerSecurityContext = new SecurityContextBuilder() .withReadOnlyRootFilesystem(true) .build(); assertThatDeployerCreatesPodSpecWithContainerSecurityContext(globalDeployerProps, deploymentProps, expectedContainerSecurityContext); } @Test @DisplayName("created from global deployer property sourced from yaml") void createdFromGlobalDeployerPropertySourcedFromYaml() throws Exception { KubernetesDeployerProperties globalDeployerProps = bindDeployerProperties(); Map deploymentProps = Collections.emptyMap(); SecurityContext expectedContainerSecurityContext = new SecurityContextBuilder() .withAllowPrivilegeEscalation(true) .withCapabilities(new Capabilities(Arrays.asList("a", "b"), Arrays.asList("c"))) .withPrivileged(true) .withProcMount("DefaultProcMount") .withReadOnlyRootFilesystem(true) .withRunAsUser(65534L) .withRunAsGroup(65534L) .withRunAsNonRoot(true) .withSeLinuxOptions(new SELinuxOptions("s0:c123,c456", null, null, null)) .withSeccompProfile(new SeccompProfile("my-profiles/profile-allow.json", "Localhost")) .withWindowsOptions(new WindowsSecurityContextOptions("specA", "specA-name", true, "userA")) .build(); assertThatDeployerCreatesPodSpecWithContainerSecurityContext(globalDeployerProps, deploymentProps, expectedContainerSecurityContext); } @Test @DisplayName("created from global deployer property") void createdFromGlobalDeployerProperty() { KubernetesDeployerProperties globalDeployerProps = new KubernetesDeployerProperties(); KubernetesDeployerProperties.ContainerSecurityContext securityContext = new KubernetesDeployerProperties.ContainerSecurityContext(); securityContext.setAllowPrivilegeEscalation(false); securityContext.setReadOnlyRootFilesystem(true); globalDeployerProps.setContainerSecurityContext(securityContext); Map deploymentProps = Collections.emptyMap(); SecurityContext expectedContainerSecurityContext = new SecurityContextBuilder() .withAllowPrivilegeEscalation(false) .withReadOnlyRootFilesystem(true) .build(); assertThatDeployerCreatesPodSpecWithContainerSecurityContext(globalDeployerProps, deploymentProps, expectedContainerSecurityContext); } @Test @DisplayName("created from deployment property overrriding global deployer property") void createdFromDeploymentPropertyOverridingGlobalDeployerProperty() { KubernetesDeployerProperties globalDeployerProps = new KubernetesDeployerProperties(); KubernetesDeployerProperties.ContainerSecurityContext securityContext = new KubernetesDeployerProperties.ContainerSecurityContext(); securityContext.setAllowPrivilegeEscalation(true); securityContext.setReadOnlyRootFilesystem(false); globalDeployerProps.setContainerSecurityContext(securityContext); Map deploymentProps = Collections.singletonMap("spring.cloud.deployer.kubernetes.containerSecurityContext", "{allowPrivilegeEscalation: false, readOnlyRootFilesystem: true}"); SecurityContext expectedContainerSecurityContext = new SecurityContextBuilder() .withAllowPrivilegeEscalation(false) .withReadOnlyRootFilesystem(true) .build(); assertThatDeployerCreatesPodSpecWithContainerSecurityContext(globalDeployerProps, deploymentProps, expectedContainerSecurityContext); } private void assertThatDeployerCreatesPodSpecWithContainerSecurityContext( KubernetesDeployerProperties globalDeployerProps, Map deploymentProps, SecurityContext expectedContainerSecurityContext ) { PodSpec podSpec = deployerCreatesPodSpec(globalDeployerProps, deploymentProps); assertThat(podSpec.getContainers()) .singleElement() .extracting(Container::getSecurityContext) .isEqualTo(expectedContainerSecurityContext); } } private PodSpec deployerCreatesPodSpec(KubernetesDeployerProperties globalDeployerProperties, Map deploymentProperties) { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), deploymentProperties); KubernetesAppDeployer deployer = k8sAppDeployer(globalDeployerProperties); return deployer.createPodSpec(appDeploymentRequest); } @Test public void testWithLifecyclePostStart() { Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.lifecycle.postStart.exec.command", "/bin/sh,-c,echo Hello from the postStart handler > /usr/share/message"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getContainers().get(0).getLifecycle().getPostStart().getExec().getCommand()) .containsExactlyInAnyOrder("/bin/sh", "-c", "echo Hello from the postStart handler > /usr/share/message"); } @Test public void testWithLifecyclePreStop() { Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.lifecycle.preStop.exec.command", "/bin/sh,-c,nginx -s quit; while killall -0 nginx; do sleep 1; done"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getContainers().get(0).getLifecycle().getPreStop().getExec().getCommand()) .containsExactlyInAnyOrder( "/bin/sh", "-c", "nginx -s quit; while killall -0 nginx; do sleep 1; done"); } @Test public void testLifecyclePostStartOverridesGlobalPostStart() { Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.lifecycle.postStart.exec.command", "/bin/sh,-c,nginx -s quit; while killall -0 nginx; do sleep 1; done"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); KubernetesDeployerProperties.Lifecycle lifecycle = new KubernetesDeployerProperties.Lifecycle(); lifecycle.setPostStart(new KubernetesDeployerProperties.Lifecycle.Hook() { @Override KubernetesDeployerProperties.Lifecycle.Exec getExec() { return new KubernetesDeployerProperties.Lifecycle.Exec() { @Override List getCommand() { return Arrays.asList("echo", "postStart"); } }; } }); lifecycle.setPreStop(new KubernetesDeployerProperties.Lifecycle.Hook() { @Override KubernetesDeployerProperties.Lifecycle.Exec getExec() { return new KubernetesDeployerProperties.Lifecycle.Exec() { @Override List getCommand() { return Arrays.asList("echo", "preStop"); } }; } }); kubernetesDeployerProperties.setLifecycle(lifecycle); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getContainers().get(0).getLifecycle().getPostStart().getExec().getCommand()) .containsExactlyInAnyOrder( "/bin/sh", "-c", "nginx -s quit; while killall -0 nginx; do sleep 1; done"); assertThat(podSpec.getContainers().get(0).getLifecycle().getPreStop().getExec().getCommand()) .containsExactlyInAnyOrder("echo", "preStop"); } @Test public void testLifecyclePrestopOverridesGlobalPrestop() { Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.lifecycle.preStop.exec.command", "/bin/sh,-c,nginx -s quit; while killall -0 nginx; do sleep 1; done"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); KubernetesDeployerProperties.Lifecycle lifecycle = new KubernetesDeployerProperties.Lifecycle(); lifecycle.setPostStart(new KubernetesDeployerProperties.Lifecycle.Hook() { @Override KubernetesDeployerProperties.Lifecycle.Exec getExec() { return new KubernetesDeployerProperties.Lifecycle.Exec() { @Override List getCommand() { return Arrays.asList("echo", "postStart"); } }; } }); lifecycle.setPreStop(new KubernetesDeployerProperties.Lifecycle.Hook() { @Override KubernetesDeployerProperties.Lifecycle.Exec getExec() { return new KubernetesDeployerProperties.Lifecycle.Exec() { @Override List getCommand() { return Arrays.asList("echo", "preStop"); } }; } }); kubernetesDeployerProperties.setLifecycle(lifecycle); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getContainers().get(0).getLifecycle().getPreStop().getExec().getCommand()) .containsExactlyInAnyOrder( "/bin/sh", "-c", "nginx -s quit; while killall -0 nginx; do sleep 1; done"); assertThat(podSpec.getContainers().get(0).getLifecycle().getPostStart().getExec().getCommand()) .containsExactlyInAnyOrder("echo", "postStart"); } @Test public void terminationGracePeriodFromDeployerProp() { Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.terminationGracePeriodSeconds", "5150"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setTerminationGracePeriodSeconds(6160L); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getTerminationGracePeriodSeconds()).isEqualTo(5150L); } @Test public void terminationGracePeriodFromGlobalProp() { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), Collections.emptyMap()); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setTerminationGracePeriodSeconds(6160L); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getTerminationGracePeriodSeconds()).isEqualTo(6160L); } @Test public void terminationGracePeriodNotSpecified() { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), Collections.emptyMap()); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getTerminationGracePeriodSeconds()).isNull(); } private Resource getResource() { return new DockerResource("springcloud/spring-cloud-deployer-spi-test-app:latest"); } private KubernetesDeployerProperties bindDeployerProperties() throws Exception { YamlPropertiesFactoryBean properties = new YamlPropertiesFactoryBean(); properties.setResources(new ClassPathResource("dataflow-server.yml"), new ClassPathResource("dataflow-server-tolerations.yml"), new ClassPathResource("dataflow-server-secretKeyRef.yml"), new ClassPathResource("dataflow-server-configMapKeyRef.yml"), new ClassPathResource("dataflow-server-podsecuritycontext.yml"), new ClassPathResource("dataflow-server-containerSecurityContext.yml"), new ClassPathResource("dataflow-server-nodeAffinity.yml"), new ClassPathResource("dataflow-server-podAffinity.yml"), new ClassPathResource("dataflow-server-podAntiAffinity.yml")); Properties yaml = properties.getObject(); MapConfigurationPropertySource source = new MapConfigurationPropertySource(yaml); return new Binder(source).bind("", Bindable.of(KubernetesDeployerProperties.class)).get(); } protected KubernetesAppDeployer k8sAppDeployer() throws Exception { return k8sAppDeployer(bindDeployerProperties()); } protected KubernetesAppDeployer k8sAppDeployer(KubernetesDeployerProperties kubernetesDeployerProperties) { return new KubernetesAppDeployer(kubernetesDeployerProperties, null); } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/test/java/org/springframework/cloud/deployer/spi/kubernetes/KubernetesConfigurationPropertiesTests.java ================================================ /* * Copyright 2018-2021 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.cloud.deployer.spi.kubernetes; import java.net.MalformedURLException; import java.net.URL; import io.fabric8.kubernetes.client.KubernetesClient; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** * @author Ilayaperumal Gopinathan * @author Chris Schaefer * @author Corneil du Plessis */ public class KubernetesConfigurationPropertiesTests { @Test public void testFabric8Namespacing() throws MalformedURLException { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.getFabric8().setTrustCerts(true); kubernetesDeployerProperties.getFabric8().setMasterUrl("http://localhost:8090"); // this can be set programatically in properties as well as an environment variable // (ie: CI, cmd line, etc) so ensure we have a clean slate here kubernetesDeployerProperties.setNamespace(null); kubernetesDeployerProperties.getFabric8().setNamespace("testing"); KubernetesClient kubernetesClient = KubernetesClientFactory .getKubernetesClient(kubernetesDeployerProperties); assertThat(kubernetesClient.getMasterUrl()).isEqualTo(new URL("http://localhost:8090/")); assertThat(kubernetesClient.getNamespace()).isEqualTo("testing"); assertThat(kubernetesClient.getConfiguration().getMasterUrl()).isEqualTo("http://localhost:8090/"); assertThat(kubernetesClient.getConfiguration().isTrustCerts()).isTrue(); } @Test public void testTopLevelNamespacing() throws MalformedURLException { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.getFabric8().setTrustCerts(true); kubernetesDeployerProperties.getFabric8().setMasterUrl("http://localhost:8090/"); kubernetesDeployerProperties.setNamespace("toplevel"); KubernetesClient kubernetesClient = KubernetesClientFactory .getKubernetesClient(kubernetesDeployerProperties); assertThat(kubernetesClient.getMasterUrl()).isEqualTo(new URL("http://localhost:8090/")); assertThat(kubernetesClient.getNamespace()).isEqualTo("toplevel"); assertThat(kubernetesClient.getConfiguration().getMasterUrl()).isEqualTo("http://localhost:8090/"); assertThat(kubernetesClient.getConfiguration().isTrustCerts()).isTrue(); } @Test public void testTopLevelNamespacingOverride() throws MalformedURLException { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.getFabric8().setTrustCerts(true); kubernetesDeployerProperties.getFabric8().setMasterUrl("http://localhost:8090/"); kubernetesDeployerProperties.getFabric8().setNamespace("toplevel"); kubernetesDeployerProperties.setNamespace("toplevel"); KubernetesClient kubernetesClient = KubernetesClientFactory .getKubernetesClient(kubernetesDeployerProperties); assertThat(kubernetesClient.getMasterUrl()).isEqualTo(new URL("http://localhost:8090/")); assertThat(kubernetesClient.getNamespace()).isEqualTo("toplevel"); assertThat(kubernetesClient.getConfiguration().getMasterUrl()).isEqualTo("http://localhost:8090/"); assertThat(kubernetesClient.getConfiguration().isTrustCerts()).isTrue(); } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/test/java/org/springframework/cloud/deployer/spi/kubernetes/KubernetesDeployerPropertiesTests.java ================================================ /* * Copyright 2021-2021 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.cloud.deployer.spi.kubernetes; import org.junit.jupiter.api.Test; import org.springframework.util.StringUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; /** * Tests for {@link KubernetesDeployerProperties}. * * @author Glenn Renfro */ public class KubernetesDeployerPropertiesTests { @Test public void testImagePullPolicyDefault() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); assertThat(kubernetesDeployerProperties.getImagePullPolicy()).as("Image pull policy should not be null").isNotNull(); assertEquals(ImagePullPolicy.IfNotPresent, kubernetesDeployerProperties.getImagePullPolicy(), "Invalid default image pull policy"); } @Test public void testImagePullPolicyCanBeCustomized() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setImagePullPolicy(ImagePullPolicy.Never); assertThat(kubernetesDeployerProperties.getImagePullPolicy()).as("Image pull policy should not be null").isNotNull(); assertEquals(ImagePullPolicy.Never, kubernetesDeployerProperties.getImagePullPolicy(), "Unexpected image pull policy"); } @Test public void testRestartPolicyDefault() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); assertThat(kubernetesDeployerProperties.getRestartPolicy()).as("Restart policy should not be null").isNotNull(); assertEquals(RestartPolicy.Always, kubernetesDeployerProperties.getRestartPolicy(), "Invalid default restart policy"); } @Test public void testRestartPolicyCanBeCustomized() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setRestartPolicy(RestartPolicy.OnFailure); assertThat(kubernetesDeployerProperties.getRestartPolicy()).as("Restart policy should not be null").isNotNull(); assertEquals(RestartPolicy.OnFailure, kubernetesDeployerProperties.getRestartPolicy(), "Unexpected restart policy"); } @Test public void testEntryPointStyleDefault() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); assertThat(kubernetesDeployerProperties.getEntryPointStyle()).as("Entry point style should not be null").isNotNull(); assertEquals(EntryPointStyle.exec, kubernetesDeployerProperties.getEntryPointStyle(), "Invalid default entry point style"); } @Test public void testEntryPointStyleCanBeCustomized() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setEntryPointStyle(EntryPointStyle.shell); assertThat(kubernetesDeployerProperties.getEntryPointStyle()).as("Entry point style should not be null").isNotNull(); assertEquals(EntryPointStyle.shell, kubernetesDeployerProperties.getEntryPointStyle(), "Unexpected entry point stype"); } @Test public void testNamespaceDefault() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); if (kubernetesDeployerProperties.getNamespace() == null) { kubernetesDeployerProperties.setNamespace("default"); assertThat(StringUtils.hasText(kubernetesDeployerProperties.getNamespace())).as("Namespace should not be empty or null").isTrue(); assertEquals("default", kubernetesDeployerProperties.getNamespace(), "Invalid default namespace"); } } @Test public void testNamespaceCanBeCustomized() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setNamespace("myns"); assertThat(StringUtils.hasText(kubernetesDeployerProperties.getNamespace())).as("Namespace should not be empty or null").isTrue(); assertEquals("myns", kubernetesDeployerProperties.getNamespace(), "Unexpected namespace"); } @Test public void testImagePullSecretDefault() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); assertThat(kubernetesDeployerProperties.getImagePullSecret()).as("No default image pull secret should be set").isNull(); } @Test public void testImagePullSecretCanBeCustomized() { String secret = "mysecret"; KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setImagePullSecret(secret); assertThat(kubernetesDeployerProperties.getImagePullSecret()).as("Image pull secret should not be null").isNotNull(); assertEquals(secret, kubernetesDeployerProperties.getImagePullSecret(), "Unexpected image pull secret"); } @Test public void testEnvironmentVariablesDefault() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); assertEquals(0, kubernetesDeployerProperties.getEnvironmentVariables().length, "No default environment variables should be set"); } @Test public void testEnvironmentVariablesCanBeCustomized() { String[] envVars = new String[] { "var1=val1", "var2=val2" }; KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setEnvironmentVariables(envVars); assertThat(kubernetesDeployerProperties.getEnvironmentVariables()).as("Environment variables should not be null").isNotNull(); assertEquals(2, kubernetesDeployerProperties.getEnvironmentVariables().length, "Unexpected number of environment variables"); } @Test public void testTaskServiceAccountNameDefault() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); assertThat(kubernetesDeployerProperties.getTaskServiceAccountName()).as("Task service account name should not be null").isNotNull(); assertEquals(kubernetesDeployerProperties.DEFAULT_TASK_SERVICE_ACCOUNT_NAME, kubernetesDeployerProperties.getTaskServiceAccountName(), "Unexpected default task service account name"); } @Test public void testTaskServiceAccountNameCanBeCustomized() { String taskServiceAccountName = "mysa"; KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setTaskServiceAccountName(taskServiceAccountName); assertThat(kubernetesDeployerProperties.getTaskServiceAccountName()).as("Task service account name should not be null").isNotNull(); assertEquals(taskServiceAccountName, kubernetesDeployerProperties.getTaskServiceAccountName(), "Unexpected task service account name"); } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/test/java/org/springframework/cloud/deployer/spi/kubernetes/KubernetesSchedulerIT.java ================================================ /* * Copyright 2018-2021 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.cloud.deployer.spi.kubernetes; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import io.fabric8.kubernetes.api.model.Container; import io.fabric8.kubernetes.api.model.EnvVar; import io.fabric8.kubernetes.api.model.LocalObjectReference; import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.PodSpec; import io.fabric8.kubernetes.api.model.PodTemplateSpec; import io.fabric8.kubernetes.api.model.Status; import io.fabric8.kubernetes.api.model.StatusCause; import io.fabric8.kubernetes.api.model.StatusDetails; import io.fabric8.kubernetes.api.model.batch.v1.CronJob; import io.fabric8.kubernetes.api.model.batch.v1.CronJobList; import io.fabric8.kubernetes.api.model.batch.v1.CronJobSpec; import io.fabric8.kubernetes.api.model.batch.v1.JobSpec; import io.fabric8.kubernetes.api.model.batch.v1.JobTemplateSpec; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientBuilder; import io.fabric8.kubernetes.client.KubernetesClientException; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.cloud.deployer.resource.docker.DockerResource; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.scheduler.CreateScheduleException; import org.springframework.cloud.deployer.spi.scheduler.ScheduleInfo; import org.springframework.cloud.deployer.spi.scheduler.ScheduleRequest; import org.springframework.cloud.deployer.spi.scheduler.Scheduler; import org.springframework.cloud.deployer.spi.scheduler.SchedulerPropertyKeys; import org.springframework.cloud.deployer.spi.scheduler.test.AbstractSchedulerIntegrationJUnit5Tests; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.Resource; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Tests for Kubernetes {@link Scheduler} implementation. * * @author Chris Schaefer * @author Ilayaperumal Gopinathan * @author Corneil du Plessis */ @ExtendWith(SpringExtension.class) @SpringBootTest(webEnvironment = WebEnvironment.NONE) @ContextConfiguration(classes = {KubernetesSchedulerIT.Config.class}) public class KubernetesSchedulerIT extends AbstractSchedulerIntegrationJUnit5Tests { private static final Logger LOGGER = LoggerFactory.getLogger(KubernetesSchedulerIT.class); @Autowired private Scheduler scheduler; @Autowired private KubernetesClient kubernetesClient; @Override protected Scheduler provideScheduler() { return this.scheduler; } @Override protected List getCommandLineArgs() { List commandLineArguments = new ArrayList<>(); commandLineArguments.add("arg1=value1"); commandLineArguments.add("arg2=value2"); return commandLineArguments; } @Override protected Map getSchedulerProperties() { return Collections.singletonMap(SchedulerPropertyKeys.CRON_EXPRESSION, "57 13 ? * *"); } private Map getSchedulerProperties(String concurrencyPolicy) { Map schedulerProperties = new HashMap<>(getSchedulerProperties()); schedulerProperties.put(KubernetesScheduler.KUBERNETES_DEPLOYER_CRON_CONCURRENCY_POLICY, concurrencyPolicy); return schedulerProperties; } @Override protected Map getDeploymentProperties() { return Collections.singletonMap(SchedulerPropertyKeys.CRON_EXPRESSION, "57 13 ? * *"); } @Override protected Map getAppProperties() { Map applicationProperties = new HashMap<>(); applicationProperties.put("prop.1.key", "prop.1.value"); applicationProperties.put("prop.2.key", "prop.2.value"); return applicationProperties; } @Override // schedule name must match "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" and size must be between 0 // and 63 protected String randomName() { return UUID.randomUUID().toString().substring(0, 18); } @Override // schedule name must match "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" and size must be between 0 // and 63 protected String scheduleName() { return "schedulename-"; } protected Resource testApplication() { return new DockerResource("springcloud/spring-cloud-deployer-spi-scheduler-test-app:latest"); } @Test public void test() { super.testListFilter(); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testMissingSchedule(boolean isDeprecated) { AppDefinition appDefinition = new AppDefinition(randomName(), null); ScheduleRequest scheduleRequest = (isDeprecated) ? new ScheduleRequest(appDefinition, null, null, null, null, testApplication()) : new ScheduleRequest(appDefinition, null, (List) null, null, testApplication()); assertThatThrownBy(() -> { scheduler.schedule(scheduleRequest); }).isInstanceOf(CreateScheduleException.class); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testInvalidNameSchedule(boolean isDeprecated) { AppDefinition appDefinition = new AppDefinition("AAAAAA", null); ScheduleRequest scheduleRequest = (isDeprecated) ? new ScheduleRequest(appDefinition, null, null, null, "AAAAA", testApplication()) : new ScheduleRequest(appDefinition, null, (List) null, "AAAAA", testApplication()); assertThatThrownBy(() -> { scheduler.schedule(scheduleRequest); }).isInstanceOf(CreateScheduleException.class); } @Test public void testSchedulerPropertiesMerge() { final String baseScheduleName = "test-schedule1"; Map schedulerProperties = new HashMap<>(); schedulerProperties.put(SchedulerPropertyKeys.CRON_EXPRESSION, "0/10 * * * *"); schedulerProperties.put(KubernetesSchedulerProperties.KUBERNETES_SCHEDULER_PROPERTIES_PREFIX + ".imagePullPolicy", "Never"); Map deploymentProperties = new HashMap<>(); deploymentProperties.put(KubernetesDeployerProperties.KUBERNETES_DEPLOYER_PROPERTIES_PREFIX + ".environmentVariables", "MYVAR1=MYVAL1,MYVAR2=MYVAL2"); deploymentProperties.put(KubernetesDeployerProperties.KUBERNETES_DEPLOYER_PROPERTIES_PREFIX + ".imagePullPolicy", "Always"); AppDefinition appDefinition = new AppDefinition(randomName(), null); ScheduleRequest scheduleRequest = new ScheduleRequest(appDefinition, schedulerProperties, deploymentProperties, null, baseScheduleName, testApplication()); Map mergedProperties = KubernetesScheduler.mergeSchedulerProperties(scheduleRequest); assertThat(mergedProperties .get(KubernetesDeployerProperties.KUBERNETES_DEPLOYER_PROPERTIES_PREFIX + ".imagePullPolicy")) .as("Expected value from Scheduler properties, but found in Deployer properties") .isEqualTo("Never"); assertThat(mergedProperties .get(KubernetesDeployerProperties.KUBERNETES_DEPLOYER_PROPERTIES_PREFIX + ".environmentVariables")) .as("Deployer property is expected to be merged as scheduler property") .isEqualTo("MYVAR1=MYVAL1,MYVAR2=MYVAL2"); } @Test public void listScheduleWithExternalCronJobs() { CronJobList cronJobList = new CronJobList(); CronJobSpec cronJobSpec = new CronJobSpec(); JobTemplateSpec jobTemplateSpec = new JobTemplateSpec(); JobSpec jobSpec = new JobSpec(); PodTemplateSpec podTemplateSpec = new PodTemplateSpec(); PodSpec podSpec = new PodSpec(); Container container = new Container(); container.setName("test"); container.setImage("busybox"); podSpec.setContainers(List.of(container)); podSpec.setRestartPolicy("OnFailure"); podTemplateSpec.setSpec(podSpec); jobSpec.setTemplate(podTemplateSpec); jobTemplateSpec.setSpec(jobSpec); cronJobSpec.setJobTemplate(jobTemplateSpec); cronJobSpec.setSchedule("0/10 * * * *"); CronJob cronJob1 = new CronJob(); ObjectMeta objectMeta1 = new ObjectMeta(); Map labels = new HashMap<>(); labels.put("spring-cronjob-id", "test"); objectMeta1.setLabels(labels); objectMeta1.setName("job1"); cronJob1.setMetadata(objectMeta1); cronJob1.setSpec(cronJobSpec); ObjectMeta objectMeta2 = new ObjectMeta(); objectMeta2.setName("job2"); CronJob cronJob2 = new CronJob(); cronJob2.setSpec(cronJobSpec); cronJob2.setMetadata(objectMeta2); ObjectMeta objectMeta3 = new ObjectMeta(); objectMeta3.setName("job3"); CronJob cronJob3 = new CronJob(); cronJob3.setSpec(cronJobSpec); cronJob3.setMetadata(objectMeta3); cronJobList.setItems(Arrays.asList(cronJob1, cronJob2, cronJob3)); this.kubernetesClient.batch().v1().cronjobs().inNamespace("default").resource(cronJob1).create(); this.kubernetesClient.batch().v1().cronjobs().inNamespace("default").resource(cronJob2).create(); this.kubernetesClient.batch().v1().cronjobs().inNamespace("default").resource(cronJob3).create(); List scheduleInfos = this.scheduler.list(); assertThat(scheduleInfos.size()).isEqualTo(1); assertThat(scheduleInfos.get(0).getScheduleName()).isEqualTo("job1"); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testInvalidCronSyntax(boolean isDeprecated) { Map schedulerProperties = Collections.singletonMap(SchedulerPropertyKeys.CRON_EXPRESSION, "1 2 3 4"); AppDefinition appDefinition = new AppDefinition(randomName(), null); ScheduleRequest scheduleRequest = (isDeprecated) ? new ScheduleRequest(appDefinition, schedulerProperties, null, null, randomName(), testApplication()) : new ScheduleRequest(appDefinition, schedulerProperties, (List) null, randomName(), testApplication()); assertThatThrownBy(() -> { scheduler.schedule(scheduleRequest); }).isInstanceOf(CreateScheduleException.class); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testNameTooLong(boolean isDeprecated) { final String baseScheduleName = (isDeprecated) ? "tencharlng-scdf-itcouldbesaidthatthisislongtoowayold" : "tencharlng-scdf-itcouldbesaidthatthisislongtoowaytoo"; Map schedulerProperties = Collections.singletonMap(SchedulerPropertyKeys.CRON_EXPRESSION, "0/10 * * * *"); AppDefinition appDefinition = new AppDefinition(randomName(), null); ScheduleRequest scheduleRequest = (isDeprecated) ? new ScheduleRequest(appDefinition, schedulerProperties, null, null, baseScheduleName, testApplication()) : new ScheduleRequest(appDefinition, schedulerProperties, (List) null, baseScheduleName, testApplication()); //verify no validation fired. scheduler.schedule(scheduleRequest); ScheduleRequest scheduleRequest2 = (isDeprecated) ? new ScheduleRequest(appDefinition, schedulerProperties, null, null, baseScheduleName + "1", testApplication()) : new ScheduleRequest(appDefinition, schedulerProperties, (List) null, baseScheduleName + "1", testApplication()); assertThatThrownBy(() -> { scheduler.schedule(scheduleRequest2); }).isInstanceOf(CreateScheduleException.class) .hasMessage(String.format("Failed to create schedule because Schedule Name: '%s' has too many characters. Schedule name length must be 52 characters or less", baseScheduleName + "1")); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testWithExecEntryPoint(boolean isDeprecated) { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); if (kubernetesDeployerProperties.getNamespace() == null) { kubernetesDeployerProperties.setNamespace("default"); } kubernetesDeployerProperties.setEntryPointStyle(EntryPointStyle.exec); KubernetesClient kubernetesClient = (new KubernetesClientBuilder()).build(); KubernetesScheduler kubernetesScheduler = new KubernetesScheduler(kubernetesClient, kubernetesDeployerProperties); AppDefinition appDefinition = new AppDefinition(randomName(), getAppProperties()); ScheduleRequest scheduleRequest = (isDeprecated) ? new ScheduleRequest(appDefinition, getSchedulerProperties(), null, getCommandLineArgs(), randomName(), testApplication()) : new ScheduleRequest(appDefinition, getDeploymentProperties(), getCommandLineArgs(), randomName(), testApplication()); CronJob cronJob = kubernetesScheduler.createCronJob(scheduleRequest); CronJobSpec cronJobSpec = cronJob.getSpec(); Container container = cronJobSpec.getJobTemplate().getSpec().getTemplate().getSpec().getContainers().get(0); assertThat(container.getArgs()).as("Command line arguments should not be null").isNotNull(); assertThat(container.getEnv()).as("Environment variables should not be null").isNotNull(); assertThat(container.getEnv()).as("Environment variables should only have SPRING_CLOUD_APPLICATION_GUID") .hasSize(1); safeUnschedule(kubernetesScheduler, cronJob.getMetadata().getName()); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testWithShellEntryPoint(boolean isDeprecated) { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); if (kubernetesDeployerProperties.getNamespace() == null) { kubernetesDeployerProperties.setNamespace("default"); } kubernetesDeployerProperties.setEntryPointStyle(EntryPointStyle.shell); KubernetesClient kubernetesClient = (new KubernetesClientBuilder()).build(); KubernetesScheduler kubernetesScheduler = new KubernetesScheduler(kubernetesClient, kubernetesDeployerProperties); AppDefinition appDefinition = new AppDefinition(randomName(), getAppProperties()); ScheduleRequest scheduleRequest = (isDeprecated) ? new ScheduleRequest(appDefinition, getSchedulerProperties(), null, getCommandLineArgs(), randomName(), testApplication()) : new ScheduleRequest(appDefinition, getSchedulerProperties(), getCommandLineArgs(), randomName(), testApplication()); CronJob cronJob = kubernetesScheduler.createCronJob(scheduleRequest); CronJobSpec cronJobSpec = cronJob.getSpec(); Container container = cronJobSpec.getJobTemplate().getSpec().getTemplate().getSpec().getContainers().get(0); assertThat(container.getArgs()).as("Command line arguments should not be null").isNotNull(); assertThat(container.getArgs()).as("Invalid number of command line arguments").isEmpty(); assertThat(container.getEnv()).as("Environment variables should not be null").isNotNull(); assertThat(container.getEnv()).as("Invalid number of environment variables").hasSizeGreaterThan(1); safeUnschedule(kubernetesScheduler, cronJob.getMetadata().getName()); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testWithBootEntryPoint(boolean isDeprecated) throws IOException { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setEntryPointStyle(EntryPointStyle.boot); if (kubernetesDeployerProperties.getNamespace() == null) { kubernetesDeployerProperties.setNamespace("default"); } KubernetesClient kubernetesClient = (new KubernetesClientBuilder()).build(); KubernetesScheduler kubernetesScheduler = new KubernetesScheduler(kubernetesClient, kubernetesDeployerProperties); AppDefinition appDefinition = new AppDefinition(randomName(), getAppProperties()); ScheduleRequest scheduleRequest = (isDeprecated) ? new ScheduleRequest(appDefinition, getSchedulerProperties(), null, getCommandLineArgs(), randomName(), testApplication()) : new ScheduleRequest(appDefinition, getSchedulerProperties(), getCommandLineArgs(), randomName(), testApplication()); CronJob cronJob = kubernetesScheduler.createCronJob(scheduleRequest); CronJobSpec cronJobSpec = cronJob.getSpec(); Container container = cronJobSpec.getJobTemplate().getSpec().getTemplate().getSpec().getContainers().get(0); assertThat(container.getArgs()).as("Command line arguments should not be null").isNotNull(); assertThat(container.getArgs()).as("Invalid number of command line arguments").hasSize(2); assertThat(container.getEnv()).as("Environment variables should not be null").isNotNull(); assertThat(container.getEnv()).as("Invalid number of environment variables").hasSizeGreaterThan(1); String springApplicationJson = container.getEnv().get(0).getValue(); Map springApplicationJsonValues = new ObjectMapper().readValue(springApplicationJson, new TypeReference>() { }); assertThat(springApplicationJsonValues).as("SPRING_APPLICATION_JSON should not be null").isNotNull(); assertThat(springApplicationJsonValues).as("Invalid number of SPRING_APPLICATION_JSON entries").hasSize(2); safeUnschedule(kubernetesScheduler, cronJob.getMetadata().getName()); } @Test public void testGetExceptionMessageForExistingField() { StatusCause statusCause = new StatusCause("spec.schedule", null, null); StatusDetails statusDetails = new StatusDetails(); statusDetails.setCauses(Collections.singletonList(statusCause)); Status status = new Status(); status.setCode(0); status.setMessage("invalid cron expression"); status.setDetails(statusDetails); KubernetesClientException kubernetesClientException = new KubernetesClientException(status); String message = ((KubernetesScheduler) scheduler).getExceptionMessageForField(kubernetesClientException, "spec.schedule"); assertThat(message).as("Field message should not be null").isNotNull(); assertThat(message).as("Invalid message for field").isEqualTo("invalid cron expression"); } @Test public void testGetExceptionMessageForNonExistentField() { StatusCause statusCause = new StatusCause("spec.schedule", null, null); StatusDetails statusDetails = new StatusDetails(); statusDetails.setCauses(Collections.singletonList(statusCause)); Status status = new Status(); status.setCode(0); status.setMessage("invalid cron expression"); status.setDetails(statusDetails); KubernetesClientException kubernetesClientException = new KubernetesClientException(status); String message = ((KubernetesScheduler) scheduler).getExceptionMessageForField(kubernetesClientException, "spec.restartpolicy"); assertThat(message).as("Field message should be null").isNull(); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testEntryPointStyleOverride(boolean isDeprecated) throws Exception { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); if (kubernetesDeployerProperties.getNamespace() == null) { kubernetesDeployerProperties.setNamespace("default"); } KubernetesClient kubernetesClient = (new KubernetesClientBuilder()).build(); KubernetesScheduler kubernetesScheduler = new KubernetesScheduler(kubernetesClient, kubernetesDeployerProperties); String prefix = KubernetesSchedulerProperties.KUBERNETES_SCHEDULER_PROPERTIES_PREFIX; Map schedulerProperties = new HashMap<>(getSchedulerProperties()); schedulerProperties.put(prefix + ".entryPointStyle", "boot"); AppDefinition appDefinition = new AppDefinition(randomName(), getAppProperties()); ScheduleRequest scheduleRequest = (isDeprecated) ? new ScheduleRequest(appDefinition, schedulerProperties, null, getCommandLineArgs(), randomName(), testApplication()) : new ScheduleRequest(appDefinition, schedulerProperties, getCommandLineArgs(), randomName(), testApplication()); CronJob cronJob = kubernetesScheduler.createCronJob(scheduleRequest); CronJobSpec cronJobSpec = cronJob.getSpec(); Container container = cronJobSpec.getJobTemplate().getSpec().getTemplate().getSpec().getContainers().get(0); assertThat(container.getEnv()).as("Invalid number of environment variables").hasSizeGreaterThan(1); String springApplicationJson = container.getEnv().get(0).getValue(); Map springApplicationJsonValues = new ObjectMapper().readValue(springApplicationJson, new TypeReference>() { }); assertThat(springApplicationJsonValues).as("SPRING_APPLICATION_JSON should not be null").isNotNull(); assertThat(springApplicationJsonValues).as("Invalid number of SPRING_APPLICATION_JSON entries").hasSize(2); safeUnschedule(kubernetesScheduler, cronJob.getMetadata().getName()); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testEntryPointStyleDefault(boolean isDeprecated) { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); if (kubernetesDeployerProperties.getNamespace() == null) { kubernetesDeployerProperties.setNamespace("default"); } KubernetesClient kubernetesClient = (new KubernetesClientBuilder()).build(); KubernetesScheduler kubernetesScheduler = new KubernetesScheduler(kubernetesClient, kubernetesDeployerProperties); AppDefinition appDefinition = new AppDefinition(randomName(), getAppProperties()); ScheduleRequest scheduleRequest = (isDeprecated) ? new ScheduleRequest(appDefinition, getSchedulerProperties(), Collections.emptyMap(), getCommandLineArgs(), randomName(), testApplication()) : new ScheduleRequest(appDefinition, getDeploymentProperties(), getCommandLineArgs(), randomName(), testApplication()); CronJob cronJob = kubernetesScheduler.createCronJob(scheduleRequest); CronJobSpec cronJobSpec = cronJob.getSpec(); Container container = cronJobSpec.getJobTemplate().getSpec().getTemplate().getSpec().getContainers().get(0); assertThat(container.getEnv()).as("Environment variables should only have SPRING_CLOUD_APPLICATION_GUID") .hasSize(1); assertThat(container.getArgs()).as("Command line arguments should not be empty").isNotEmpty(); safeUnschedule(kubernetesScheduler, cronJob.getMetadata().getName()); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testImagePullPolicyOverride(boolean isDeprecated) { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); if (kubernetesDeployerProperties.getNamespace() == null) { kubernetesDeployerProperties.setNamespace("default"); } KubernetesClient kubernetesClient = (new KubernetesClientBuilder()).build(); KubernetesScheduler kubernetesScheduler = new KubernetesScheduler(kubernetesClient, kubernetesDeployerProperties); String prefix = KubernetesSchedulerProperties.KUBERNETES_SCHEDULER_PROPERTIES_PREFIX; Map schedulerProperties = new HashMap<>(getSchedulerProperties()); schedulerProperties.put(prefix + ".imagePullPolicy", "Always"); AppDefinition appDefinition = new AppDefinition(randomName(), getAppProperties()); ScheduleRequest scheduleRequest = (isDeprecated) ? new ScheduleRequest(appDefinition, schedulerProperties, null, getCommandLineArgs(), randomName(), testApplication()) : new ScheduleRequest(appDefinition, schedulerProperties, getCommandLineArgs(), randomName(), testApplication()); CronJob cronJob = kubernetesScheduler.createCronJob(scheduleRequest); CronJobSpec cronJobSpec = cronJob.getSpec(); Container container = cronJobSpec.getJobTemplate().getSpec().getTemplate().getSpec().getContainers().get(0); assertThat(container.getImagePullPolicy()).as("Unexpected image pull policy").isEqualTo("Always"); safeUnschedule(kubernetesScheduler, cronJob.getMetadata().getName()); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testJobAnnotationsAndLabelsFromSchedulerProperties(boolean isDeprecated) { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setJobAnnotations("test1:value1"); kubernetesDeployerProperties.setPodAnnotations("podtest1:podvalue1"); kubernetesDeployerProperties.setDeploymentLabels("label1:value1,label2:value2"); if (kubernetesDeployerProperties.getNamespace() == null) { kubernetesDeployerProperties.setNamespace("default"); } KubernetesClient kubernetesClient = (new KubernetesClientBuilder()).build(); KubernetesScheduler kubernetesScheduler = new KubernetesScheduler(kubernetesClient, kubernetesDeployerProperties); AppDefinition appDefinition = new AppDefinition(randomName(), getAppProperties()); ScheduleRequest scheduleRequest = (isDeprecated) ? new ScheduleRequest(appDefinition, getSchedulerProperties(), null, getCommandLineArgs(), randomName(), testApplication()) : new ScheduleRequest(appDefinition, getSchedulerProperties(), getCommandLineArgs(), randomName(), testApplication()); CronJob cronJob = kubernetesScheduler.createCronJob(scheduleRequest); assertThat(cronJob.getMetadata().getAnnotations().get("test1")).as("Job annotation is not set") .isEqualTo("value1"); assertThat(cronJob.getSpec().getJobTemplate().getSpec().getTemplate().getMetadata().getAnnotations() .get("podtest1")).as("Pod annotation is not set").isEqualTo("podvalue1"); assertThat(cronJob.getSpec().getJobTemplate().getSpec().getTemplate().getMetadata().getLabels() .get("label1")).as("Pod Label1 is not set").isEqualTo("value1"); assertThat(cronJob.getSpec().getJobTemplate().getSpec().getTemplate().getMetadata().getLabels() .get("label2")).as("Pod Label2 is not set").isEqualTo("value2"); safeUnschedule(kubernetesScheduler, cronJob.getMetadata().getName()); } @Test public void testDefaultLabel() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); if (kubernetesDeployerProperties.getNamespace() == null) { kubernetesDeployerProperties.setNamespace("default"); } KubernetesClient kubernetesClient = (new KubernetesClientBuilder()).build(); KubernetesScheduler kubernetesScheduler = new KubernetesScheduler(kubernetesClient, kubernetesDeployerProperties); AppDefinition appDefinition = new AppDefinition(randomName(), getAppProperties()); ScheduleRequest scheduleRequest = new ScheduleRequest(appDefinition, getSchedulerProperties(), null, getCommandLineArgs(), randomName(), testApplication()); CronJob cronJob = kubernetesScheduler.createCronJob(scheduleRequest); assertThat(cronJob.getSpec().getJobTemplate().getSpec().getTemplate().getMetadata().getAnnotations()).isEmpty(); assertThat(cronJob.getSpec().getJobTemplate().getSpec().getTemplate().getMetadata().getLabels() .size()).as("Should have one label").isEqualTo(1); assertThat(cronJob.getSpec().getJobTemplate().getSpec().getTemplate().getMetadata().getLabels().get( KubernetesScheduler.SPRING_CRONJOB_ID_KEY)).as("Default label is not set").isNotNull(); safeUnschedule(kubernetesScheduler, cronJob.getMetadata().getName()); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testJobAnnotationsAndLabelsFromSchedulerRequest(boolean isDeprecated) { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setJobAnnotations("test1:value1"); kubernetesDeployerProperties.setPodAnnotations("podtest1:podvalue1"); kubernetesDeployerProperties.setDeploymentLabels("label1:value1,label2:value2"); if (kubernetesDeployerProperties.getNamespace() == null) { kubernetesDeployerProperties.setNamespace("default"); } KubernetesClient kubernetesClient = (new KubernetesClientBuilder()).build(); KubernetesScheduler kubernetesScheduler = new KubernetesScheduler(kubernetesClient, kubernetesDeployerProperties); AppDefinition appDefinition = new AppDefinition(randomName(), getAppProperties()); Map scheduleProperties = new HashMap<>(); scheduleProperties.putAll(getSchedulerProperties()); if (isDeprecated) { scheduleProperties.put("spring.cloud.scheduler.kubernetes.deploymentLabels", "requestLabel1:requestValue1,requestLabel2:requestValue2"); scheduleProperties.put("spring.cloud.scheduler.kubernetes.podAnnotations", "requestPod1:requestPodValue1"); } else { scheduleProperties.put("spring.cloud.deployer.kubernetes.deploymentLabels", "requestLabel1:requestValue1,requestLabel2:requestValue2"); scheduleProperties.put("spring.cloud.deployer.kubernetes.podAnnotations", "requestPod1:requestPodValue1"); } ScheduleRequest scheduleRequest = (isDeprecated) ? new ScheduleRequest(appDefinition, scheduleProperties, null, getCommandLineArgs(), randomName(), testApplication()) : new ScheduleRequest(appDefinition, scheduleProperties, getCommandLineArgs(), randomName(), testApplication()); CronJob cronJob = kubernetesScheduler.createCronJob(scheduleRequest); assertThat(cronJob.getMetadata().getAnnotations().get("test1")).as("Job annotation is not set") .isEqualTo("value1"); // Pod annotation from the request should override the top level property values assertThat(cronJob.getSpec().getJobTemplate().getSpec().getTemplate().getMetadata().getAnnotations() .get("requestPod1")).as("Pod annotation is not set").isEqualTo("requestPodValue1"); // Deployment label from the request should get appended to the top level property values assertThat(cronJob.getSpec().getJobTemplate().getSpec().getTemplate().getMetadata().getLabels() .get("requestLabel1")).as("Pod Label1 from the request is not set").isEqualTo("requestValue1"); assertThat(cronJob.getSpec().getJobTemplate().getSpec().getTemplate().getMetadata().getLabels() .get("requestLabel2")).as("Pod Label2 from the request is not set").isEqualTo("requestValue2"); assertThat(cronJob.getSpec().getJobTemplate().getSpec().getTemplate().getMetadata().getLabels() .get("label1")).as("Pod Label1 is not set").isEqualTo("value1"); assertThat(cronJob.getSpec().getJobTemplate().getSpec().getTemplate().getMetadata().getLabels() .get("label2")).as("Pod Label2 is not set").isEqualTo("value2"); safeUnschedule(kubernetesScheduler, cronJob.getMetadata().getName()); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testJobAnnotationsOverride(boolean isDeprecated) { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setJobAnnotations("test1:value1"); if (kubernetesDeployerProperties.getNamespace() == null) { kubernetesDeployerProperties.setNamespace("default"); } KubernetesClient kubernetesClient = (new KubernetesClientBuilder()).build(); KubernetesScheduler kubernetesScheduler = new KubernetesScheduler(kubernetesClient, kubernetesDeployerProperties); AppDefinition appDefinition = new AppDefinition(randomName(), getAppProperties()); String prefix = KubernetesSchedulerProperties.KUBERNETES_SCHEDULER_PROPERTIES_PREFIX; Map schedulerProperties = new HashMap<>(getSchedulerProperties()); schedulerProperties.put(prefix + ".jobAnnotations", "test1:value2"); ScheduleRequest scheduleRequest = (isDeprecated) ? new ScheduleRequest(appDefinition, schedulerProperties, null, getCommandLineArgs(), randomName(), testApplication()) : new ScheduleRequest(appDefinition, schedulerProperties, getCommandLineArgs(), randomName(), testApplication()); CronJob cronJob = kubernetesScheduler.createCronJob(scheduleRequest); assertThat(cronJob.getMetadata().getAnnotations().get("test1")).as("Job annotation is not set") .isEqualTo("value2"); safeUnschedule(kubernetesScheduler, cronJob.getMetadata().getName()); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testImagePullPolicyDefault(boolean isDeprecated) { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); if (kubernetesDeployerProperties.getNamespace() == null) { kubernetesDeployerProperties.setNamespace("default"); } KubernetesClient kubernetesClient = (new KubernetesClientBuilder()).build(); KubernetesScheduler kubernetesScheduler = new KubernetesScheduler(kubernetesClient, kubernetesDeployerProperties); AppDefinition appDefinition = new AppDefinition(randomName(), getAppProperties()); ScheduleRequest scheduleRequest = (isDeprecated) ? new ScheduleRequest(appDefinition, getSchedulerProperties(), Collections.emptyMap(), getCommandLineArgs(), randomName(), testApplication()) : new ScheduleRequest(appDefinition, getDeploymentProperties(), getCommandLineArgs(), randomName(), testApplication()); CronJob cronJob = kubernetesScheduler.createCronJob(scheduleRequest); CronJobSpec cronJobSpec = cronJob.getSpec(); Container container = cronJobSpec.getJobTemplate().getSpec().getTemplate().getSpec().getContainers().get(0); assertThat(ImagePullPolicy.relaxedValueOf(container.getImagePullPolicy())).as("Unexpected default image pull policy") .isEqualTo(ImagePullPolicy.IfNotPresent); safeUnschedule(kubernetesScheduler, cronJob.getMetadata().getName()); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testImagePullSecret(boolean isDeprecated) { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); if (kubernetesDeployerProperties.getNamespace() == null) { kubernetesDeployerProperties.setNamespace("default"); } KubernetesClient kubernetesClient = (new KubernetesClientBuilder()).build(); KubernetesScheduler kubernetesScheduler = new KubernetesScheduler(kubernetesClient, kubernetesDeployerProperties); String secretName = "mysecret"; String prefix = KubernetesSchedulerProperties.KUBERNETES_SCHEDULER_PROPERTIES_PREFIX; Map schedulerProperties = new HashMap<>(getSchedulerProperties()); schedulerProperties.put(prefix + ".imagePullSecret", secretName); AppDefinition appDefinition = new AppDefinition(randomName(), getAppProperties()); ScheduleRequest scheduleRequest = (isDeprecated) ? new ScheduleRequest(appDefinition, schedulerProperties, null, getCommandLineArgs(), randomName(), testApplication()) : new ScheduleRequest(appDefinition, schedulerProperties, getCommandLineArgs(), randomName(), testApplication()); CronJob cronJob = kubernetesScheduler.createCronJob(scheduleRequest); CronJobSpec cronJobSpec = cronJob.getSpec(); List secrets = cronJobSpec.getJobTemplate().getSpec().getTemplate().getSpec() .getImagePullSecrets(); assertThat(secrets.get(0).getName()).as("Unexpected image pull secret").isEqualTo(secretName); safeUnschedule(kubernetesScheduler, cronJob.getMetadata().getName()); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testImagePullSecretDefault(boolean isDeprecated) { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); if (kubernetesDeployerProperties.getNamespace() == null) { kubernetesDeployerProperties.setNamespace("default"); } KubernetesClient kubernetesClient = (new KubernetesClientBuilder()).build(); KubernetesScheduler kubernetesScheduler = new KubernetesScheduler(kubernetesClient, kubernetesDeployerProperties); AppDefinition appDefinition = new AppDefinition(randomName(), getAppProperties()); ScheduleRequest scheduleRequest = (isDeprecated) ? new ScheduleRequest(appDefinition, getSchedulerProperties(), Collections.emptyMap(), getCommandLineArgs(), randomName(), testApplication()) : new ScheduleRequest(appDefinition, getDeploymentProperties(), getCommandLineArgs(), randomName(), testApplication()); CronJob cronJob = kubernetesScheduler.createCronJob(scheduleRequest); CronJobSpec cronJobSpec = cronJob.getSpec(); List secrets = cronJobSpec.getJobTemplate().getSpec().getTemplate().getSpec() .getImagePullSecrets(); assertThat(secrets).as("There should be no secrets").isEmpty(); safeUnschedule(kubernetesScheduler, cronJob.getMetadata().getName()); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testImagePullSecretFromSchedulerProperties(boolean isDeprecated) { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); if (kubernetesDeployerProperties.getNamespace() == null) { kubernetesDeployerProperties.setNamespace("default"); } String secretName = "image-secret"; kubernetesDeployerProperties.setImagePullSecret(secretName); KubernetesClient kubernetesClient = (new KubernetesClientBuilder()).build(); KubernetesScheduler kubernetesScheduler = new KubernetesScheduler(kubernetesClient, kubernetesDeployerProperties); AppDefinition appDefinition = new AppDefinition(randomName(), getAppProperties()); ScheduleRequest scheduleRequest = (isDeprecated) ? new ScheduleRequest(appDefinition, getSchedulerProperties(), Collections.emptyMap(), getCommandLineArgs(), randomName(), testApplication()) : new ScheduleRequest(appDefinition, getDeploymentProperties(), getCommandLineArgs(), randomName(), testApplication()); CronJob cronJob = kubernetesScheduler.createCronJob(scheduleRequest); CronJobSpec cronJobSpec = cronJob.getSpec(); List secrets = cronJobSpec.getJobTemplate().getSpec().getTemplate().getSpec() .getImagePullSecrets(); assertThat(secrets.get(0).getName()).as("Unexpected image pull secret").isEqualTo(secretName); safeUnschedule(kubernetesScheduler, cronJob.getMetadata().getName()); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testCustomEnvironmentVariables(boolean isDeprecated) { String prefix = (isDeprecated) ? KubernetesSchedulerProperties.KUBERNETES_SCHEDULER_PROPERTIES_PREFIX : KubernetesDeployerProperties.KUBERNETES_DEPLOYER_PROPERTIES_PREFIX; Map schedulerProperties = new HashMap<>(getSchedulerProperties()); schedulerProperties.put(prefix + ".environmentVariables", "MYVAR1=MYVAL1,MYVAR2=MYVAL2"); EnvVar[] expectedVars = new EnvVar[] {new EnvVar("MYVAR1", "MYVAL1", null), new EnvVar("MYVAR2", "MYVAL2", null)}; testEnvironmentVariables(new KubernetesDeployerProperties(), schedulerProperties, expectedVars, isDeprecated); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testGlobalEnvironmentVariables(boolean isDeprecated) { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); if (kubernetesDeployerProperties.getNamespace() == null) { kubernetesDeployerProperties.setNamespace("default"); } kubernetesDeployerProperties.setEnvironmentVariables(new String[] {"MYVAR1=MYVAL1", "MYVAR2=MYVAL2"}); EnvVar[] expectedVars = new EnvVar[] {new EnvVar("MYVAR1", "MYVAL1", null), new EnvVar("MYVAR2", "MYVAL2", null)}; testEnvironmentVariables(kubernetesDeployerProperties, getSchedulerProperties(), expectedVars, isDeprecated); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testCustomEnvironmentVariablesWithNestedComma(boolean isDeprecated) { String prefix = (isDeprecated) ? KubernetesSchedulerProperties.KUBERNETES_SCHEDULER_PROPERTIES_PREFIX : KubernetesDeployerProperties.KUBERNETES_DEPLOYER_PROPERTIES_PREFIX; Map schedulerProperties = new HashMap<>(getSchedulerProperties()); schedulerProperties.put(prefix + ".environmentVariables", "MYVAR='VAL1,VAL2',MYVAR2=MYVAL2"); EnvVar[] expectedVars = new EnvVar[] {new EnvVar("MYVAR", "VAL1,VAL2", null), new EnvVar("MYVAR2", "MYVAL2", null)}; testEnvironmentVariables((isDeprecated) ? new KubernetesSchedulerProperties() : new KubernetesDeployerProperties(), schedulerProperties, expectedVars, isDeprecated); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testGlobalAndCustomEnvironmentVariables(boolean isDeprecated) { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); if (kubernetesDeployerProperties.getNamespace() == null) { kubernetesDeployerProperties.setNamespace("default"); } kubernetesDeployerProperties.setEnvironmentVariables(new String[] {"MYVAR1=MYVAL1", "MYVAR2=MYVAL2"}); String prefix = (isDeprecated) ? KubernetesSchedulerProperties.KUBERNETES_SCHEDULER_PROPERTIES_PREFIX : KubernetesDeployerProperties.KUBERNETES_DEPLOYER_PROPERTIES_PREFIX; Map schedulerProperties = new HashMap<>(getSchedulerProperties()); schedulerProperties.put(prefix + ".environmentVariables", "MYVAR3=MYVAL3,MYVAR4=MYVAL4"); EnvVar[] expectedVars = new EnvVar[] {new EnvVar("MYVAR1", "MYVAL1", null), new EnvVar("MYVAR2", "MYVAL2", null), new EnvVar("MYVAR3", "MYVAL3", null), new EnvVar("MYVAR4", "MYVAL4", null)}; testEnvironmentVariables(kubernetesDeployerProperties, schedulerProperties, expectedVars, isDeprecated); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testCustomEnvironmentVariablesOverrideGlobal(boolean isDeprecated) { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); if (kubernetesDeployerProperties.getNamespace() == null) { kubernetesDeployerProperties.setNamespace("default"); } kubernetesDeployerProperties.setEnvironmentVariables(new String[] {"MYVAR1=MYVAL1", "MYVAR2=MYVAL2"}); String prefix = (isDeprecated) ? KubernetesSchedulerProperties.KUBERNETES_SCHEDULER_PROPERTIES_PREFIX : KubernetesDeployerProperties.KUBERNETES_DEPLOYER_PROPERTIES_PREFIX; Map schedulerProperties = new HashMap<>(getSchedulerProperties()); schedulerProperties.put(prefix + ".environmentVariables", "MYVAR2=OVERRIDE"); EnvVar[] expectedVars = new EnvVar[] {new EnvVar("MYVAR1", "MYVAL1", null), new EnvVar("MYVAR2", "OVERRIDE", null)}; testEnvironmentVariables(kubernetesDeployerProperties, schedulerProperties, expectedVars, isDeprecated); } private void testEnvironmentVariables(KubernetesDeployerProperties kubernetesDeployerProperties, Map schedulerProperties, EnvVar[] expectedVars, boolean isDeprecated) { if (kubernetesDeployerProperties.getNamespace() == null) { kubernetesDeployerProperties.setNamespace("default"); } KubernetesClient kubernetesClient = (new KubernetesClientBuilder()).build(); KubernetesScheduler kubernetesScheduler = new KubernetesScheduler(kubernetesClient, kubernetesDeployerProperties); AppDefinition appDefinition = new AppDefinition(randomName(), getAppProperties()); ScheduleRequest scheduleRequest = (isDeprecated) ? new ScheduleRequest(appDefinition, schedulerProperties, null, getCommandLineArgs(), randomName(), testApplication()) : new ScheduleRequest(appDefinition, schedulerProperties, getCommandLineArgs(), randomName(), testApplication()); CronJob cronJob = kubernetesScheduler.createCronJob(scheduleRequest); CronJobSpec cronJobSpec = cronJob.getSpec(); Container container = cronJobSpec.getJobTemplate().getSpec().getTemplate().getSpec().getContainers().get(0); assertThat(container.getEnv()).as("Environment variables should not be empty").isNotEmpty(); assertThat(container.getEnv()).contains(expectedVars); safeUnschedule(kubernetesScheduler, cronJob.getMetadata().getName()); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testTaskServiceAccountNameOverride(boolean isDeprecated) { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); if (kubernetesDeployerProperties.getNamespace() == null) { kubernetesDeployerProperties.setNamespace("default"); } KubernetesClient kubernetesClient = (new KubernetesClientBuilder()).build(); KubernetesScheduler kubernetesScheduler = new KubernetesScheduler(kubernetesClient, kubernetesDeployerProperties); String taskServiceAccountName = "mysa"; String prefix = KubernetesSchedulerProperties.KUBERNETES_SCHEDULER_PROPERTIES_PREFIX; Map schedulerProperties = new HashMap<>(getSchedulerProperties()); schedulerProperties.put(prefix + ".taskServiceAccountName", taskServiceAccountName); AppDefinition appDefinition = new AppDefinition(randomName(), getAppProperties()); ScheduleRequest scheduleRequest = new ScheduleRequest(appDefinition, schedulerProperties, null, getCommandLineArgs(), randomName(), testApplication()); CronJob cronJob = kubernetesScheduler.createCronJob(scheduleRequest); CronJobSpec cronJobSpec = cronJob.getSpec(); String serviceAccountName = cronJobSpec.getJobTemplate().getSpec().getTemplate().getSpec() .getServiceAccountName(); assertThat(serviceAccountName).as("Unexpected service account name").isEqualTo(taskServiceAccountName); safeUnschedule(kubernetesScheduler, cronJob.getMetadata().getName()); } @ParameterizedTest @ValueSource(booleans = {true, false}) public void testTaskServiceAccountNameDefault(boolean isDeprecated) { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); if (kubernetesDeployerProperties.getNamespace() == null) { kubernetesDeployerProperties.setNamespace("default"); } KubernetesClient kubernetesClient = (new KubernetesClientBuilder()).build(); KubernetesScheduler kubernetesScheduler = new KubernetesScheduler(kubernetesClient, kubernetesDeployerProperties); AppDefinition appDefinition = new AppDefinition(randomName(), getAppProperties()); ScheduleRequest scheduleRequest = (isDeprecated) ? new ScheduleRequest(appDefinition, getSchedulerProperties(), getDeploymentProperties(), getCommandLineArgs(), randomName(), testApplication()) : new ScheduleRequest(appDefinition, getDeploymentProperties(), getCommandLineArgs(), randomName(), testApplication()); CronJob cronJob = kubernetesScheduler.createCronJob(scheduleRequest); CronJobSpec cronJobSpec = cronJob.getSpec(); String serviceAccountName = cronJobSpec.getJobTemplate().getSpec().getTemplate().getSpec() .getServiceAccountName(); assertThat(serviceAccountName).as("Unexpected service account name") .isEqualTo(KubernetesSchedulerProperties.DEFAULT_TASK_SERVICE_ACCOUNT_NAME); safeUnschedule(kubernetesScheduler, cronJob.getMetadata().getName()); } @ParameterizedTest @ValueSource(strings = {"Forbid", "Allow", "Replace"}) public void testConcurrencyPolicy(String concurrencyPolicy) { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); if (kubernetesDeployerProperties.getNamespace() == null) { kubernetesDeployerProperties.setNamespace("default"); } KubernetesClient kubernetesClient = (new KubernetesClientBuilder()).build(); KubernetesScheduler kubernetesScheduler = new KubernetesScheduler(kubernetesClient, kubernetesDeployerProperties); AppDefinition appDefinition = new AppDefinition(randomName(), getAppProperties()); ScheduleRequest scheduleRequest = new ScheduleRequest(appDefinition, getSchedulerProperties(concurrencyPolicy), getCommandLineArgs(), randomName(), testApplication()); CronJob cronJob = kubernetesScheduler.createCronJob(scheduleRequest); assertThat(cronJob.getSpec().getConcurrencyPolicy()).isEqualTo(concurrencyPolicy); safeUnschedule(kubernetesScheduler, cronJob.getMetadata().getName()); } @Test public void testConcurrencyPolicyDefault() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); if (kubernetesDeployerProperties.getNamespace() == null) { kubernetesDeployerProperties.setNamespace("default"); } KubernetesClient kubernetesClient = (new KubernetesClientBuilder()).build(); KubernetesScheduler kubernetesScheduler = new KubernetesScheduler(kubernetesClient, kubernetesDeployerProperties); AppDefinition appDefinition = new AppDefinition(randomName(), getAppProperties()); ScheduleRequest scheduleRequest = new ScheduleRequest(appDefinition, getSchedulerProperties(), getCommandLineArgs(), randomName(), testApplication()); CronJob cronJob = kubernetesScheduler.createCronJob(scheduleRequest); assertThat(cronJob.getSpec().getConcurrencyPolicy()).isEqualTo("Allow"); safeUnschedule(kubernetesScheduler, cronJob.getMetadata().getName()); } @Test public void testConcurrencyPolicyFromServerProperties() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); if (kubernetesDeployerProperties.getNamespace() == null) { kubernetesDeployerProperties.setNamespace("default"); } kubernetesDeployerProperties.getCron().setConcurrencyPolicy("Forbid"); KubernetesClient kubernetesClient = (new KubernetesClientBuilder()).build(); KubernetesScheduler kubernetesScheduler = new KubernetesScheduler(kubernetesClient, kubernetesDeployerProperties); AppDefinition appDefinition = new AppDefinition(randomName(), getAppProperties()); ScheduleRequest scheduleRequest = new ScheduleRequest(appDefinition, getSchedulerProperties(), getCommandLineArgs(), randomName(), testApplication()); CronJob cronJob = kubernetesScheduler.createCronJob(scheduleRequest); assertThat(cronJob.getSpec().getConcurrencyPolicy()).isEqualTo("Forbid"); safeUnschedule(kubernetesScheduler, cronJob.getMetadata().getName()); } @ParameterizedTest @ValueSource(strings = {"100", "86400"}) public void testTtlSecondsAfterFinished(String ttlSecondsAfterFinished) { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); if (kubernetesDeployerProperties.getNamespace() == null) { kubernetesDeployerProperties.setNamespace("default"); } KubernetesClient kubernetesClient = (new KubernetesClientBuilder()).build(); KubernetesScheduler kubernetesScheduler = new KubernetesScheduler(kubernetesClient, kubernetesDeployerProperties); AppDefinition appDefinition = new AppDefinition(randomName(), getAppProperties()); Map schedulerProperties = new HashMap<>(getSchedulerProperties()); schedulerProperties.put(KubernetesScheduler.KUBERNETES_DEPLOYER_CRON_TTL_SECONDS_AFTER_FINISHED, ttlSecondsAfterFinished); ScheduleRequest scheduleRequest = new ScheduleRequest(appDefinition, schedulerProperties, getCommandLineArgs(), randomName(), testApplication()); CronJob cronJob = kubernetesScheduler.createCronJob(scheduleRequest); assertThat(cronJob.getSpec().getJobTemplate().getSpec().getTtlSecondsAfterFinished()) .isEqualTo(Integer.parseInt(ttlSecondsAfterFinished)); safeUnschedule(kubernetesScheduler, cronJob.getMetadata().getName()); } @ParameterizedTest @ValueSource(strings = {"100 2", "one", "10'"}) public void testInvalidTtlSecondsAfterFinished(String ttlSecondsAfterFinished) { AppDefinition appDefinition = new AppDefinition(randomName(), getAppProperties()); Map schedulerProperties = new HashMap<>(getSchedulerProperties()); schedulerProperties.put(KubernetesScheduler.KUBERNETES_DEPLOYER_CRON_TTL_SECONDS_AFTER_FINISHED, ttlSecondsAfterFinished); ScheduleRequest scheduleRequest = new ScheduleRequest(appDefinition, schedulerProperties, getCommandLineArgs(), randomName(), testApplication()); assertThatThrownBy(() -> { scheduler.schedule(scheduleRequest); }).isInstanceOf(NumberFormatException.class); } @Test public void testTtlSecondsAfterFinishedDefault() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); if (kubernetesDeployerProperties.getNamespace() == null) { kubernetesDeployerProperties.setNamespace("default"); } KubernetesClient kubernetesClient = (new KubernetesClientBuilder()).build(); KubernetesScheduler kubernetesScheduler = new KubernetesScheduler(kubernetesClient, kubernetesDeployerProperties); AppDefinition appDefinition = new AppDefinition(randomName(), getAppProperties()); ScheduleRequest scheduleRequest = new ScheduleRequest(appDefinition, getSchedulerProperties(), getCommandLineArgs(), randomName(), testApplication()); CronJob cronJob = kubernetesScheduler.createCronJob(scheduleRequest); assertThat(cronJob.getSpec().getJobTemplate().getSpec().getTtlSecondsAfterFinished()) .isNull(); safeUnschedule(kubernetesScheduler, cronJob.getMetadata().getName()); } @Test public void testTtlSecondsAfterFinishedFromServerProperties() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); if (kubernetesDeployerProperties.getNamespace() == null) { kubernetesDeployerProperties.setNamespace("default"); } kubernetesDeployerProperties.getCron().setTtlSecondsAfterFinished(86400); KubernetesClient kubernetesClient = (new KubernetesClientBuilder()).build(); KubernetesScheduler kubernetesScheduler = new KubernetesScheduler(kubernetesClient, kubernetesDeployerProperties); AppDefinition appDefinition = new AppDefinition(randomName(), getAppProperties()); ScheduleRequest scheduleRequest = new ScheduleRequest(appDefinition, getSchedulerProperties(), getCommandLineArgs(), randomName(), testApplication()); CronJob cronJob = kubernetesScheduler.createCronJob(scheduleRequest); assertThat(cronJob.getSpec().getJobTemplate().getSpec().getTtlSecondsAfterFinished()).isEqualTo(86400); safeUnschedule(kubernetesScheduler, cronJob.getMetadata().getName()); } @Test public void testBackoffLimit() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); if (kubernetesDeployerProperties.getNamespace() == null) { kubernetesDeployerProperties.setNamespace("default"); } KubernetesClient kubernetesClient = (new KubernetesClientBuilder()).build(); KubernetesScheduler kubernetesScheduler = new KubernetesScheduler(kubernetesClient, kubernetesDeployerProperties); AppDefinition appDefinition = new AppDefinition(randomName(), getAppProperties()); Map schedulerProperties = new HashMap<>(getSchedulerProperties()); schedulerProperties.put(KubernetesScheduler.KUBERNETES_DEPLOYER_CRON_BACKOFF_LIMIT, "5"); ScheduleRequest scheduleRequest = new ScheduleRequest(appDefinition, schedulerProperties, getCommandLineArgs(), randomName(), testApplication()); CronJob cronJob = kubernetesScheduler.createCronJob(scheduleRequest); assertThat(cronJob.getSpec().getJobTemplate().getSpec().getBackoffLimit()).isEqualTo(5); safeUnschedule(kubernetesScheduler, cronJob.getMetadata().getName()); } @Test public void testBackoffLimitDefault() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); if (kubernetesDeployerProperties.getNamespace() == null) { kubernetesDeployerProperties.setNamespace("default"); } KubernetesClient kubernetesClient = (new KubernetesClientBuilder()).build(); KubernetesScheduler kubernetesScheduler = new KubernetesScheduler(kubernetesClient, kubernetesDeployerProperties); AppDefinition appDefinition = new AppDefinition(randomName(), getAppProperties()); ScheduleRequest scheduleRequest = new ScheduleRequest(appDefinition, getSchedulerProperties(), getCommandLineArgs(), randomName(), testApplication()); CronJob cronJob = kubernetesScheduler.createCronJob(scheduleRequest); assertThat(cronJob.getSpec().getJobTemplate().getSpec().getBackoffLimit()).isNull(); safeUnschedule(kubernetesScheduler, cronJob.getMetadata().getName()); } @Test public void testBackoffLimitFromServerProperties() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); if (kubernetesDeployerProperties.getNamespace() == null) { kubernetesDeployerProperties.setNamespace("default"); } kubernetesDeployerProperties.getCron().setBackoffLimit(7); KubernetesClient kubernetesClient = (new KubernetesClientBuilder()).build(); KubernetesScheduler kubernetesScheduler = new KubernetesScheduler(kubernetesClient, kubernetesDeployerProperties); AppDefinition appDefinition = new AppDefinition(randomName(), getAppProperties()); ScheduleRequest scheduleRequest = new ScheduleRequest(appDefinition, getSchedulerProperties(), getCommandLineArgs(), randomName(), testApplication()); CronJob cronJob = kubernetesScheduler.createCronJob(scheduleRequest); assertThat(cronJob.getSpec().getJobTemplate().getSpec().getBackoffLimit()).isEqualTo(7); safeUnschedule(kubernetesScheduler, cronJob.getMetadata().getName()); } @AfterAll public static void cleanup() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); KubernetesClient kubernetesClient = (new KubernetesClientBuilder()).build(); KubernetesScheduler kubernetesScheduler = new KubernetesScheduler(kubernetesClient, kubernetesDeployerProperties); List scheduleInfos = kubernetesScheduler.list(); for (ScheduleInfo scheduleInfo : scheduleInfos) { safeUnschedule(kubernetesScheduler, scheduleInfo.getScheduleName()); } // Cleanup the schedules that aren't part of the list() - created from listScheduleWithExternalCronJobs test safeUnschedule(kubernetesScheduler, "job2"); safeUnschedule(kubernetesScheduler, "job3"); } private static void safeUnschedule(KubernetesScheduler scheduler, String scheduleName) { try { scheduler.unschedule(scheduleName); } catch (Exception ex) { LOGGER.warn("Failed to unschedule '" + scheduleName + "'", ex); } } @Configuration @EnableAutoConfiguration @EnableConfigurationProperties public static class Config { private final KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); @Bean public Scheduler scheduler(KubernetesClient kubernetesClient) { return new KubernetesScheduler(kubernetesClient, kubernetesDeployerProperties); } @Bean public KubernetesClient kubernetesClient() { if (kubernetesDeployerProperties.getNamespace() == null) { kubernetesDeployerProperties.setNamespace("default"); } return KubernetesClientFactory.getKubernetesClient(kubernetesDeployerProperties); } } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/test/java/org/springframework/cloud/deployer/spi/kubernetes/KubernetesSchedulerPropertiesTests.java ================================================ /* * Copyright 2018-2021 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.cloud.deployer.spi.kubernetes; import org.junit.jupiter.api.Test; import org.springframework.util.StringUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; /** * Tests for {@link KubernetesSchedulerProperties}. * * @author Chris Schaefer */ public class KubernetesSchedulerPropertiesTests { @Test public void testImagePullPolicyDefault() { KubernetesSchedulerProperties kubernetesSchedulerProperties = new KubernetesSchedulerProperties(); assertThat(kubernetesSchedulerProperties.getImagePullPolicy()).as("Image pull policy should not be null").isNotNull(); assertEquals(ImagePullPolicy.IfNotPresent, kubernetesSchedulerProperties.getImagePullPolicy(), "Invalid default image pull policy"); } @Test public void testImagePullPolicyCanBeCustomized() { KubernetesSchedulerProperties kubernetesSchedulerProperties = new KubernetesSchedulerProperties(); kubernetesSchedulerProperties.setImagePullPolicy(ImagePullPolicy.Never); assertThat(kubernetesSchedulerProperties.getImagePullPolicy()).as("Image pull policy should not be null").isNotNull(); assertEquals(ImagePullPolicy.Never, kubernetesSchedulerProperties.getImagePullPolicy(), "Unexpected image pull policy"); } @Test public void testRestartPolicyDefault() { KubernetesSchedulerProperties kubernetesSchedulerProperties = new KubernetesSchedulerProperties(); assertThat(kubernetesSchedulerProperties.getRestartPolicy()).as("Restart policy should not be null").isNotNull(); assertEquals(RestartPolicy.Never, kubernetesSchedulerProperties.getRestartPolicy(), "Invalid default restart policy"); } @Test public void testRestartPolicyCanBeCustomized() { KubernetesSchedulerProperties kubernetesSchedulerProperties = new KubernetesSchedulerProperties(); kubernetesSchedulerProperties.setRestartPolicy(RestartPolicy.OnFailure); assertThat(kubernetesSchedulerProperties.getRestartPolicy()).as("Restart policy should not be null").isNotNull(); assertEquals(RestartPolicy.OnFailure, kubernetesSchedulerProperties.getRestartPolicy(), "Unexpected restart policy"); } @Test public void testEntryPointStyleDefault() { KubernetesSchedulerProperties kubernetesSchedulerProperties = new KubernetesSchedulerProperties(); assertThat(kubernetesSchedulerProperties.getEntryPointStyle()).as("Entry point style should not be null").isNotNull(); assertEquals(EntryPointStyle.exec, kubernetesSchedulerProperties.getEntryPointStyle(), "Invalid default entry point style"); } @Test public void testEntryPointStyleCanBeCustomized() { KubernetesSchedulerProperties kubernetesSchedulerProperties = new KubernetesSchedulerProperties(); kubernetesSchedulerProperties.setEntryPointStyle(EntryPointStyle.shell); assertThat(kubernetesSchedulerProperties.getEntryPointStyle()).as("Entry point style should not be null").isNotNull(); assertEquals(EntryPointStyle.shell, kubernetesSchedulerProperties.getEntryPointStyle(), "Unexpected entry point stype"); } @Test public void testNamespaceDefault() { KubernetesSchedulerProperties kubernetesSchedulerProperties = new KubernetesSchedulerProperties(); if (kubernetesSchedulerProperties.getNamespace() == null) { kubernetesSchedulerProperties.setNamespace("default"); assertThat(StringUtils.hasText(kubernetesSchedulerProperties.getNamespace())).as("Namespace should not be empty or null").isTrue(); assertEquals("default", kubernetesSchedulerProperties.getNamespace(), "Invalid default namespace"); } } @Test public void testNamespaceCanBeCustomized() { KubernetesSchedulerProperties kubernetesSchedulerProperties = new KubernetesSchedulerProperties(); kubernetesSchedulerProperties.setNamespace("myns"); assertThat(StringUtils.hasText(kubernetesSchedulerProperties.getNamespace())).as("Namespace should not be empty or null").isTrue(); assertEquals("myns", kubernetesSchedulerProperties.getNamespace(), "Unexpected namespace"); } @Test public void testImagePullSecretDefault() { KubernetesSchedulerProperties kubernetesSchedulerProperties = new KubernetesSchedulerProperties(); assertThat(kubernetesSchedulerProperties.getImagePullSecret()).as("No default image pull secret should be set").isNull(); } @Test public void testImagePullSecretCanBeCustomized() { String secret = "mysecret"; KubernetesSchedulerProperties kubernetesSchedulerProperties = new KubernetesSchedulerProperties(); kubernetesSchedulerProperties.setImagePullSecret(secret); assertThat(kubernetesSchedulerProperties.getImagePullSecret()).as("Image pull secret should not be null").isNotNull(); assertEquals(secret, kubernetesSchedulerProperties.getImagePullSecret(), "Unexpected image pull secret"); } @Test public void testEnvironmentVariablesDefault() { KubernetesSchedulerProperties kubernetesSchedulerProperties = new KubernetesSchedulerProperties(); assertEquals(0, kubernetesSchedulerProperties.getEnvironmentVariables().length, "No default environment variables should be set"); } @Test public void testEnvironmentVariablesCanBeCustomized() { String[] envVars = new String[] { "var1=val1", "var2=val2" }; KubernetesSchedulerProperties kubernetesSchedulerProperties = new KubernetesSchedulerProperties(); kubernetesSchedulerProperties.setEnvironmentVariables(envVars); assertThat(kubernetesSchedulerProperties.getEnvironmentVariables()).as("Environment variables should not be null").isNotNull(); assertEquals(2, kubernetesSchedulerProperties.getEnvironmentVariables().length, "Unexpected number of environment variables"); } @Test public void testTaskServiceAccountNameDefault() { KubernetesSchedulerProperties kubernetesSchedulerProperties = new KubernetesSchedulerProperties(); assertThat(kubernetesSchedulerProperties.getTaskServiceAccountName()).as("Task service account name should not be null").isNotNull(); assertEquals(KubernetesSchedulerProperties.DEFAULT_TASK_SERVICE_ACCOUNT_NAME, kubernetesSchedulerProperties.getTaskServiceAccountName(), "Unexpected default task service account name"); } @Test public void testTaskServiceAccountNameCanBeCustomized() { String taskServiceAccountName = "mysa"; KubernetesSchedulerProperties kubernetesSchedulerProperties = new KubernetesSchedulerProperties(); kubernetesSchedulerProperties.setTaskServiceAccountName(taskServiceAccountName); assertThat(kubernetesSchedulerProperties.getTaskServiceAccountName()).as("Task service account name should not be null").isNotNull(); assertEquals(taskServiceAccountName, kubernetesSchedulerProperties.getTaskServiceAccountName(), "Unexpected task service account name"); } // Re-implement when we have a proper env binding via boot // @RunWith(PowerMockRunner.class) // @PrepareForTest({ KubernetesSchedulerProperties.class }) // public static class EnvTests { // @Test // public void testNamespaceFromEnvironment() throws Exception { // PowerMockito.mockStatic(System.class); // PowerMockito.when(System.getenv(KubernetesSchedulerProperties.ENV_KEY_KUBERNETES_NAMESPACE)) // .thenReturn("nsfromenv"); // KubernetesSchedulerProperties kubernetesSchedulerProperties = new KubernetesSchedulerProperties(); // assertTrue("Namespace should not be empty or null", // StringUtils.hasText(kubernetesSchedulerProperties.getNamespace())); // assertEquals("Unexpected namespace from environment", "nsfromenv", // kubernetesSchedulerProperties.getNamespace()); // } // } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/test/java/org/springframework/cloud/deployer/spi/kubernetes/KubernetesTaskLauncherIntegrationIT.java ================================================ /* * Copyright 2016-2022 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.cloud.deployer.spi.kubernetes; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.function.Consumer; import io.fabric8.kubernetes.api.model.Container; import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.client.utils.PodStatusUtil; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.task.LaunchState; import org.springframework.core.io.Resource; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; /** * Integration tests for {@link KubernetesTaskLauncher}. * *

NOTE: The tests do not call {@code TaskLauncher.destroy/cleanup} in a finally block but instead rely on the * {@link AbstractKubernetesTaskLauncherIntegrationTests#cleanupLingeringApps() AfterEach method} to clean any stray apps. * * @author Thomas Risberg * @author Chris Schaefer * @author Chris Bono * @author Glenn Renfro */ @SpringBootTest(classes = {KubernetesAutoConfiguration.class}, properties = { "spring.cloud.deployer.kubernetes.namespace=default" }) @ExtendWith(OutputCaptureExtension.class) public class KubernetesTaskLauncherIntegrationIT extends AbstractKubernetesTaskLauncherIntegrationTests { @Test void taskLaunchedWithJobPodAnnotations(TestInfo testInfo) { logTestInfo(testInfo); launchTaskPodAndValidateCreatedPodWithCleanup( Collections.singletonMap("spring.cloud.deployer.kubernetes.jobAnnotations", "key1:val1,key2:val2,key3:val31:val32"), (pod) -> { assertThat(pod.getSpec().getContainers()).isNotEmpty() .element(0).extracting(Container::getPorts).asInstanceOf(InstanceOfAssertFactories.LIST).isEmpty(); assertThat(pod.getMetadata().getAnnotations()).isNotEmpty() .contains(entry("key1", "val1"), entry("key2", "val2"), entry("key3", "val31:val32")); }); } @Test void taskLaunchedWithDeploymentLabels(TestInfo testInfo) { logTestInfo(testInfo); launchTaskPodAndValidateCreatedPodWithCleanup( Collections.singletonMap("spring.cloud.deployer.kubernetes.deploymentLabels", "label1:value1,label2:value2"), (pod) -> { assertThat(pod.getSpec().getContainers()).isNotEmpty() .element(0).extracting(Container::getPorts).asInstanceOf(InstanceOfAssertFactories.LIST).isEmpty(); assertThat(pod.getMetadata().getLabels()).isNotEmpty() .contains(entry("label1", "value1"), entry("label2", "value2")); }); } @Test void tasksLaunchedWithAdditionalContainers(TestInfo testInfo) { logTestInfo(testInfo); launchTaskPodAndValidateCreatedPodWithCleanup( Collections.singletonMap("spring.cloud.deployer.kubernetes.additionalContainers", "[{name: 'test', image: 'busybox:latest', command: ['sh', '-c', 'echo hello']}]"), (pod) -> assertThat(pod.getSpec().getContainers()).hasSize(2) .filteredOn("name", "test").singleElement() .hasFieldOrPropertyWithValue("image", "busybox:latest") .hasFieldOrPropertyWithValue("command", Arrays.asList("sh", "-c", "echo hello"))); } private void launchTaskPodAndValidateCreatedPodWithCleanup(Map deploymentProps, Consumer assertingConsumer) { String taskName = randomName(); AppDefinition definition = new AppDefinition(taskName, null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, deploymentProps); log.info("Launching {}...", taskName); String launchId = taskLauncher().launch(request); awaitWithPollAndTimeout(deploymentTimeout()) .untilAsserted(() -> assertThat(taskLauncher().status(launchId).getState()).isEqualTo(LaunchState.running)); log.info("Checking task Pod for {}...", taskName); List pods = getPodsForTask(taskName); assertThat(pods).hasSize(1); assertThat(pods).singleElement().satisfies(assertingConsumer); log.info("Destroying {}...", taskName); taskLauncher().destroy(taskName); awaitWithPollAndTimeout(undeploymentTimeout()) .untilAsserted(() -> assertThat(taskLauncher().status(launchId).getState()).isEqualTo(LaunchState.unknown)); } @Test void cleanupDeletesTaskPod(TestInfo testInfo) { logTestInfo(testInfo); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, null); String taskName = request.getDefinition().getName(); log.info("Launching {}...", taskName); String launchId = taskLauncher().launch(request); awaitWithPollAndTimeout(deploymentTimeout()) .untilAsserted(() -> assertThat(taskLauncher().status(launchId).getState()).isEqualTo(LaunchState.running)); List pods = getPodsForTask(taskName); assertThat(pods).hasSize(1); assertThat(PodStatusUtil.isRunning(pods.get(0))).isTrue(); log.info("Cleaning up {}...", taskName); taskLauncher().cleanup(launchId); awaitWithPollAndTimeout(undeploymentTimeout()) .untilAsserted(() -> assertThat(taskLauncher().status(launchId).getState()).isEqualTo(LaunchState.unknown)); pods = getPodsForTask(taskName); assertThat(pods).isEmpty(); } @Test void cleanupForNonExistentTaskThrowsException(TestInfo testInfo, CapturedOutput taskOutput) { logTestInfo(testInfo); taskLauncher().cleanup("foo"); assertThat(taskOutput.getAll()).contains("Cannot delete pod for task \"foo\" (reason: pod does not exist)"); } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/test/java/org/springframework/cloud/deployer/spi/kubernetes/KubernetesTaskLauncherMaximumConcurrentTasksTests.java ================================================ /* * Copyright 2021 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.cloud.deployer.spi.kubernetes; import java.util.ArrayList; import java.util.Collections; import java.util.List; import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.api.model.PodBuilder; import io.fabric8.kubernetes.api.model.PodList; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.PodResource; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.task.TaskLauncher; import org.springframework.core.io.Resource; import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; /** * @author David Turanski **/ @SpringBootTest(classes = { KubernetesAutoConfiguration.class }, properties = { "spring.cloud.deployer.kubernetes.maximum-concurrent-tasks=10" }) @ExtendWith(SpringExtension.class) public class KubernetesTaskLauncherMaximumConcurrentTasksTests { @Autowired private TaskLauncher taskLauncher; @MockBean private KubernetesClient client; private List pods; @Test public void getMaximumConcurrentTasksExceeded() { assertThat(taskLauncher).isNotNull(); pods = stubForRunningPods(10); MixedOperation podsOperation = mock(MixedOperation.class); FilterWatchListDeletable filterWatchListDeletable = mock(FilterWatchListDeletable.class); when(podsOperation.withLabel("task-name")).thenReturn(filterWatchListDeletable); when(filterWatchListDeletable.list()).thenAnswer(invocation -> { PodList podList = new PodList(); List items = new ArrayList<>(); podList.setItems(pods); return podList; }); when(client.pods()).thenReturn(podsOperation); when(podsOperation.withName(anyString())).thenAnswer(invocation -> { Pod p = pods.stream().filter(pod -> pod.getMetadata().getName().equals(invocation.getArgument(0))) .findFirst().orElse(null); PodResource podResource = mock(PodResource.class); when(podResource.get()).thenReturn(p); return podResource; }); int executionCount = taskLauncher.getRunningTaskExecutionCount(); assertThat(executionCount).isEqualTo(10); assertThat(taskLauncher.getMaximumConcurrentTasks()).isEqualTo(taskLauncher.getRunningTaskExecutionCount()); AppDefinition appDefinition = new AppDefinition("task", Collections.emptyMap()); AppDeploymentRequest request = new AppDeploymentRequest(appDefinition, mock(Resource.class), Collections.emptyMap()); assertThatThrownBy(() -> { taskLauncher.launch(request); }).isInstanceOf(IllegalStateException.class).hasMessageContaining( "Cannot launch task task. The maximum concurrent task executions is at its limit [10]."); } private List stubForRunningPods(int numTasks) { List items = new ArrayList<>(); for (int i = 0; i < numTasks; i++) { items.add(new PodBuilder().withNewMetadata() .withName("task-" + i).endMetadata() .withNewStatus() .withPhase("Running") .endStatus().build()); } return items; } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/test/java/org/springframework/cloud/deployer/spi/kubernetes/KubernetesTaskLauncherWithJobIntegrationIT.java ================================================ /* * Copyright 2016-2022 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.cloud.deployer.spi.kubernetes; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Consumer; import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.api.model.batch.v1.Job; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.task.LaunchState; import org.springframework.core.io.Resource; import org.springframework.test.context.TestPropertySource; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.entry; /** * Integration tests for {@link KubernetesTaskLauncher} using jobs instead of bare pods. * *

NOTE: The tests do not call {@code TaskLauncher.destroy/cleanup} in a finally block but instead rely on the * {@link AbstractKubernetesTaskLauncherIntegrationTests#cleanupLingeringApps() AfterEach method} to clean any stray apps. * * @author Leonardo Diniz * @author Chris Schaefer * @author Ilayaperumal Gopinathan * @author Chris Bono * @author Glenn Renfro */ @SpringBootTest(classes = {KubernetesAutoConfiguration.class}) @TestPropertySource(properties = "spring.cloud.deployer.kubernetes.create-job=true") @ExtendWith(OutputCaptureExtension.class) public class KubernetesTaskLauncherWithJobIntegrationIT extends AbstractKubernetesTaskLauncherIntegrationTests { @BeforeEach public void setup() { if (kubernetesClient.getNamespace() == null) { kubernetesClient.getConfiguration().setNamespace("default"); } } @Test void taskLaunchedWithJobAnnotations(TestInfo testInfo) { logTestInfo(testInfo); launchTaskJobAndValidateCreatedJobAndPodWithCleanup( Collections.singletonMap("spring.cloud.deployer.kubernetes.jobAnnotations", "key1:val1,key2:val2,key3:val31:val32"), (job) -> assertThat(job.getMetadata().getAnnotations()).isNotEmpty() .contains(entry("key1", "val1"), entry("key2", "val2"), entry("key3", "val31:val32")), (pod) -> assertThat(pod.getMetadata().getAnnotations()).isNotEmpty() .contains(entry("key1", "val1"), entry("key2", "val2"), entry("key3", "val31:val32"))); } @Test void taskLaunchedWithJobSpecProperties(TestInfo testInfo) { logTestInfo(testInfo); Map deploymentProps = new HashMap<>(); deploymentProps.put("spring.cloud.deployer.kubernetes.restartPolicy", "OnFailure"); deploymentProps.put("spring.cloud.deployer.kubernetes.backoffLimit", "5"); launchTaskJobAndValidateCreatedJobAndPodWithCleanup( deploymentProps, (job) -> assertThat(job.getSpec().getBackoffLimit()).isEqualTo(5), (pod) -> {}); } private void launchTaskJobAndValidateCreatedJobAndPodWithCleanup(Map deploymentProps, Consumer assertingJobConsumer, Consumer assertingPodConsumer) { String taskName = randomName(); AppDefinition definition = new AppDefinition(taskName, null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, deploymentProps); log.info("Launching {}...", taskName); String launchId = taskLauncher().launch(request); awaitWithPollAndTimeout(deploymentTimeout()) .untilAsserted(() -> assertThat(taskLauncher().status(launchId).getState()).isEqualTo(LaunchState.launching)); log.info("Checking task Job for {}...", taskName); List jobs = getJobsForTask(taskName); assertThat(jobs).hasSize(1); assertThat(jobs).singleElement().satisfies(assertingJobConsumer); log.info("Checking task Pod for {}...", taskName); List pods = getPodsForTask(taskName); assertThat(pods).hasSize(1); assertThat(pods).singleElement().satisfies(assertingPodConsumer); log.info("Destroying {}...", taskName); taskLauncher().destroy(taskName); awaitWithPollAndTimeout(undeploymentTimeout()) .untilAsserted(() -> assertThat(taskLauncher().status(launchId).getState()).isEqualTo(LaunchState.unknown)); } @Test void taskLaunchedWithInvalidRestartPolicyThrowsException(TestInfo testInfo) { logTestInfo(testInfo); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); Map deploymentProps = new HashMap<>(); deploymentProps.put("spring.cloud.deployer.kubernetes.restartPolicy", "Always"); deploymentProps.put("spring.cloud.deployer.kubernetes.backoffLimit", "5"); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, deploymentProps); log.info("Launching {}...", request.getDefinition().getName()); assertThatThrownBy(() -> taskLauncher.launch(request)) .isInstanceOf(Exception.class) .hasMessage("RestartPolicy should not be 'Always' when the JobSpec is used."); } @Test void cleanupDeletesTaskJob(TestInfo testInfo) { logTestInfo(testInfo); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, null); String taskName = request.getDefinition().getName(); log.info("Launching {}...", taskName); String launchId = taskLauncher().launch(request); awaitWithPollAndTimeout(deploymentTimeout()) .untilAsserted(() -> assertThat(taskLauncher().status(launchId).getState()).isEqualTo(LaunchState.launching)); List jobs = getJobsForTask(taskName); assertThat(jobs).hasSize(1); log.info("Cleaning up {}...", taskName); taskLauncher().cleanup(launchId); awaitWithPollAndTimeout(undeploymentTimeout()) .untilAsserted(() -> assertThat(taskLauncher().status(launchId).getState()).isEqualTo(LaunchState.unknown)); jobs = getJobsForTask(taskName); assertThat(jobs).isEmpty(); } @Test void cleanupForNonExistentTaskThrowsException(TestInfo testInfo, CapturedOutput taskOutput) { logTestInfo(testInfo); taskLauncher().cleanup("foo"); assertThat(taskOutput.getAll()).contains("Cannot delete job for task \"foo\" (reason: job does not exist)"); } @Test void deleteJobAfterTtlSecondsOnAfterFinishedExpire(TestInfo testInfo) { logTestInfo(testInfo); Map applicationProps = new HashMap<>(); applicationProps.put("killDelay", "1"); AppDefinition definition = new AppDefinition(randomName(), applicationProps); Resource resource = testApplication(); Map deploymentProps = new HashMap<>(); deploymentProps.put("spring.cloud.deployer.kubernetes.restartPolicy", "Never"); deploymentProps.put("spring.cloud.deployer.kubernetes.backoffLimit", "0"); deploymentProps.put("spring.cloud.deployer.kubernetes.ttlSecondsAfterFinished", "3"); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, deploymentProps); String taskName = request.getDefinition().getName(); log.info("Launching {}...", taskName); String launchId = taskLauncher().launch(request); awaitWithPollAndTimeout(deploymentTimeout()) .untilAsserted(() -> assertThat(taskLauncher().status(launchId).getState()).isEqualTo(LaunchState.launching)); List jobs = getJobsForTask(taskName); assertThat(jobs).hasSize(1); log.info("Waiting for deleting the job {}...", taskName); awaitWithPollAndTimeout(undeploymentTimeout()) .untilAsserted(() -> assertThat(taskLauncher().status(launchId).getState()).isEqualTo(LaunchState.unknown)); jobs = getJobsForTask(taskName); assertThat(jobs).isEmpty(); } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/test/java/org/springframework/cloud/deployer/spi/kubernetes/PropertyParserUtilsTests.java ================================================ /* * Copyright 2018-2022 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.cloud.deployer.spi.kubernetes; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; import org.springframework.cloud.deployer.spi.kubernetes.support.PropertyParserUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Tests for PropertyParserUtils * * @author Chris Schaefer * @author Ilayaperumal Gopinathan * @author Glenn Renfro */ public class PropertyParserUtilsTests { @Test public void testAnnotationParseSingle() { Map annotations = PropertyParserUtils.getStringPairsToMap("annotation:value"); assertThat(annotations.isEmpty()).isFalse(); assertThat(annotations.size() == 1).isTrue(); assertThat(annotations.containsKey("annotation")).isTrue(); assertThat(annotations.get("annotation").equals("value")).isTrue(); } @Test public void testAnnotationParseMultiple() { Map annotations = PropertyParserUtils.getStringPairsToMap("annotation1:value1,annotation2:value2"); assertThat(annotations.isEmpty()).isFalse(); assertThat(annotations.size() == 2).isTrue(); assertThat(annotations.containsKey("annotation1")).isTrue(); assertThat(annotations.get("annotation1").equals("value1")).isTrue(); assertThat(annotations.containsKey("annotation2")).isTrue(); assertThat(annotations.get("annotation2").equals("value2")).isTrue(); } @Test public void testAnnotationParseMultipleWithCommas() { assertThat(PropertyParserUtils.getStringPairsToMap("annotation1:\"value1,a,b,c,d\",annotation2:value2")) .isNotEmpty() .hasSize(2) .containsEntry("annotation1", "\"value1,a,b,c,d\"") .containsEntry("annotation2", "value2"); assertThat(PropertyParserUtils.getStringPairsToMap("annotation1:value1,annotation2:\"value2,a,b,c,d\"")) .isNotEmpty() .hasSize(2) .containsEntry("annotation1", "value1") .containsEntry("annotation2", "\"value2,a,b,c,d\""); assertThat(PropertyParserUtils.getStringPairsToMap("annotation1:\"value1,a,b,c,d\",annotation2:\"value2,a,b,c,d\"")) .isNotEmpty() .hasSize(2) .containsEntry("annotation1", "\"value1,a,b,c,d\"") .containsEntry("annotation2", "\"value2,a,b,c,d\""); // Test even number of quotes not to be used as token for ignoring commas boundary. assertThat(PropertyParserUtils.getStringPairsToMap("annotation1:\"value1,a,b,\"\"c,d\",annotation2:\"value2,a,b,c,d\"")) .isNotEmpty() .hasSize(2) .containsEntry("annotation1", "\"value1,a,b,\"\"c,d\"") .containsEntry("annotation2", "\"value2,a,b,c,d\""); } @Test public void testAnnotationWithQuotes() { Map annotations = PropertyParserUtils.getStringPairsToMap("annotation1:\"value1\",annotation2:value2"); assertThat(annotations.isEmpty()).isFalse(); assertThat(annotations.size() == 2).isTrue(); assertThat(annotations.containsKey("annotation1")).isTrue(); assertThat(annotations.get("annotation1").equals("\"value1\"")).isTrue(); assertThat(annotations.containsKey("annotation2")).isTrue(); assertThat(annotations.get("annotation2").equals("value2")).isTrue(); } @Test public void testAnnotationMultipleColon() { String annotation = "iam.amazonaws.com/role:arn:aws:iam::12345678:role/role-name,key1:val1:val2:val3," + "key2:val4::val5:val6::val7:val8"; Map annotations = PropertyParserUtils.getStringPairsToMap(annotation); assertThat(annotations.isEmpty()).isFalse(); assertThat(annotations.size() == 3).isTrue(); assertThat(annotations.containsKey("iam.amazonaws.com/role")).isTrue(); assertThat(annotations.get("iam.amazonaws.com/role").equals("arn:aws:iam::12345678:role/role-name")).isTrue(); assertThat(annotations.containsKey("key1")).isTrue(); assertThat(annotations.get("key1").equals("val1:val2:val3")).isTrue(); assertThat(annotations.containsKey("key2")).isTrue(); assertThat(annotations.get("key2").equals("val4::val5:val6::val7:val8")).isTrue(); } @Test public void testAnnotationParseInvalidValue() { assertThatThrownBy(() -> { PropertyParserUtils.getStringPairsToMap("annotation1:value1,annotation2,annotation3:value3"); }).isInstanceOf(IllegalArgumentException.class); } @Test public void testDeploymentPropertyParsing() { Map deploymentProps = new HashMap<>(); deploymentProps.put("SPRING_CLOUD_DEPLOYER_KUBERNETES_IMAGEPULLPOLICY", "Never"); deploymentProps.put("spring.cloud.deployer.kubernetes.pod-annotations", "key1:value1,key2:value2"); deploymentProps.put("spring.cloud.deployer.kubernetes.serviceAnnotations", "key3:value3,key4:value4"); deploymentProps.put("spring.cloud.deployer.kubernetes.init-container.image-name", "springcloud/openjdk"); deploymentProps.put("spring.cloud.deployer.kubernetes.initContainer.containerName", "test"); deploymentProps.put("spring.cloud.deployer.kubernetes.shareProcessNamespace", "true"); deploymentProps.put("spring.cloud.deployer.kubernetes.priority-class-name", "high-priority"); deploymentProps.put("spring.cloud.deployer.kubernetes.init-container.commands", "['sh','echo hello']"); assertThat(PropertyParserUtils.getDeploymentPropertyValue(deploymentProps, "spring.cloud.deployer.kubernetes.podAnnotations").equals("key1:value1,key2:value2")).isTrue(); assertThat(PropertyParserUtils.getDeploymentPropertyValue(deploymentProps, "spring.cloud.deployer.kubernetes.serviceAnnotations").equals("key3:value3,key4:value4")).isTrue(); assertThat(PropertyParserUtils.getDeploymentPropertyValue(deploymentProps, "spring.cloud.deployer.kubernetes.initContainer.imageName").equals("springcloud/openjdk")).isTrue(); assertThat(PropertyParserUtils.getDeploymentPropertyValue(deploymentProps, "spring.cloud.deployer.kubernetes.initContainer.imageName").equals("springcloud/openjdk")).isTrue(); assertThat(PropertyParserUtils.getDeploymentPropertyValue(deploymentProps, "spring.cloud.deployer.kubernetes.imagePullPolicy").equals("Never")).isTrue(); assertThat(PropertyParserUtils.getDeploymentPropertyValue(deploymentProps, "spring.cloud.deployer.kubernetes.priority-class-name").equals("high-priority")).isTrue(); assertThat(PropertyParserUtils.getDeploymentPropertyValue(deploymentProps, "spring.cloud.deployer.kubernetes.shareProcessNamespace").equals("true")).isTrue(); } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/test/java/org/springframework/cloud/deployer/spi/kubernetes/RunAbstractKubernetesDeployerTests.java ================================================ /* * Copyright 2016-2021 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.cloud.deployer.spi.kubernetes; import java.util.Collections; import java.util.HashMap; import java.util.Map; import io.fabric8.kubernetes.api.model.Quantity; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.core.io.FileSystemResource; import static org.assertj.core.api.Assertions.assertThat; /** * Unit test for {@link AbstractKubernetesDeployer}. * * @author Moritz Schulze * @author Chris Schaefer * @author Ilayaperumal Gopinathan */ public class RunAbstractKubernetesDeployerTests { private AppDeploymentRequest deploymentRequest; private KubernetesDeployerProperties kubernetesDeployerProperties; private Map deploymentProperties; private DeploymentPropertiesResolver deploymentPropertiesResolver; @BeforeEach public void setUp() throws Exception { this.deploymentProperties = new HashMap<>(); this.deploymentRequest = new AppDeploymentRequest(new AppDefinition("foo", Collections.emptyMap()), new FileSystemResource(""), deploymentProperties); this.kubernetesDeployerProperties = new KubernetesDeployerProperties(); this.deploymentPropertiesResolver = new DeploymentPropertiesResolver( KubernetesDeployerProperties.KUBERNETES_DEPLOYER_PROPERTIES_PREFIX, this.kubernetesDeployerProperties); } @Test public void deduceImagePullPolicy_fallsBackToIfNotPresentIfOverrideNotParseable() { deploymentProperties.put("spring.cloud.deployer.kubernetes.imagePullPolicy", "not-a-real-value"); ImagePullPolicy pullPolicy = this.deploymentPropertiesResolver.deduceImagePullPolicy(deploymentRequest.getDeploymentProperties()); assertThat(pullPolicy).isEqualTo(ImagePullPolicy.IfNotPresent); } @Test public void limitGpu_noDeploymentProperty_incompleteServerProperty1_noGpu() { kubernetesDeployerProperties.getLimits().setGpuVendor("nvidia.com"); Map limits = this.deploymentPropertiesResolver.deduceResourceLimits(deploymentRequest.getDeploymentProperties()); assertThat(limits.get("nvidia.com/gpu")).isNull(); } @Test public void limitGpu_noDeploymentProperty_incompleteServerProperty2_noGpu() { kubernetesDeployerProperties.getLimits().setGpuCount("2"); Map limits = this.deploymentPropertiesResolver.deduceResourceLimits(deploymentRequest.getDeploymentProperties()); assertThat(limits.get("nvidia.com/gpu")).isNull(); } @Test public void limitGpu_noDeploymentProperty_serverProperty_usesServerProperty() { kubernetesDeployerProperties.getLimits().setGpuVendor("nvidia.com/gpu"); kubernetesDeployerProperties.getLimits().setGpuCount("2"); Map limits = this.deploymentPropertiesResolver.deduceResourceLimits(deploymentRequest.getDeploymentProperties()); assertThat(limits.get("nvidia.com/gpu")).isEqualTo(new Quantity("2")); } @Test public void limitGpu_deploymentPropertyVendor_usesDeploymentProperty() { kubernetesDeployerProperties.getLimits().setGpuVendor("nvidia.com/gpu"); kubernetesDeployerProperties.getLimits().setGpuCount("2"); deploymentProperties.put("spring.cloud.deployer.kubernetes.limits.gpu_vendor", "ati.com/gpu"); Map limits = this.deploymentPropertiesResolver.deduceResourceLimits(deploymentRequest.getDeploymentProperties()); assertThat(limits.get("ati.com/gpu")).isEqualTo(new Quantity("2")); } @Test public void limitGpu_deploymentPropertyCount_usesDeploymentProperty() { kubernetesDeployerProperties.getLimits().setGpuVendor("nvidia.com/gpu"); kubernetesDeployerProperties.getLimits().setGpuCount("2"); deploymentProperties.put("spring.cloud.deployer.kubernetes.limits.gpu_count", "1"); Map limits = this.deploymentPropertiesResolver.deduceResourceLimits(deploymentRequest.getDeploymentProperties()); assertThat(limits.get("nvidia.com/gpu")).isEqualTo(new Quantity("1")); } @Test public void limitGpu_deploymentPropertyBoth_usesDeploymentProperty() { kubernetesDeployerProperties.getLimits().setGpuVendor("nvidia.com/gpu"); kubernetesDeployerProperties.getLimits().setGpuCount("2"); deploymentProperties.put("spring.cloud.deployer.kubernetes.limits.gpu_vendor", "ati.com/gpu"); deploymentProperties.put("spring.cloud.deployer.kubernetes.limits.gpu_count", "1"); Map limits = this.deploymentPropertiesResolver.deduceResourceLimits(deploymentRequest.getDeploymentProperties()); assertThat(limits.get("ati.com/gpu")).isEqualTo(new Quantity("1")); } @Test public void limitCpu_noDeploymentProperty_serverProperty_usesServerProperty() { kubernetesDeployerProperties.getLimits().setCpu("400m"); Map limits = this.deploymentPropertiesResolver.deduceResourceLimits(deploymentRequest.getDeploymentProperties()); assertThat(limits.get("cpu")).isEqualTo(new Quantity("400m")); } @Test public void limitMemory_noDeploymentProperty_serverProperty_usesServerProperty() { kubernetesDeployerProperties.getLimits().setMemory("540Mi"); Map limits = this.deploymentPropertiesResolver.deduceResourceLimits(deploymentRequest.getDeploymentProperties()); assertThat(limits.get("memory")).isEqualTo(new Quantity("540Mi")); } @Test public void limitCpu_deploymentProperty_usesDeploymentProperty() { kubernetesDeployerProperties.getLimits().setCpu("100m"); deploymentProperties.put("spring.cloud.deployer.kubernetes.limits.cpu", "400m"); Map limits = this.deploymentPropertiesResolver.deduceResourceLimits(deploymentRequest.getDeploymentProperties()); assertThat(limits.get("cpu")).isEqualTo(new Quantity("400m")); } @Test public void limitMemory_deploymentProperty_usesDeploymentProperty() { kubernetesDeployerProperties.getLimits().setMemory("640Mi"); deploymentProperties.put("spring.cloud.deployer.kubernetes.limits.memory", "256Mi"); Map limits = this.deploymentPropertiesResolver.deduceResourceLimits(deploymentRequest.getDeploymentProperties()); assertThat(limits.get("memory")).isEqualTo(new Quantity("256Mi")); } @Test public void requestCpu_noDeploymentProperty_serverProperty_usesServerProperty() { kubernetesDeployerProperties.getRequests().setCpu("400m"); Map requests = this.deploymentPropertiesResolver.deduceResourceRequests(deploymentRequest.getDeploymentProperties()); assertThat(requests.get("cpu")).isEqualTo(new Quantity("400m")); } @Test public void requestMemory_noDeploymentProperty_serverProperty_usesServerProperty() { kubernetesDeployerProperties.getRequests().setMemory("120Mi"); Map requests = this.deploymentPropertiesResolver.deduceResourceRequests(deploymentRequest.getDeploymentProperties()); assertThat(requests.get("memory")).isEqualTo(new Quantity("120Mi")); } @Test public void requestCpu_deploymentProperty_usesDeploymentProperty() { kubernetesDeployerProperties.getRequests().setCpu("1000m"); deploymentProperties.put("spring.cloud.deployer.kubernetes.requests.cpu", "461m"); Map requests = this.deploymentPropertiesResolver.deduceResourceRequests(deploymentRequest.getDeploymentProperties()); assertThat(requests.get("cpu")).isEqualTo(new Quantity("461m")); } @Test public void requestMemory_deploymentProperty_usesDeploymentProperty() { kubernetesDeployerProperties.getRequests().setMemory("640Mi"); deploymentProperties.put("spring.cloud.deployer.kubernetes.requests.memory", "256Mi"); Map requests = this.deploymentPropertiesResolver.deduceResourceRequests(deploymentRequest.getDeploymentProperties()); assertThat(requests.get("memory")).isEqualTo(new Quantity("256Mi")); } @Test public void requestEphemeralStorage_deploymentProperty_usesDeploymentProperty() { kubernetesDeployerProperties.getRequests().setEphemeralStorage("2Gi"); deploymentProperties.put("spring.cloud.deployer.kubernetes.requests.ephemeral-storage", "2Gi"); Map requests = this.deploymentPropertiesResolver.deduceResourceRequests(deploymentRequest.getDeploymentProperties()); assertThat(requests.get("ephemeral-storage")).isEqualTo(new Quantity("2Gi")); } @Test public void limitEphemeralStorage_deploymentProperty_usesDeploymentProperty() { kubernetesDeployerProperties.getLimits().setEphemeralStorage("2Gi"); deploymentProperties.put("spring.cloud.deployer.kubernetes.limits.ephemeral-storage", "2Gi"); Map limits = this.deploymentPropertiesResolver.deduceResourceLimits(deploymentRequest.getDeploymentProperties()); assertThat(limits.get("ephemeral-storage")).isEqualTo(new Quantity("2Gi")); } @Test public void requestHugepages1Gi_deploymentProperty_usesDeploymentProperty() { kubernetesDeployerProperties.getRequests().setHugepages1Gi("4"); deploymentProperties.put("spring.cloud.deployer.kubernetes.requests.hugepages-1Gi", "4"); Map requests = this.deploymentPropertiesResolver.deduceResourceRequests(deploymentRequest.getDeploymentProperties()); assertThat(requests.get("hugepages-1Gi")).isEqualTo(new Quantity("4")); } @Test public void limitHugepages1Gi_deploymentProperty_usesDeploymentProperty() { kubernetesDeployerProperties.getLimits().setHugepages1Gi("4"); deploymentProperties.put("spring.cloud.deployer.kubernetes.limits.hugepages-1Gi", "4"); Map limits = this.deploymentPropertiesResolver.deduceResourceLimits(deploymentRequest.getDeploymentProperties()); assertThat(limits.get("hugepages-1Gi")).isEqualTo(new Quantity("4")); } @Test public void requestHugepages2Mi_deploymentProperty_usesDeploymentProperty() { kubernetesDeployerProperties.getRequests().setHugepages2Mi("40"); deploymentProperties.put("spring.cloud.deployer.kubernetes.requests.hugepages-2Mi", "40"); Map requests = this.deploymentPropertiesResolver.deduceResourceRequests(deploymentRequest.getDeploymentProperties()); assertThat(requests.get("hugepages-2Mi")).isEqualTo(new Quantity("40")); } @Test public void limitHugepages2Mi_deploymentProperty_usesDeploymentProperty() { kubernetesDeployerProperties.getLimits().setHugepages2Mi("40"); deploymentProperties.put("spring.cloud.deployer.kubernetes.limits.hugepages-2Mi", "40"); Map limits = this.deploymentPropertiesResolver.deduceResourceLimits(deploymentRequest.getDeploymentProperties()); assertThat(limits.get("hugepages-2Mi")).isEqualTo(new Quantity("40")); } } ================================================ FILE: spring-cloud-deployer-kubernetes/src/test/resources/dataflow-server-configMapKeyRef.yml ================================================ # spring.cloud.deployer.kubernetes.configMapKeyRefs: configMapKeyRefs: - envVarName: "MY_ENV" configMapName: "myConfigMap" dataKey: "envName" ================================================ FILE: spring-cloud-deployer-kubernetes/src/test/resources/dataflow-server-containerSecurityContext.yml ================================================ # spring.cloud.deployer.kubernetes.containerSecurityContext: containerSecurityContext: allowPrivilegeEscalation: true capabilities: add: - "a" - "b" drop: - "c" privileged: true procMount: DefaultProcMount readOnlyRootFilesystem: true runAsUser: 65534 runAsGroup: 65534 runAsNonRoot: true seLinuxOptions: level: "s0:c123,c456" seccompProfile: type: Localhost localhostProfile: my-profiles/profile-allow.json windowsOptions: gmsaCredentialSpec: specA gmsaCredentialSpecName: specA-name hostProcess: true runAsUserName: userA ================================================ FILE: spring-cloud-deployer-kubernetes/src/test/resources/dataflow-server-nodeAffinity.yml ================================================ # spring.cloud.deployer.kubernetes.nodeAffinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/e2e-az-name operator: In values: - e2e-az1 - e2e-az2 preferredDuringSchedulingIgnoredDuringExecution: - weight: 1 preference: matchExpressions: - key: another-node-label-key operator: In values: - another-node-label-value ================================================ FILE: spring-cloud-deployer-kubernetes/src/test/resources/dataflow-server-podAffinity.yml ================================================ # spring.cloud.deployer.kubernetes.podAffinity: podAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: security operator: In values: - S1 topologyKey: failure-domain.beta.kubernetes.io/zone preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: security operator: In values: - S2 topologyKey: failure-domain.beta.kubernetes.io/zone ================================================ FILE: spring-cloud-deployer-kubernetes/src/test/resources/dataflow-server-podAntiAffinity.yml ================================================ # spring.cloud.deployer.kubernetes.podAntiAffinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: app operator: In values: - store topologyKey: "kubernetes.io/hostname" preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: security operator: In values: - S2 topologyKey: failure-domain.beta.kubernetes.io/zone ================================================ FILE: spring-cloud-deployer-kubernetes/src/test/resources/dataflow-server-podsecuritycontext.yml ================================================ # spring.cloud.deployer.kubernetes.podSecurityContext: podSecurityContext: fsGroup: 65534 fsGroupChangePolicy: Always runAsUser: 65534 runAsGroup: 65534 runAsNonRoot: true seLinuxOptions: level: "s0:c123,c456" seccompProfile: type: Localhost localhostProfile: my-profiles/profile-allow.json supplementalGroups: - 65534 - 65535 sysctls: - name: "kernel.shm_rmid_forced" value: 0 - name: "net.core.somaxconn" value: 1024 windowsOptions: gmsaCredentialSpec: specA gmsaCredentialSpecName: specA-name hostProcess: true runAsUserName: userA ================================================ FILE: spring-cloud-deployer-kubernetes/src/test/resources/dataflow-server-secretKeyRef.yml ================================================ # spring.cloud.deployer.kubernetes.secretKeyRefs: secretKeyRefs: - envVarName: "SECRET_PASSWORD" secretName: "mySecret" dataKey: "myPassword" ================================================ FILE: spring-cloud-deployer-kubernetes/src/test/resources/dataflow-server-tolerations.yml ================================================ # spring.cloud.deployer.kubernetes.tolerations: tolerations: - key: "scdf" operator: "Equal" value: "true" effect: "NoSchedule" ================================================ FILE: spring-cloud-deployer-kubernetes/src/test/resources/dataflow-server.yml ================================================ # spring.cloud.deployer.kubernetes.volumes: volumes: - name: testhostpath hostPath: path: /test/hostPath - name: testpvc persistentVolumeClaim: claimName: testClaim readOnly: true - name: testnfs nfs: server: 10.0.0.1:111 path: /test/nfs ================================================ FILE: spring-cloud-deployer-local/README.adoc ================================================ == Spring Cloud Local Deployer Spring Cloud Local Deployer is an implementation of the Spring Cloud Deployer SPI for use to deploy applications on the same machine. This occurs by this application spawning a new JVM process for the deployed application. NOTE: It's important to note that this deployer spawns new JVMs that are not monitored or maintained by this deployer. No attempts at high availability, fault tolerance, or resiliency are provided by the deployer. Since the deployer SPI expects an underlying platform to provide that level of resiliency, any use of this deployer in a production environment should be accompanied with additional monitoring at the app level (the apps this deployer deploys). This deployer will not be updated to take on those requirements. Therefore the user is encouraged to explore the CloudFoundry and Kubernetes variants as ways to meet them. === Building Build and skip all tests: [source,shell] ---- ./mvnw clean package -DskipTests ---- Or build project and run tests: [source,shell] ---- ./mvnw clean package ---- Run the integration tests in Docker mode: [source,shell] ---- ./mvnw clean install -pl :spring-cloud-deployer-local -Dspring-cloud-deployer-spi-test-use-docker=true -Dspring.cloud.deployer.local.hostname=localhost ---- ================================================ FILE: spring-cloud-deployer-local/pom.xml ================================================ 4.0.0 spring-cloud-deployer-local jar Spring Cloud Deployer Local org.springframework.cloud spring-cloud-deployer-parent 3.0.0-SNAPSHOT .. org.springframework.cloud spring-cloud-deployer-spi org.springframework.cloud spring-cloud-deployer-resource-maven org.springframework.cloud spring-cloud-deployer-resource-docker org.springframework.boot spring-boot-starter org.springframework spring-web com.fasterxml.jackson.core jackson-databind org.springframework.boot spring-boot-starter-validation org.hibernate.validator hibernate-validator-annotation-processor org.springframework.boot spring-boot-configuration-processor true org.springframework.cloud spring-cloud-deployer-spi-test ${project.version} test junit junit org.awaitility awaitility test org.apache.maven.plugins maven-surefire-plugin 3.1.2 1 false 1 methods ================================================ FILE: spring-cloud-deployer-local/src/main/java/org/springframework/cloud/deployer/spi/local/AbstractLocalDeployerSupport.java ================================================ /* * Copyright 2016-2022 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.cloud.deployer.spi.local; import java.io.IOException; import java.net.URL; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; import java.util.Set; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; import org.springframework.cloud.deployer.resource.docker.DockerResource; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; import org.springframework.cloud.deployer.spi.util.RuntimeVersionUtils; import org.springframework.http.ResponseEntity; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.util.Assert; import org.springframework.web.client.RestTemplate; /** * Base class for local app deployer and task launcher providing support for common * functionality. * * @author Janne Valkealahti * @author Mark Fisher * @author Ilayaperumal Gopinathan * @author Thomas Risberg * @author Oleg Zhurakousky * @author Vinicius Carvalho * @author Michael Minella * @author David Turanski * @author Christian Tzolov */ public abstract class AbstractLocalDeployerSupport { protected static Set usedPorts = Collections.newSetFromMap(new LinkedHashMap() { @Override protected boolean removeEldestEntry(Map.Entry eldest) { return size() > 1000; } }); protected final Logger logger = LoggerFactory.getLogger(getClass()); public static final String SPRING_APPLICATION_JSON = "SPRING_APPLICATION_JSON"; public static final int DEFAULT_SERVER_PORT = 8080; private static final String USE_SPRING_APPLICATION_JSON_KEY = LocalDeployerProperties.PREFIX + ".use-spring-application-json"; static final String SERVER_PORT_KEY = "server.port"; static final String SERVER_PORT_KEY_COMMAND_LINE_ARG = "--" + SERVER_PORT_KEY + "="; private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private final LocalDeployerProperties localDeployerProperties; private final RestTemplate restTemplate; private final JavaCommandBuilder javaCommandBuilder; private final DockerCommandBuilder dockerCommandBuilder; /** * Instantiates a new abstract deployer support. * * @param localDeployerProperties the local deployer properties */ public AbstractLocalDeployerSupport(LocalDeployerProperties localDeployerProperties) { Assert.notNull(localDeployerProperties, "LocalDeployerProperties must not be null"); this.localDeployerProperties = localDeployerProperties; this.javaCommandBuilder = new JavaCommandBuilder(localDeployerProperties); this.dockerCommandBuilder = new DockerCommandBuilder(localDeployerProperties.getDocker().getNetwork()); this.restTemplate = buildRestTemplate(localDeployerProperties); } /** * Builds a {@link RestTemplate} used for calling app's shutdown endpoint. If needed can * be overridden from an implementing class. This default implementation sets connection * and read timeouts for {@link SimpleClientHttpRequestFactory} and configures * {@link RestTemplate} to use that factory. If shutdown timeout in properties negative, * returns default {@link RestTemplate} which doesn't use timeouts. * * @param properties the local deployer properties * @return the rest template */ protected RestTemplate buildRestTemplate(LocalDeployerProperties properties) { if (properties != null && properties.getShutdownTimeout() > -1) { SimpleClientHttpRequestFactory clientHttpRequestFactory = new SimpleClientHttpRequestFactory(); clientHttpRequestFactory.setConnectTimeout(properties.getShutdownTimeout() * 1000); clientHttpRequestFactory.setReadTimeout(properties.getShutdownTimeout() * 1000); return new RestTemplate(clientHttpRequestFactory); } // fall back to plain default constructor return new RestTemplate(); } /** * Create the RuntimeEnvironmentInfo. * * @return the local runtime environment info */ protected RuntimeEnvironmentInfo createRuntimeEnvironmentInfo(Class spiClass, Class implementationClass) { return new RuntimeEnvironmentInfo.Builder().spiClass(spiClass) .implementationName(implementationClass.getSimpleName()) .implementationVersion(RuntimeVersionUtils.getVersion(implementationClass)).platformType("Local") .platformApiVersion(System.getProperty("os.name") + " " + System.getProperty("os.version")) .platformClientVersion(System.getProperty("os.version")) .platformHostVersion(System.getProperty("os.version")).build(); } /** * Gets the local deployer properties. * * @return the local deployer properties */ final protected LocalDeployerProperties getLocalDeployerProperties() { return localDeployerProperties; } /** * Builds the process builder. Application properties are expected to be calculated prior * to this method. No additional consolidation of application properties is done while * creating the {@code ProcessBuilder}. * * @param request the request * @param appInstanceEnv the instance environment variables * @return the process builder */ protected ProcessBuilder buildProcessBuilder(AppDeploymentRequest request, Map appInstanceEnv, Optional appInstanceNumber, String deploymentId) { Assert.notNull(request, "AppDeploymentRequest must be set"); Map appPropertiesToUse = formatApplicationProperties(request, appInstanceEnv); if (logger.isInfoEnabled()) { logger.info("Preparing to run an application from {}. This may take some time if the artifact must be " + "downloaded from a remote host.", request.getResource()); } LocalDeployerProperties localDeployerProperties = bindDeploymentProperties(request.getDeploymentProperties()); Optional debugAddressOption = DebugAddress.from(localDeployerProperties, appInstanceNumber.orElse(0)); ProcessBuilder builder = getCommandBuilder(request) .buildExecutionCommand(request, appPropertiesToUse, deploymentId, appInstanceNumber, localDeployerProperties, debugAddressOption); logger.info(String.format("Command to be executed: %s", String.join(" ", builder.command()))); //logger.debug(String.format("Environment Variables to be used : %s", builder.environment().entrySet().stream() // .map(entry -> entry.getKey() + " : " + entry.getValue()).collect(Collectors.joining(", ")))); return builder; } /** * Detects the Command builder by the type of the resource in the requests. * @param request deployment request containing information about the resource to be deployed. * @return Returns a command builder compatible with the type of the resource set in the request. */ protected CommandBuilder getCommandBuilder(AppDeploymentRequest request) { return (request.getResource() instanceof DockerResource) ? this.dockerCommandBuilder : this.javaCommandBuilder; } /** * tweak escaping double quotes needed for windows * @param commands * @return */ public static String[] windowsSupport(String[] commands) { if (LocalDeployerUtils.isWindows()) { for (int i = 0; i < commands.length; i++) { commands[i] = commands[i].replace("\"", "\\\""); } } return commands; } /** * This will merge the deployment properties that were passed in at runtime with the * deployment properties of the Deployer instance. * @param runtimeDeploymentProperties deployment properties passed in at runtime * @return merged deployer properties */ protected LocalDeployerProperties bindDeploymentProperties(Map runtimeDeploymentProperties) { LocalDeployerProperties copyOfDefaultProperties = new LocalDeployerProperties(this.localDeployerProperties); return new Binder(new MapConfigurationPropertySource(runtimeDeploymentProperties)) .bind(LocalDeployerProperties.PREFIX, Bindable.ofInstance(copyOfDefaultProperties)) .orElse(copyOfDefaultProperties); } protected Map formatApplicationProperties(AppDeploymentRequest request, Map appInstanceEnvToUse) { Map applicationPropertiesToUse = new HashMap<>(appInstanceEnvToUse); if (useSpringApplicationJson(request)) { try { // If SPRING_APPLICATION_JSON is found, explode it and merge back into appProperties if (applicationPropertiesToUse.containsKey(SPRING_APPLICATION_JSON)) { applicationPropertiesToUse .putAll(OBJECT_MAPPER.readValue(applicationPropertiesToUse.get(SPRING_APPLICATION_JSON), new TypeReference>() { })); applicationPropertiesToUse.remove(SPRING_APPLICATION_JSON); } } catch (IOException e) { throw new IllegalArgumentException( "Unable to read existing SPRING_APPLICATION_JSON to merge properties", e); } try { String saj = OBJECT_MAPPER.writeValueAsString(applicationPropertiesToUse); applicationPropertiesToUse = new HashMap<>(1); applicationPropertiesToUse.put(SPRING_APPLICATION_JSON, saj); } catch (JsonProcessingException e) { throw new IllegalArgumentException( "Unable to create SPRING_APPLICATION_JSON from application properties", e); } } return applicationPropertiesToUse; } /** * Shut down the {@link Process} backing the application {@link Instance}. If the * application exposes a {@code /shutdown} endpoint, that will be invoked followed by a * wait that will not exceed the number of seconds indicated by * {@link LocalDeployerProperties#getShutdownTimeout()} . If the timeout period is exceeded (or * if the {@code /shutdown} endpoint is not exposed), the process will be shut down via * {@link Process#destroy()}. * * @param instance the application instance to shut down */ protected void shutdownAndWait(Instance instance) { try { int timeout = getLocalDeployerProperties().getShutdownTimeout(); if (timeout > 0) { logger.debug("About to call shutdown endpoint for the instance {}", instance); ResponseEntity response = restTemplate.postForEntity( instance.getBaseUrl() + "/shutdown", null, String.class); logger.debug("Response for shutdown endpoint completed for the instance {} with response {}", instance, response); if (response.getStatusCode().is2xxSuccessful()) { long timeoutTimestamp = System.currentTimeMillis() + (timeout * 1000); while (isAlive(instance.getProcess()) && System.currentTimeMillis() < timeoutTimestamp) { Thread.sleep(1000); } } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (Exception e) { // ignore all other errors as we're going to // destroy process if it's alive } finally { if (isAlive(instance.getProcess())) { logger.debug("About to call destroy the process for the instance {}", instance); instance.getProcess().destroy(); logger.debug("Call completed to destroy the process for the instance {}", instance); } } } // Copy-pasting of JDK8+ isAlive method to retain JDK7 compatibility protected boolean isAlive(Process process) { try { logger.debug("About to call exitValue of the process {}", process); process.exitValue(); logger.debug("Call to exitValue of the process {} complete, return false", process); return false; } catch (IllegalThreadStateException e) { logger.debug("Call to exitValue of the process {} threw exception, return true", process); return true; } } protected boolean useSpringApplicationJson(AppDeploymentRequest request) { return request.getDefinition().getProperties().containsKey(USE_SPRING_APPLICATION_JSON_KEY) || this.localDeployerProperties.isUseSpringApplicationJson(); } // TODO (tzolov): This method has a treacherous side affect! Apart from returning the computed Port it also modifies // the appInstanceEnvVars map! Later is used for down stream app deployment configuration. // As a consequence if you place the calcServerPort in wrong place (for example after the buildProcessBuilder(..) // call then the Port configuration won't be known to the command builder). // Proper solution is to (1) either make the method void and rename it to calcAndSetServerPort or (2) make the // method return the mutated appInstanceEnvVars. Sync with the SCT team because the method is used by the // LocalTaskLauncher (e.g. prod. grade) protected int calcServerPort(AppDeploymentRequest request, boolean useDynamicPort, Map appInstanceEnvVars) { int port = DEFAULT_SERVER_PORT; Integer commandLineArgPort = isServerPortKeyPresentOnArgs(request); if (useDynamicPort) { port = getRandomPort(request); appInstanceEnvVars.put(LocalAppDeployer.SERVER_PORT_KEY, String.valueOf(port)); } else if (commandLineArgPort != null) { port = commandLineArgPort; } else if (request.getDefinition().getProperties().containsKey(LocalAppDeployer.SERVER_PORT_KEY)) { port = Integer.parseInt(request.getDefinition().getProperties().get(LocalAppDeployer.SERVER_PORT_KEY)); } return port; } /** * Will check if {@link LocalDeployerProperties#INHERIT_LOGGING} is set by checking * deployment properties. */ protected boolean shouldInheritLogging(AppDeploymentRequest request) { LocalDeployerProperties bindDeployerProperties = bindDeploymentProperties(request.getDeploymentProperties()); return bindDeployerProperties.isInheritLogging(); } public synchronized int getRandomPort(AppDeploymentRequest request) { Set availPorts = new HashSet<>(); // SocketUtils.findAvailableTcpPorts retries 6 times, add additional retry on top. for (int retryCount = 0; retryCount < 5; retryCount++) { int randomInt = getCommandBuilder(request).getPortSuggestion(localDeployerProperties); try { availPorts = DeployerSocketUtils.findAvailableTcpPorts(5, randomInt, randomInt + 5); try { // Give some time for the system to release up ports that were scanned. Thread.sleep(100); } catch (InterruptedException e) { logger.debug(e.getMessage() + "Retrying to find available ports."); } break; } catch (IllegalStateException e) { logger.debug(e.getMessage() + " Retrying to find available ports."); } } if (availPorts.isEmpty()) { throw new IllegalStateException( "Could not find an available TCP port in the range" + localDeployerProperties.getPortRange()); } int finalPort = -1; logger.debug("Available Ports: " + availPorts); for (Integer freePort : availPorts) { if (!usedPorts.contains(freePort)) { finalPort = freePort; usedPorts.add(finalPort); break; } } if (finalPort == -1) { throw new IllegalStateException( "Could not find a free random port range " + localDeployerProperties.getPortRange()); } logger.debug("Using Port: " + finalPort); return finalPort; } protected Integer isServerPortKeyPresentOnArgs(AppDeploymentRequest request) { return request.getCommandlineArguments().stream() .filter(argument -> argument.startsWith(SERVER_PORT_KEY_COMMAND_LINE_ARG)) .map(argument -> Integer.parseInt(argument.replace(SERVER_PORT_KEY_COMMAND_LINE_ARG, "").trim())) .findFirst() .orElse(null); } protected interface Instance { URL getBaseUrl(); Process getProcess(); } } ================================================ FILE: spring-cloud-deployer-local/src/main/java/org/springframework/cloud/deployer/spi/local/CommandBuilder.java ================================================ /* * Copyright 2016-2021 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.cloud.deployer.spi.local; import java.net.URL; import java.util.Map; import java.util.Optional; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; /** * Strategy interface for Execution Command builder. * * @author Ilayaperumal Gopinathan * @author Thomas Risberg * @author Michael Minella * @author Christian Tzolov */ public interface CommandBuilder { /** * Builds the execution command for an application. * * @param request the request for the application to execute. * @param appInstanceNumber application instance id. * @param appInstanceEnv the env vars tha might be needed when building the execution command. * @param debugAddress application remote debug address. * @return the build command as a string array. */ ProcessBuilder buildExecutionCommand(AppDeploymentRequest request, Map appInstanceEnv, String deployerId, Optional appInstanceNumber, LocalDeployerProperties localDeployerProperties, Optional debugAddress); /** * Compute an App unique URL over apps deployerId, instance index and computed port. * @param deploymentId App deployment id. * @param index App instance index. * @param port App port. * @return Returns app's URL. */ URL getBaseUrl(String deploymentId, int index, int port); /** * Allow the concrete implementation to suggests the target application port. * @param localDeployerProperties * @return Returns a port suggestion. */ int getPortSuggestion(LocalDeployerProperties localDeployerProperties); /** * Computes the JDWP options with the provided suspend and address arguments. * @param suspend suspend debug argument. * @param address debug address. * @return Returns the JDWP options with the provided suspend and address arguments. */ default String getJdwpOptions(String suspend, String address) { return String.format("-agentlib:jdwp=transport=dt_socket,server=y,suspend=%s,address=%s", suspend, address); } } ================================================ FILE: spring-cloud-deployer-local/src/main/java/org/springframework/cloud/deployer/spi/local/DebugAddress.java ================================================ /* * Copyright 2020-2021 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.cloud.deployer.spi.local; import java.util.Optional; import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.StringUtils; /** * Helper for parsing the Debugging address for both the legacy debug-port and the new debug-address properties. * The debug-port supports only Java 8 and is deprecated. The debug-address can be used for jdk 8 as well as * jdk 9 and newer. * When set the debug-address property has precedence over debug-port. * * @author Christian Tzolov */ public class DebugAddress { private static final Pattern HOSTNAME_PATTERN = Pattern.compile("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])$"); private static final Pattern IP_PATTERN = Pattern.compile("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"); private static final Pattern PORT_PATTERN = Pattern.compile("^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$"); public final static Logger logger = LoggerFactory.getLogger(DebugAddress.class); private final String host; private final String port; private final String address; private final String suspend; private DebugAddress(String host, int port, String suspend) { this.host = host; this.port = "" + port; this.suspend = (StringUtils.hasText(suspend)) ? suspend.trim() : "y"; this.address = (StringUtils.hasText(host)) ? String.format("%s:%s", host, port) : this.port; } public String getHost() { return host; } public String getPort() { return port; } public String getSuspend() { return suspend; } public String getAddress() { return this.address; } public static Optional from(LocalDeployerProperties deployerProperties, int instanceNumber) { if (!StringUtils.hasText(deployerProperties.getDebugAddress()) && deployerProperties.getDebugPort() == null) { return Optional.empty(); } String debugHost = null; String debugPort = ("" + deployerProperties.getDebugPort()).trim(); if (StringUtils.hasText(deployerProperties.getDebugAddress())) { String[] addressParts = deployerProperties.getDebugAddress().split(":"); if (addressParts.length == 1) { // JDK 8 only debugPort = addressParts[0].trim(); } else if (addressParts.length == 2) { // JDK 9+ debugHost = addressParts[0].trim(); debugPort = addressParts[1].trim(); if (!("*".equals(debugHost) || HOSTNAME_PATTERN.matcher(debugHost).matches() || IP_PATTERN.matcher(debugHost).matches())) { logger.warn("Invalid debug Host: {}", deployerProperties.getDebugAddress()); return Optional.empty(); } } else { logger.warn("Invalid debug address: {}", deployerProperties.getDebugAddress()); return Optional.empty(); } } if (!PORT_PATTERN.matcher(debugPort).matches()) { logger.warn("Invalid debug port: {}", debugPort); return Optional.empty(); } int portToUse = Integer.parseInt(debugPort) + instanceNumber; return Optional.of(new DebugAddress(debugHost, portToUse, deployerProperties.getDebugSuspend().toString())); } } ================================================ FILE: spring-cloud-deployer-local/src/main/java/org/springframework/cloud/deployer/spi/local/DeployerSocketUtils.java ================================================ /* * Copyright 2016-2022 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.cloud.deployer.spi.local; import java.net.InetAddress; import java.net.ServerSocket; import java.util.Random; import java.util.SortedSet; import java.util.TreeSet; import javax.net.ServerSocketFactory; import org.springframework.util.Assert; /** * Simple utility methods for working with network sockets — for example, for * finding available ports on {@code localhost}. * * This is a replacement for SocketUtils. SocketUtils was introduced in Spring Framework 4.0, * primarily to assist in writing integration tests which start an external server on an available random port. * However, these utilities make no guarantee about the subsequent availability * of a given port and are therefore unreliable. Instead of using SocketUtils to * find an available local port for a server, it is recommended that you rely on a * server's ability to start on a random port that it selects or is assigned by the operating system. * To interact with that server, you should query the server for the port it is currently using. * * @author Sam Brannen * @author Ben Hale * @author Arjen Poutsma * @author Gunnar Hillert * @author Gary Russell * @author Glenn Renfro * @deprecated to be replaced with a more robust mechanism in https://github.com/spring-cloud/spring-cloud-deployer-local/issues/215 */ @Deprecated public class DeployerSocketUtils { /** * The default maximum value for port ranges used when finding an available socket * port. */ static final int PORT_RANGE_MAX = 65535; private static final Random random = new Random(System.nanoTime()); /** * Find an available TCP port randomly selected from the range [{@code minPort}, * {@value #PORT_RANGE_MAX}]. * @param minPort the minimum port number * @return an available TCP port number * @throws IllegalStateException if no available port could be found */ public static int findAvailableTcpPort(int minPort) { return findAvailableTcpPort(minPort, PORT_RANGE_MAX); } /** * Find an available TCP port randomly selected from the range [{@code minPort}, * {@code maxPort}]. * @param minPort the minimum port number * @param maxPort the maximum port number * @return an available TCP port number * @throws IllegalStateException if no available port could be found */ public static int findAvailableTcpPort(int minPort, int maxPort) { return SocketType.TCP.findAvailablePort(minPort, maxPort); } /** * Find the requested number of available TCP ports, each randomly selected from the * range [{@code minPort}, {@code maxPort}]. * @param numRequested the number of available ports to find * @param minPort the minimum port number * @param maxPort the maximum port number * @return a sorted set of available TCP port numbers * @throws IllegalStateException if the requested number of available ports could not * be found */ public static SortedSet findAvailableTcpPorts(int numRequested, int minPort, int maxPort) { return SocketType.TCP.findAvailablePorts(numRequested, minPort, maxPort); } private enum SocketType { TCP { @Override protected boolean isPortAvailable(int port) { try { ServerSocket serverSocket = ServerSocketFactory.getDefault().createServerSocket(port, 1, InetAddress.getByName("localhost")); serverSocket.close(); return true; } catch (Exception ex) { return false; } } }; /** * Determine if the specified port for this {@code SocketType} is currently * available on {@code localhost}. */ protected abstract boolean isPortAvailable(int port); /** * Find a pseudo-random port number within the range [{@code minPort}, * {@code maxPort}]. * @param minPort the minimum port number * @param maxPort the maximum port number * @return a random port number within the specified range */ private int findRandomPort(int minPort, int maxPort) { int portRange = maxPort - minPort; return minPort + random.nextInt(portRange + 1); } /** * Find an available port for this {@code SocketType}, randomly selected from the * range [{@code minPort}, {@code maxPort}]. * @param minPort the minimum port number * @param maxPort the maximum port number * @return an available port number for this socket type * @throws IllegalStateException if no available port could be found */ int findAvailablePort(int minPort, int maxPort) { Assert.isTrue(minPort > 0, "'minPort' must be greater than 0"); Assert.isTrue(maxPort >= minPort, "'maxPort' must be greater than or equal to 'minPort'"); Assert.isTrue(maxPort <= PORT_RANGE_MAX, "'maxPort' must be less than or equal to " + PORT_RANGE_MAX); int portRange = maxPort - minPort; int candidatePort; int searchCounter = 0; do { if (searchCounter > portRange) { throw new IllegalStateException( String.format("Could not find an available %s port in the range [%d, %d] after %d attempts", name(), minPort, maxPort, searchCounter)); } candidatePort = findRandomPort(minPort, maxPort); searchCounter++; } while (!isPortAvailable(candidatePort)); return candidatePort; } /** * Find the requested number of available ports for this {@code SocketType}, each * randomly selected from the range [{@code minPort}, {@code maxPort}]. * @param numRequested the number of available ports to find * @param minPort the minimum port number * @param maxPort the maximum port number * @return a sorted set of available port numbers for this socket type * @throws IllegalStateException if the requested number of available ports could * not be found */ SortedSet findAvailablePorts(int numRequested, int minPort, int maxPort) { Assert.isTrue(minPort > 0, "'minPort' must be greater than 0"); Assert.isTrue(maxPort > minPort, "'maxPort' must be greater than 'minPort'"); Assert.isTrue(maxPort <= PORT_RANGE_MAX, "'maxPort' must be less than or equal to " + PORT_RANGE_MAX); Assert.isTrue(numRequested > 0, "'numRequested' must be greater than 0"); Assert.isTrue((maxPort - minPort) >= numRequested, "'numRequested' must not be greater than 'maxPort' - 'minPort'"); SortedSet availablePorts = new TreeSet<>(); int attemptCount = 0; while ((++attemptCount <= numRequested + 100) && availablePorts.size() < numRequested) { availablePorts.add(findAvailablePort(minPort, maxPort)); } if (availablePorts.size() != numRequested) { throw new IllegalStateException( String.format("Could not find %d available %s ports in the range [%d, %d]", numRequested, name(), minPort, maxPort)); } return availablePorts; } } } ================================================ FILE: spring-cloud-deployer-local/src/main/java/org/springframework/cloud/deployer/spi/local/DockerCommandBuilder.java ================================================ /* * Copyright 2016-2021 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.cloud.deployer.spi.local; import java.io.IOException; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.ThreadLocalRandom; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.deployer.resource.docker.DockerResource; import org.springframework.cloud.deployer.spi.app.AppDeployer; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.util.StringUtils; /** * Command builder used to craft the command used when running apps inside docker containers. * * @author Ilayaperumal Gopinathan * @author Eric Bottard * @author Henryk Konsek * @author Thomas Risberg * @author Michael Minella * @author Christian Tzolov */ public class DockerCommandBuilder implements CommandBuilder { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); /** * Name of the deployment property used to specify the container name pattern to use. */ public static final String DOCKER_CONTAINER_NAME_KEY = AppDeployer.PREFIX + "docker.container.name"; private final Logger logger = LoggerFactory.getLogger(getClass()); private final String dockerNetwork; public DockerCommandBuilder(String dockerNetwork) { this.dockerNetwork = dockerNetwork; } @Override public int getPortSuggestion(LocalDeployerProperties localDeployerProperties) { return ThreadLocalRandom.current().nextInt(localDeployerProperties.getDocker().getPortRange().getLow(), localDeployerProperties.getDocker().getPortRange().getHigh()); } @Override public URL getBaseUrl(String deploymentId, int index, int port) { try { return new URL("http", String.format("%s-%d", deploymentId, index), port, ""); } catch (Exception e) { throw new IllegalArgumentException(e); } } @Override public ProcessBuilder buildExecutionCommand(AppDeploymentRequest request, Map appInstanceEnv, String deployerId, Optional appInstanceNumber, LocalDeployerProperties localDeployerProperties, Optional debugAddressOption) { appInstanceEnv.put("deployerId", deployerId); List commands = addDockerOptions(request, appInstanceEnv, appInstanceNumber, localDeployerProperties, debugAddressOption); commands.addAll(request.getCommandlineArguments()); logger.debug("Docker Command = " + commands); return new ProcessBuilder(Arrays.asList(AbstractLocalDeployerSupport.windowsSupport(commands.toArray(new String[0])))); } private List addDockerOptions(AppDeploymentRequest request, Map appInstanceEnv, Optional appInstanceNumber, LocalDeployerProperties localDeployerProperties, Optional debugAddressOption) { List commands = new ArrayList<>(); commands.add("docker"); commands.add("run"); if (StringUtils.hasText(this.dockerNetwork)) { commands.add("--network"); commands.add(this.dockerNetwork); } if (localDeployerProperties.getDocker().isDeleteContainerOnExit()) { commands.add("--rm"); } // Add env vars for (String env : appInstanceEnv.keySet()) { commands.add("-e"); commands.add(String.format("%s=%s", env, appInstanceEnv.get(env))); } debugAddressOption.ifPresent(debugAddress -> { String debugCommand = getJdwpOptions(debugAddress.getSuspend(), debugAddress.getAddress()); logger.debug("Deploying app with Debug Command = [{}]", debugCommand); commands.add("-e"); commands.add("JAVA_TOOL_OPTIONS=" + debugCommand); commands.add("-p"); commands.add(String.format("%s:%s", debugAddress.getPort(), debugAddress.getPort())); }); String port = getPort(appInstanceEnv); if (StringUtils.hasText(port)) { commands.add("-p"); commands.add(String.format("%s:%s", port, port)); } applyPortMappings(commands,localDeployerProperties); applyVolumeMountings(commands,localDeployerProperties); applyAdditionalHosts(commands,localDeployerProperties); if (request.getDeploymentProperties().containsKey(DOCKER_CONTAINER_NAME_KEY)) { if (appInstanceNumber.isPresent()) { commands.add(String.format("--name=%s-%d", request.getDeploymentProperties().get(DOCKER_CONTAINER_NAME_KEY), appInstanceNumber.get())); } else { commands.add(String.format("--name=%s", request.getDeploymentProperties().get(DOCKER_CONTAINER_NAME_KEY))); } } else { String group = request.getDeploymentProperties().get(AppDeployer.GROUP_PROPERTY_KEY); if (StringUtils.hasText(group)) { String deploymentId = String.format("%s.%s", group, request.getDefinition().getName()); int index = appInstanceNumber.orElse(0); commands.add(String.format("--name=%s-%d", deploymentId, index)); } } DockerResource dockerResource = (DockerResource) request.getResource(); try { String dockerImageURI = dockerResource.getURI().toString(); commands.add(dockerImageURI.substring("docker:".length())); } catch (IOException e) { throw new IllegalStateException(e); } return commands; } private void applyVolumeMountings(List commands, LocalDeployerProperties localDeployerProperties) { String volumeMounts = localDeployerProperties.getDocker().getVolumeMounts(); if (StringUtils.hasText(volumeMounts)) { for (String v : parseMapping(volumeMounts)) { commands.add("-v"); commands.add(v); } } } private void applyAdditionalHosts(List commands, LocalDeployerProperties localDeployerProperties) { String additionalHosts = localDeployerProperties.getDocker().getAdditionalHosts(); if (StringUtils.hasText(additionalHosts)) { for (String v : parseMapping(additionalHosts)) { commands.add("--add-host"); commands.add(v); } } } private void applyPortMappings(List commands, LocalDeployerProperties properties) { String portMappings = properties.getDocker().getPortMappings(); if (StringUtils.hasText(portMappings)) { for (String p : parseMapping(portMappings)) { commands.add("-p"); commands.add(p); } } } private String getPort(Map appInstanceEnv) { if (appInstanceEnv.containsKey(AbstractLocalDeployerSupport.SPRING_APPLICATION_JSON)) { try { HashMap flatProperties = new HashMap<>((OBJECT_MAPPER.readValue( appInstanceEnv.get(AbstractLocalDeployerSupport.SPRING_APPLICATION_JSON), new TypeReference>() {}))); if (flatProperties.containsKey(LocalAppDeployer.SERVER_PORT_KEY)) { return flatProperties.get(LocalAppDeployer.SERVER_PORT_KEY); } // fall back to appInstanceEnv. } catch (IOException e) { throw new IllegalArgumentException("Unable to determine server port from SPRING_APPLICATION_JSON"); } } return appInstanceEnv.get(LocalAppDeployer.SERVER_PORT_KEY); } private List parseMapping(String map) { Supplier> stream = () -> Arrays.stream(map.split(",")); stream.get().filter(s -> !s.contains(":")).forEach(s -> logger.warn("incomplete mapping {} will be ignored", s)); return stream.get().filter(s -> s.contains(":")).collect(Collectors.toList()); } } ================================================ FILE: spring-cloud-deployer-local/src/main/java/org/springframework/cloud/deployer/spi/local/HttpProbeExecutor.java ================================================ /* * Copyright 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.cloud.deployer.spi.local; import java.net.URI; import java.net.URL; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.deployer.spi.local.LocalDeployerProperties.HttpProbe; import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.DefaultUriBuilderFactory; /** * Simple probe executor using rest endpoints. * * @author Janne Valkealahti * */ public class HttpProbeExecutor { private static final Logger logger = LoggerFactory.getLogger(HttpProbeExecutor.class); private final RestTemplate restTemplate; private final URI uri; public HttpProbeExecutor(RestTemplate restTemplate, URI uri) { this.restTemplate = restTemplate; this.uri = uri; } public static HttpProbeExecutor from(URL baseUrl, HttpProbe httpProbe) { URI base = null; try { base = baseUrl.toURI(); } catch (Exception e) { } if (httpProbe == null || httpProbe.getPath() == null || base == null) { return null; } DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(base.toString()); uriBuilderFactory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.NONE); URI uri = uriBuilderFactory.builder().path("{path}").build(httpProbe.getPath()); return new HttpProbeExecutor(new RestTemplate(), uri); } public boolean probe() { try { logger.info("Probing for {}", this.uri); ResponseEntity response = restTemplate.getForEntity(uri, String.class); HttpStatusCode statusCode = response.getStatusCode(); boolean ok = statusCode.is2xxSuccessful(); if (!ok) { logger.info("Probe for {} returned {}:{}", this.uri, statusCode, response.getBody()); } return ok; } catch (Exception e) { logger.trace("Probe error for {}", this.uri, e); } return false; } } ================================================ FILE: spring-cloud-deployer-local/src/main/java/org/springframework/cloud/deployer/spi/local/JavaCommandBuilder.java ================================================ /* * Copyright 2016-2021 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.cloud.deployer.spi.local; import java.io.IOException; import java.net.Inet4Address; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.concurrent.ThreadLocalRandom; import java.util.regex.Pattern; import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; import org.springframework.cloud.deployer.spi.app.AppDeployer; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.util.ByteSizeUtils; import org.springframework.core.io.Resource; import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** * @author Mark Pollack * @author Ilayaperumal Gopinathan * @author Thomas Risberg * @author Michael Minella */ public class JavaCommandBuilder implements CommandBuilder { private final Logger logger = LoggerFactory.getLogger(getClass()); private final LocalDeployerProperties properties; public JavaCommandBuilder(LocalDeployerProperties properties) { this.properties = properties; } @Override public int getPortSuggestion(LocalDeployerProperties localDeployerProperties) { return ThreadLocalRandom.current().nextInt(localDeployerProperties.getPortRange().getLow(), localDeployerProperties.getPortRange().getHigh()); } @Override public URL getBaseUrl(String deploymentId, int index, int port) { try { return new URL("http", Inet4Address.getLocalHost().getHostAddress(), port, ""); } catch (Exception e) { throw new IllegalArgumentException(e); } } @Override public ProcessBuilder buildExecutionCommand( AppDeploymentRequest request, Map appInstanceEnv, String deployerId, Optional appInstanceNumber, LocalDeployerProperties localDeployerProperties, Optional debugAddressOption ) { ArrayList commands = new ArrayList<>(); Map deploymentProperties = request.getDeploymentProperties(); String bootVersion = deploymentProperties.get("spring.cloud.deployer.bootVersion"); if (!StringUtils.hasLength(bootVersion)) { Optional bootArg = request.getCommandlineArguments() .stream() .filter(s -> s.startsWith("--spring.cloud.deployer.bootVersion=") || s.startsWith("--spring.cloud.deployer.boot-version=")) .findFirst(); if (bootArg.isPresent()) { int indexEq = bootArg.get().indexOf('='); Assert.isTrue(indexEq > 0, () -> "Expected = in argument:" + bootArg.get()); bootVersion = bootArg.get().substring(indexEq + 1).trim(); } } if (bootVersion == null) { bootVersion = "3"; // safe to launch boot 2 with Java 17 } commands.add(bindDeploymentProperties(deploymentProperties).getJavaCommand(bootVersion)); debugAddressOption.ifPresent(debugAddress -> commands.add(getJdwpOptions(debugAddress.getSuspend(), debugAddress.getAddress()))); // Add Java System Properties (ie -Dmy.prop=val) before main class or -jar addJavaOptions(commands, deploymentProperties, properties); addJavaExecutionOptions(commands, request); commands.addAll(request.getCommandlineArguments()); logger.debug("Java Command = {}", commands); ProcessBuilder builder = new ProcessBuilder(AbstractLocalDeployerSupport.windowsSupport(commands.toArray(new String[0]))); // retain before we put in app related variables. retainEnvVars(builder.environment(), localDeployerProperties); builder.environment().putAll(appInstanceEnv); return builder; } /** * Retain the environment variable strings in the provided set indicated by * {@link LocalDeployerProperties#getEnvVarsToInherit}. * This assumes that the provided set can be modified. * * @param vars set of environment variable strings * @param localDeployerProperties local deployer properties */ protected void retainEnvVars(Map vars, LocalDeployerProperties localDeployerProperties) { List patterns = new ArrayList<>(Arrays.asList(localDeployerProperties.getEnvVarsToInherit())); for (Iterator> iterator = vars.entrySet().iterator(); iterator.hasNext(); ) { Entry entry = iterator.next(); String var = entry.getKey(); boolean retain = false; for (String pattern : patterns) { if (Pattern.matches(pattern, var)) { retain = true; break; } } if (!retain) { iterator.remove(); } } } protected void addJavaOptions( List commands, Map deploymentProperties, LocalDeployerProperties localDeployerProperties ) { String memory = null; if (deploymentProperties.containsKey(AppDeployer.MEMORY_PROPERTY_KEY)) { memory = "-Xmx" + ByteSizeUtils.parseToMebibytes(deploymentProperties.get(AppDeployer.MEMORY_PROPERTY_KEY)) + "m"; } String javaOptsString = bindDeploymentProperties(deploymentProperties).getJavaOpts(); if (javaOptsString == null && memory != null) { commands.add(memory); } if (javaOptsString != null) { String[] javaOpts = StringUtils.tokenizeToStringArray(javaOptsString, " "); boolean noJavaMemoryOption = Stream.of(javaOpts).noneMatch(s -> s.startsWith("-Xmx")); if (noJavaMemoryOption && memory != null) { commands.add(memory); } commands.addAll(Arrays.asList(javaOpts)); } else { if (localDeployerProperties.getJavaOpts() != null) { String[] javaOpts = StringUtils.tokenizeToStringArray(localDeployerProperties.getJavaOpts(), " "); commands.addAll(Arrays.asList(javaOpts)); } } } protected void addJavaExecutionOptions(List commands, AppDeploymentRequest request) { commands.add("-jar"); Resource resource = request.getResource(); try { commands.add(resource.getFile().getAbsolutePath()); } catch (IOException e) { throw new IllegalStateException(e); } } /** * This will merge the deployment properties that were passed in at runtime with the deployment properties * of the Deployer instance. * * @param runtimeDeploymentProperties deployment properties passed in at runtime * @return merged deployer properties */ protected LocalDeployerProperties bindDeploymentProperties(Map runtimeDeploymentProperties) { LocalDeployerProperties copyOfDefaultProperties = new LocalDeployerProperties(this.properties); return new Binder(new MapConfigurationPropertySource(runtimeDeploymentProperties)) .bind(LocalDeployerProperties.PREFIX, Bindable.ofInstance(copyOfDefaultProperties)) .orElse(copyOfDefaultProperties); } } ================================================ FILE: spring-cloud-deployer-local/src/main/java/org/springframework/cloud/deployer/spi/local/LocalActuatorTemplate.java ================================================ /* * Copyright 2022 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.cloud.deployer.spi.local; import org.springframework.cloud.deployer.spi.app.AbstractActuatorTemplate; import org.springframework.cloud.deployer.spi.app.AppAdmin; import org.springframework.cloud.deployer.spi.app.AppDeployer; import org.springframework.cloud.deployer.spi.app.AppInstanceStatus; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; /** * @author David Turanski */ public class LocalActuatorTemplate extends AbstractActuatorTemplate { public LocalActuatorTemplate(RestTemplate restTemplate, AppDeployer appDeployer, AppAdmin appAdmin) { super(restTemplate, appDeployer, appAdmin); } @Override protected String actuatorUrlForInstance(AppInstanceStatus appInstanceStatus) { return UriComponentsBuilder.fromUriString(appInstanceStatus.getAttributes().get("url")) .path("/actuator").toUriString(); } } ================================================ FILE: spring-cloud-deployer-local/src/main/java/org/springframework/cloud/deployer/spi/local/LocalAppDeployer.java ================================================ /* * Copyright 2016-2022 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.cloud.deployer.spi.local; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.lang.ProcessBuilder.Redirect; import java.lang.reflect.Field; import java.net.HttpURLConnection; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; import jakarta.annotation.PreDestroy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.deployer.spi.app.AppDeployer; import org.springframework.cloud.deployer.spi.app.AppInstanceStatus; import org.springframework.cloud.deployer.spi.app.AppScaleRequest; import org.springframework.cloud.deployer.spi.app.AppStatus; import org.springframework.cloud.deployer.spi.app.DeploymentState; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; import org.springframework.cloud.deployer.spi.local.LocalDeployerProperties.HttpProbe; import org.springframework.util.Assert; import org.springframework.util.FileCopyUtils; import org.springframework.util.StringUtils; /** * An {@link AppDeployer} implementation that spins off a new JVM process per app * instance. * * @author Eric Bottard * @author Marius Bogoevici * @author Mark Fisher * @author Ilayaperumal Gopinathan * @author Janne Valkealahti * @author Patrick Peralta * @author Thomas Risberg * @author Oleg Zhurakousky * @author Michael Minella * @author Glenn Renfro * @author Christian Tzolov * @author David Turanski */ public class LocalAppDeployer extends AbstractLocalDeployerSupport implements AppDeployer { private static final Logger logger = LoggerFactory.getLogger(LocalAppDeployer.class); private static final String JMX_DEFAULT_DOMAIN_KEY = "spring.jmx.default-domain"; private static final String ENDPOINTS_SHUTDOWN_ENABLED_KEY = "endpoints.shutdown.enabled"; private final Map running = new ConcurrentHashMap<>(); /** * Instantiates a new local app deployer. * * @param properties the properties */ public LocalAppDeployer(LocalDeployerProperties properties) { super(properties); } /** * Returns the process exit value. We explicitly use Integer instead of int to indicate * that if {@code NULL} is returned, the process is still running. * @param process the process * @return the process exit value or {@code NULL} if process is still alive */ private static Integer getProcessExitValue(Process process) { try { return process.exitValue(); } catch (IllegalThreadStateException e) { // process is still alive return null; } } /** * Gets the local process pid if available. This should be a safe workaround for unix * systems where reflection can be used to get pid. More reliable way should land with * jdk9. * * @param p the process * @return the local process pid */ private static synchronized int getLocalProcessPid(Process p) { int pid = 0; try { if (p.getClass().getName().equals("java.lang.UNIXProcess")) { Field f = p.getClass().getDeclaredField("pid"); f.setAccessible(true); pid = f.getInt(p); f.setAccessible(false); } } catch (Exception e) { pid = 0; } return pid; } @Override public String deploy(AppDeploymentRequest request) { String group = request.getDeploymentProperties().get(GROUP_PROPERTY_KEY); String deploymentId = String.format("%s.%s", group, request.getDefinition().getName()); validateStatus(deploymentId, DeploymentState.unknown); List processes = new ArrayList<>(); running.put(deploymentId, new AppInstancesHolder(processes, request)); try { Path workDir = createWorkingDir(request.getDeploymentProperties(), deploymentId); String countProperty = request.getDeploymentProperties().get(COUNT_PROPERTY_KEY); int count = (StringUtils.hasText(countProperty)) ? Integer.parseInt(countProperty) : 1; for (int index = 0; index < count; index++) { processes.add(deployApp(request, workDir, group, deploymentId, index, request.getDeploymentProperties())); } } catch (IOException e) { throw new RuntimeException("Exception trying to deploy " + request, e); } return deploymentId; } @Override public void scale(AppScaleRequest appScaleRequest) { validateStatus(appScaleRequest.getDeploymentId(), DeploymentState.deployed); AppInstancesHolder holder = running.get(appScaleRequest.getDeploymentId()); List instances = holder != null ? holder.instances : null; if (instances == null) { throw new IllegalStateException( "Can't find existing instances for deploymentId " + appScaleRequest.getDeploymentId()); } AppDeploymentRequest request = holder.request; String group = request.getDeploymentProperties().get(GROUP_PROPERTY_KEY); String deploymentId = String.format("%s.%s", group, request.getDefinition().getName()); try { Path workDir = createWorkingDir(request.getDeploymentProperties(), deploymentId); int deltaCount = appScaleRequest.getCount() - instances.size(); int targetCount = instances.size() + deltaCount; if (deltaCount > 0) { for (int index = instances.size(); index < targetCount; index++) { instances.add(deployApp(request, workDir, group, deploymentId, index, request.getDeploymentProperties())); } } else if (deltaCount < 0) { List processes = new ArrayList<>(); for (int index = instances.size() - 1; index >= targetCount; index--) { processes.add(instances.remove(index)); } for (AppInstance instance : processes) { if (isAlive(instance.getProcess())) { logger.info("Un-deploying app with deploymentId {} instance {}.", deploymentId, instance.getInstanceNumber()); shutdownAndWait(instance); } } } } catch (IOException e) { throw new RuntimeException("Exception trying to deploy " + request, e); } } @Override public void undeploy(String id) { AppInstancesHolder holder = running.get(id); List processes = holder != null ? holder.instances : null; if (processes != null) { for (AppInstance instance : processes) { if (isAlive(instance.getProcess())) { logger.info("Un-deploying app with deploymentId {} instance {}.", id, instance.getInstanceNumber()); shutdownAndWait(instance); } } running.remove(id); } else { throw new IllegalStateException(String.format("App with deploymentId %s is not in a deployed state.", id)); } } @Override public AppStatus status(String id) { AppInstancesHolder holder = running.get(id); List instances = holder != null ? holder.instances : null; AppStatus.Builder builder = AppStatus.of(id); if (instances != null) { for (AppInstance instance : instances) { builder.with(instance); } } return builder.build(); } @Override public String getLog(String id) { AppInstancesHolder holder = running.get(id); List instances = holder != null ? holder.instances : null; StringBuilder stringBuilder = new StringBuilder(); if (instances != null) { for (AppInstance instance : instances) { String stderr = instance.getStdErr(); if (StringUtils.hasText(stderr)) { stringBuilder.append("stderr:\n"); stringBuilder.append(stderr); } String stdout = instance.getStdOut(); if (StringUtils.hasText(stdout)) { stringBuilder.append("stdout:\n"); stringBuilder.append(stdout); } } } return stringBuilder.toString(); } @Override public RuntimeEnvironmentInfo environmentInfo() { return super.createRuntimeEnvironmentInfo(AppDeployer.class, this.getClass()); } @PreDestroy public void shutdown() { for (String deploymentId : running.keySet()) { undeploy(deploymentId); } } private AppInstance deployApp(AppDeploymentRequest request, Path workDir, String group, String deploymentId, int index, Map deploymentProperties) throws IOException { LocalDeployerProperties localDeployerPropertiesToUse = bindDeploymentProperties(deploymentProperties); // consolidatedAppProperties is a Map of all application properties to be used by // the app being launched. These values should end up as environment variables // either explicitly or as a SPRING_APPLICATION_JSON value. HashMap consolidatedAppProperties = new HashMap<>(request.getDefinition().getProperties()); consolidatedAppProperties.put(JMX_DEFAULT_DOMAIN_KEY, deploymentId); if (!request.getDefinition().getProperties().containsKey(ENDPOINTS_SHUTDOWN_ENABLED_KEY)) { consolidatedAppProperties.put(ENDPOINTS_SHUTDOWN_ENABLED_KEY, "true"); } consolidatedAppProperties.put("endpoints.jmx.unique-names", "true"); if (group != null) { consolidatedAppProperties.put("spring.cloud.application.group", group); } // This Map is the consolidated application properties *for the instance* // to be deployed in this iteration Map appInstanceEnv = new HashMap<>(consolidatedAppProperties); // we only set 'normal' style props reflecting what we set for env format // for cross reference to work inside SAJ. // looks like for now we can't remove these env style formats as i.e. // DeployerIntegrationTestProperties in tests really assume 'INSTANCE_INDEX' and // this might be indication that we can't yet fully remove those. String guid = toGuid(deploymentId, index); if (useSpringApplicationJson(request)) { appInstanceEnv.put("instance.index", Integer.toString(index)); appInstanceEnv.put("spring.cloud.stream.instanceIndex", Integer.toString(index)); appInstanceEnv.put("spring.application.index", Integer.toString(index)); appInstanceEnv.put("spring.cloud.application.guid", guid); } else { appInstanceEnv.put("INSTANCE_INDEX", Integer.toString(index)); appInstanceEnv.put("SPRING_APPLICATION_INDEX", Integer.toString(index)); appInstanceEnv.put("SPRING_CLOUD_APPLICATION_GUID", guid); } this.getLocalDeployerProperties().getAppAdmin().addCredentialsToAppEnvironmentAsProperties(appInstanceEnv); boolean useDynamicPort = !request.getDefinition().getProperties().containsKey(SERVER_PORT_KEY); // WATCH OUT: The calcServerPort sets the computed port in the appInstanceEnv#SERVER_PORT_KEY. // Later is implicitly passed to and used inside the command builder. Therefore the calcServerPort() method // must always be called before the buildProcessBuilder(..)! int port = calcServerPort(request, useDynamicPort, appInstanceEnv); ProcessBuilder builder = buildProcessBuilder(request, appInstanceEnv, Optional.of(index), deploymentId) .inheritIO(); builder.directory(workDir.toFile()); URL baseUrl = (StringUtils.hasText(localDeployerPropertiesToUse.getHostname())) ? new URL("http", localDeployerPropertiesToUse.getHostname(), port, "") : getCommandBuilder(request).getBaseUrl(deploymentId, index, port); AppInstance instance = new AppInstance(deploymentId, index, port, baseUrl, localDeployerPropertiesToUse.getStartupProbe(), localDeployerPropertiesToUse.getHealthProbe()); if (this.shouldInheritLogging(request)) { instance.start(builder, workDir); logger.info("Deploying app with deploymentId {} instance {}.\n Logs will be inherited.", deploymentId, index); } else { instance.start(builder, workDir, getLocalDeployerProperties().isDeleteFilesOnExit()); logger.info("Deploying app with deploymentId {} instance {}.\n Logs will be in {}", deploymentId, index, workDir); } return instance; } private Path createWorkingDir(Map deploymentProperties, String deploymentId) throws IOException { LocalDeployerProperties localDeployerPropertiesToUse = bindDeploymentProperties(deploymentProperties); Path workingDirectoryRoot = Files.createDirectories(localDeployerPropertiesToUse.getWorkingDirectoriesRoot()); Path workDir = Files.createDirectories(workingDirectoryRoot.resolve(Long.toString(System.currentTimeMillis())).resolve(deploymentId)); if (getLocalDeployerProperties().isDeleteFilesOnExit()) { workDir.toFile().deleteOnExit(); } return workDir; } private void validateStatus(String deploymentId, DeploymentState expectedState) { DeploymentState state = status(deploymentId).getState(); Assert.state(expectedState.equals(state), String.format("App with deploymentId [%s] with state [%s] doesn't match expected state [%s]", deploymentId, state, expectedState)); } private static String toGuid(String deploymentId, int appIndex) { return String.format("%s-%s", deploymentId, appIndex); } private static class AppInstance implements Instance, AppInstanceStatus { private final String deploymentId; private final int instanceNumber; private final URL baseUrl; private final Map attributes = new TreeMap<>(); private int pid; private Process process; private File workFile; private File stdout; private File stderr; private int port; private HttpProbeExecutor startupProbeExecutor; private HttpProbeExecutor healthProbeExecutor; private boolean startupProbeOk; private AppInstance(String deploymentId, int instanceNumber, int port, URL baseUrl, HttpProbe startupProbe, HttpProbe healthProbe) { this.deploymentId = deploymentId; this.instanceNumber = instanceNumber; this.port = port; this.baseUrl = baseUrl; this.attributes.put("port", Integer.toString(port)); this.attributes.put("guid", toGuid(deploymentId, instanceNumber)); this.attributes.put("url", baseUrl.toString()); this.startupProbeExecutor = HttpProbeExecutor.from(baseUrl, startupProbe); this.healthProbeExecutor = HttpProbeExecutor.from(baseUrl, healthProbe); } @Override public String getId() { return deploymentId + "-" + instanceNumber; } @Override public URL getBaseUrl() { return this.baseUrl; } @Override public Process getProcess() { return this.process; } @Override public String toString() { return String.format("%s [%s]", getId(), getState()); } @Override public DeploymentState getState() { Integer exit = getProcessExitValue(process); // TODO: consider using exit code mapper concept from batch if (exit != null) { return DeploymentState.failed; } if (port < 1) { // Support case where user passed in zero or negative port indicating fully random // chosen by OS. In this case we simply return deployed if process is up. // Also we can't even try http check as we would not know port to connect to. return DeploymentState.deployed; } // do startup probe first and only until we're deployed if (startupProbeExecutor != null && !startupProbeOk) { boolean ok = startupProbeExecutor.probe(); if (ok) { startupProbeOk = true; return DeploymentState.deployed; } else { return DeploymentState.deploying; } } // now deployed, checking health probe if (healthProbeExecutor != null) { return healthProbeExecutor.probe() ? DeploymentState.deployed : DeploymentState.failed; } try { HttpURLConnection urlConnection = (HttpURLConnection) baseUrl.openConnection(); urlConnection.setConnectTimeout(100); urlConnection.connect(); urlConnection.disconnect(); return DeploymentState.deployed; } catch (IOException e) { return DeploymentState.deploying; } } public String getStdOut() { try { return FileCopyUtils.copyToString(new InputStreamReader(Files.newInputStream(this.stdout.toPath()))); } catch (IOException e) { return "Log retrieval returned " + e.getMessage(); } } public String getStdErr() { try { return FileCopyUtils.copyToString(new InputStreamReader(Files.newInputStream(this.stderr.toPath()))); } catch (IOException e) { return "Log retrieval returned " + e.getMessage(); } } public int getInstanceNumber() { return instanceNumber; } @Override public Map getAttributes() { return this.attributes; } /** * Will start the process while redirecting 'out' and 'err' streams to the 'out' and 'err' * streams of this process. */ private void start(ProcessBuilder builder, Path workDir) throws IOException { if (logger.isDebugEnabled()) { logger.debug("Local App Deployer Commands: " + String.join(",", builder.command()) + ", Environment: " + builder.environment()); } this.workFile = workDir.toFile(); this.attributes.put("working.dir", this.workFile.getAbsolutePath()); this.process = builder.start(); this.pid = getLocalProcessPid(this.process); if (pid > 0) { // add pid if we got it attributes.put("pid", Integer.toString(pid)); } } private void start(ProcessBuilder builder, Path workDir, boolean deleteOnExit) throws IOException { String workDirPath = workDir.toFile().getAbsolutePath(); this.stdout = Files.createFile(Paths.get(workDirPath, "stdout_" + instanceNumber + ".log")).toFile(); this.attributes.put("stdout", stdout.getAbsolutePath()); this.stderr = Files.createFile(Paths.get(workDirPath, "stderr_" + instanceNumber + ".log")).toFile(); this.attributes.put("stderr", stderr.getAbsolutePath()); if (deleteOnExit) { this.stdout.deleteOnExit(); this.stderr.deleteOnExit(); } builder.redirectOutput(Redirect.to(this.stdout)); builder.redirectError(Redirect.to(this.stderr)); this.start(builder, workDir); } } private static class AppInstancesHolder { final List instances; final AppDeploymentRequest request; public AppInstancesHolder(List instances, AppDeploymentRequest request) { this.instances = instances; this.request = request; } } } ================================================ FILE: spring-cloud-deployer-local/src/main/java/org/springframework/cloud/deployer/spi/local/LocalDeployerAutoConfiguration.java ================================================ /* * Copyright 2016-2022 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.cloud.deployer.spi.local; import org.springframework.boot.autoconfigure.AutoConfigureOrder; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.deployer.spi.app.ActuatorOperations; import org.springframework.cloud.deployer.spi.app.AppDeployer; import org.springframework.cloud.deployer.spi.task.TaskLauncher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.web.client.RestTemplate; /** * Creates a {@link LocalAppDeployer} and {@link LocalTaskLauncher} * * @author Mark Fisher * @author David Turanski */ @Configuration @EnableConfigurationProperties(LocalDeployerProperties.class) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) public class LocalDeployerAutoConfiguration { @Bean @ConditionalOnMissingBean(AppDeployer.class) public AppDeployer appDeployer(LocalDeployerProperties properties) { return new LocalAppDeployer(properties); } @Bean @ConditionalOnMissingBean(TaskLauncher.class) public TaskLauncher taskLauncher(LocalDeployerProperties properties) { return new LocalTaskLauncher(properties); } @Bean @ConditionalOnMissingBean RestTemplate actuatorRestTemplate() { return new RestTemplate(); } @Bean @ConditionalOnMissingBean(ActuatorOperations.class) ActuatorOperations actuatorOperations(RestTemplate actuatorRestTemplate, AppDeployer appDeployer, LocalDeployerProperties properties) { return new LocalActuatorTemplate(actuatorRestTemplate, appDeployer, properties.getAppAdmin()); } } ================================================ FILE: spring-cloud-deployer-local/src/main/java/org/springframework/cloud/deployer/spi/local/LocalDeployerProperties.java ================================================ /* * Copyright 2015-2022 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.cloud.deployer.spi.local; import java.io.File; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; import jakarta.validation.constraints.Min; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.cloud.deployer.spi.app.AppAdmin; import org.springframework.core.style.ToStringCreator; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.validation.annotation.Validated; /** * Configuration properties for the local deployer. * * @author Eric Bottard * @author Mark Fisher * @author Ilayaperumal Gopinathan * @author Oleg Zhurakousky * @author Vinicius Carvalho * @author David Turanski * @author Christian Tzolov */ @Validated @ConfigurationProperties(prefix = LocalDeployerProperties.PREFIX) public class LocalDeployerProperties { /** * Top level prefix for local deployer configuration properties. */ public static final String PREFIX = "spring.cloud.deployer.local"; /** * Deployer property allowing logging to be redirected to the output stream of * the process that triggered child process. Could be set per the entire * deployment (i.e. {@literal deployer.*.local.inheritLogging=true}) or * per individual application (i.e. * {@literal deployer..local.inheritLogging=true}). */ public static final String INHERIT_LOGGING = PREFIX + ".inherit-logging"; /** * Remote debugging property allowing one to specify port for the remote debug * session. Must be set per individual application (i.e. * {@literal deployer..local.debugPort=9999}). * * @deprecated This is only JDK 8 compatible. Use the {@link #DEBUG_ADDRESS} instead for supporting all JDKs. */ public static final String DEBUG_PORT = PREFIX + ".debug-port"; /** * Remote debugging property allowing one to specify the address for the remote debug * session. On Java versions 1.8 or older use the port format. On Java versions 1.9 or greater use the * host:port format. The host could default to *. May be set for individual applications (i.e. * {@literal deployer..local.debugAddress=*:9999}). */ public static final String DEBUG_ADDRESS = PREFIX + ".debug-address"; /** * Remote debugging property allowing one to specify if the startup of the * application should be suspended until remote debug session is established. * Values must be either 'y' or 'n'. Must be set per individual application * (i.e. {@literal deployer..local.debugSuspend=y}). */ public static final String DEBUG_SUSPEND = PREFIX + ".debug-suspend"; private static final Logger logger = LoggerFactory.getLogger(LocalDeployerProperties.class); private static final String JAVA_COMMAND = LocalDeployerUtils.isWindows() ? "java.exe" : "java"; // looks like some windows systems uses 'Path' but process builder give it as 'PATH' private static final String[] ENV_VARS_TO_INHERIT_DEFAULTS_WIN = {"TMP", "TEMP", "PATH", "Path", AbstractLocalDeployerSupport.SPRING_APPLICATION_JSON}; private static final String[] ENV_VARS_TO_INHERIT_DEFAULTS_OTHER = {"TMP", "LANG", "LANGUAGE", "LC_.*", "PATH", AbstractLocalDeployerSupport.SPRING_APPLICATION_JSON}; /** * Directory in which all created processes will run and create log files. */ private Path workingDirectoriesRoot = new File(System.getProperty("java.io.tmpdir")).toPath(); /** * Whether to delete created files and directories on JVM exit. */ private boolean deleteFilesOnExit = true; /** * Array of regular expression patterns for environment variables that should be * passed to launched applications. */ private String[] envVarsToInherit = LocalDeployerUtils.isWindows() ? ENV_VARS_TO_INHERIT_DEFAULTS_WIN : ENV_VARS_TO_INHERIT_DEFAULTS_OTHER; /** * The command to run java. */ private String javaCmd = JAVA_COMMAND; /** * Maximum number of seconds to wait for application shutdown. via the * {@code /shutdown} endpoint. A timeout value of 0 specifies an infinite * timeout. Default is 30 seconds. */ @Min(-1) private int shutdownTimeout = 30; /** * The Java Options to pass to the JVM, e.g -Dtest=foo */ private String javaOpts; /** * Flag to indicate whether application properties are passed as command line * args or in a SPRING_APPLICATION_JSON environment variable. Default value is * {@code true}. */ private boolean useSpringApplicationJson = true; private final PortRange portRange = new PortRange(); /** * The maximum concurrent tasks allowed for this platform instance. */ @Min(1) private int maximumConcurrentTasks = 20; /** * Set remote debugging port for JDK 8 runtimes. * * @deprecated Use the {@link #debugAddress} instead! */ private Integer debugPort; /** * Debugging address for the remote clients to attache to. Addresses have the format ":" where * is the host name and is the socket port number at which it attaches or listens. * For JDK 8 or earlier, the address consists of the port number alone (the host name is implicit to localhost). * Example addresses for JDK version 9 or higher: *:20075, 192.168.178.10:20075. * Example addresses for JDK version 8 or earlier: 20075. */ private String debugAddress; public enum DebugSuspendType {y, n} /** * Suspend defines whether the JVM should suspend and wait for a debugger to attach or not */ private DebugSuspendType debugSuspend = DebugSuspendType.y; private boolean inheritLogging; private final Docker docker = new Docker(); /** * (optional) hostname to use when computing the URL of the deployed application. * By default the {@link CommandBuilder} implementations decide how to build the hostname. */ private String hostname; private Map javaHomePath = new HashMap<>(); private AppAdmin appAdmin = new AppAdmin(); public LocalDeployerProperties() { String javaHome = System.getProperty("java.home"); if (javaHome != null) { javaHomePath.put("2", javaHome); javaHomePath.put("3", javaHome); } } public LocalDeployerProperties(LocalDeployerProperties from) { this.debugPort = from.getDebugPort(); this.debugAddress = from.getDebugAddress(); this.debugSuspend = from.getDebugSuspend(); this.deleteFilesOnExit = from.isDeleteFilesOnExit(); this.docker.network = from.getDocker().getNetwork(); this.docker.deleteContainerOnExit = from.getDocker().isDeleteContainerOnExit(); this.docker.portRange = from.getDocker().getPortRange(); this.envVarsToInherit = new String[from.getEnvVarsToInherit().length]; System.arraycopy(from.getEnvVarsToInherit(), 0, this.envVarsToInherit, 0, from.getEnvVarsToInherit().length); this.inheritLogging = from.isInheritLogging(); this.javaCmd = from.getJavaCmd(); this.javaOpts = from.getJavaOpts(); this.maximumConcurrentTasks = from.getMaximumConcurrentTasks(); this.portRange.high = from.getPortRange().getHigh(); this.portRange.low = from.getPortRange().getLow(); this.shutdownTimeout = from.getShutdownTimeout(); this.useSpringApplicationJson = from.isUseSpringApplicationJson(); this.workingDirectoriesRoot = Paths.get(from.getWorkingDirectoriesRoot().toUri()); this.hostname = from.getHostname(); this.appAdmin = from.getAppAdmin(); this.javaHomePath = from.getJavaHomePath().entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); this.startupProbe = from.getStartupProbe(); this.healthProbe = from.getHealthProbe(); } public static class PortRange { /** * Lower bound for computing applications's random port. */ private int low = 20000; /** * Upper bound for computing applications's random port. */ private int high = 61000; public int getLow() { return low; } public void setLow(int low) { this.low = low; } public int getHigh() { return high; } public void setHigh(int high) { this.high = high; } @Override public String toString() { return "{ low=" + low + ", high=" + high + '}'; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + high; result = prime * result + low; return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } PortRange other = (PortRange) obj; if (high != other.high) { return false; } return low == other.low; } } public static class Docker { /** * Container network */ private String network = "bridge"; /** * Whether to delete the container on container exit. */ private boolean deleteContainerOnExit = true; /** * Allow the Docker command builder use its own port range. */ private PortRange portRange = new PortRange(); /** * Set port mappings for container */ private String portMappings; /** * Set volume mappings */ private String volumeMounts; /** * Set additional hosts */ private String additionalHosts; public PortRange getPortRange() { return portRange; } public String getNetwork() { return network; } public void setNetwork(String network) { this.network = network; } public boolean isDeleteContainerOnExit() { return deleteContainerOnExit; } public void setDeleteContainerOnExit(boolean deleteContainerOnExit) { this.deleteContainerOnExit = deleteContainerOnExit; } public String getPortMappings() { return portMappings; } public void setPortMappings(String portMappings) { this.portMappings = portMappings; } public String getVolumeMounts() { return volumeMounts; } public void setVolumeMounts(String volumeMounts) { this.volumeMounts = volumeMounts; } public String getAdditionalHosts() { return additionalHosts; } public void setAdditionalHosts(String additionalHosts) { this.additionalHosts = additionalHosts; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((network == null) ? 0 : network.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } Docker other = (Docker) obj; if (network == null) { return other.network == null; } else return network.equals(other.network); } } public Docker getDocker() { return docker; } public String getHostname() { return hostname; } public void setHostname(String hostname) { this.hostname = hostname; } public Integer getDebugPort() { return debugPort; } public DebugSuspendType getDebugSuspend() { return debugSuspend; } public void setDebugSuspend(DebugSuspendType debugSuspend) { this.debugSuspend = debugSuspend; } public void setDebugPort(Integer debugPort) { logger.warn("The debugPort is deprecated! It supports only pre Java 9 environments. " + "Please use the debugAddress property instead!"); this.debugPort = debugPort; } public String getDebugAddress() { return debugAddress; } public void setDebugAddress(String debugAddress) { this.debugAddress = debugAddress; } public boolean isInheritLogging() { return inheritLogging; } public void setInheritLogging(boolean inheritLogging) { this.inheritLogging = inheritLogging; } public String getJavaCommand(String bootVersion) { if (!StringUtils.hasText(javaCmd) || this.javaCmd.equals(JAVA_COMMAND)) { return deduceJavaCommand(bootVersion); } return javaCmd; } public Map getJavaHomePath() { return javaHomePath; } public void setJavaHomePath(Map javaHomePath) { this.javaHomePath = javaHomePath; } public String getJavaCmd() { return javaCmd; } public void setJavaCmd(String javaCmd) { this.javaCmd = javaCmd; } public Path getWorkingDirectoriesRoot() { return workingDirectoriesRoot; } public void setWorkingDirectoriesRoot(String workingDirectoriesRoot) { this.workingDirectoriesRoot = Paths.get(workingDirectoriesRoot); } public void setWorkingDirectoriesRoot(Path workingDirectoriesRoot) { this.workingDirectoriesRoot = workingDirectoriesRoot; } public boolean isDeleteFilesOnExit() { return deleteFilesOnExit; } public void setDeleteFilesOnExit(boolean deleteFilesOnExit) { this.deleteFilesOnExit = deleteFilesOnExit; } public String[] getEnvVarsToInherit() { return envVarsToInherit; } public void setEnvVarsToInherit(String[] envVarsToInherit) { this.envVarsToInherit = envVarsToInherit; } public int getShutdownTimeout() { return shutdownTimeout; } public LocalDeployerProperties setShutdownTimeout(int shutdownTimeout) { this.shutdownTimeout = shutdownTimeout; return this; } public String getJavaOpts() { return javaOpts; } public void setJavaOpts(String javaOpts) { this.javaOpts = javaOpts; } public boolean isUseSpringApplicationJson() { return useSpringApplicationJson; } public void setUseSpringApplicationJson(boolean useSpringApplicationJson) { this.useSpringApplicationJson = useSpringApplicationJson; } public PortRange getPortRange() { return portRange; } public int getMaximumConcurrentTasks() { return maximumConcurrentTasks; } public void setMaximumConcurrentTasks(int maximumConcurrentTasks) { this.maximumConcurrentTasks = maximumConcurrentTasks; } private HttpProbe startupProbe = new HttpProbe(); private HttpProbe healthProbe = new HttpProbe(); public HttpProbe getStartupProbe() { return startupProbe; } public void setStartupProbe(HttpProbe startupProbe) { this.startupProbe = startupProbe; } public HttpProbe getHealthProbe() { return healthProbe; } public void setHealthProbe(HttpProbe healthProbe) { this.healthProbe = healthProbe; } public AppAdmin getAppAdmin() { return appAdmin; } public void setAppAdmin(AppAdmin appAdmin) { this.appAdmin = appAdmin; } public static class HttpProbe { /** * Path to check as a probe */ private String path; public String getPath() { return path; } public void setPath(String path) { this.path = path; } } private String deduceJavaCommand(String bootVersion) { String javaExecutablePath = JAVA_COMMAND; String javaHome = getJavaHome(bootVersion); if (StringUtils.hasText(javaHome)) { File javaExecutable = new File(javaHome, "bin" + File.separator + javaExecutablePath); Assert.isTrue(javaExecutable.exists(), () -> "Java executable'" + javaExecutable + "'discovered via 'java.home' system property '" + this.javaHomePath + "' does not exist."); Assert.isTrue(javaExecutable.canExecute(), () -> "Java executable'" + javaExecutable + "'discovered via 'java.home' system property '" + this.javaHomePath + "' is not executable."); javaExecutablePath = javaExecutable.getAbsolutePath(); } else { logger.warn("System property 'java.home' and 'spring.cloud.deployer.local.{}.javaHomePath' is not set. " + "Defaulting to the java executable path as " + JAVA_COMMAND + " assuming it's in PATH.", bootVersion); } return javaExecutablePath; } private String getJavaHome(String bootVersion) { String path = javaHomePath.get(bootVersion); return path == null ? System.getProperty("java.home") : path; } @Override public String toString() { return new ToStringCreator(this).append("workingDirectoriesRoot", this.workingDirectoriesRoot) .append("javaOpts", this.javaOpts).append("envVarsToInherit", this.envVarsToInherit).toString(); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; LocalDeployerProperties that = (LocalDeployerProperties) o; if (deleteFilesOnExit != that.deleteFilesOnExit) return false; if (shutdownTimeout != that.shutdownTimeout) return false; if (useSpringApplicationJson != that.useSpringApplicationJson) return false; if (maximumConcurrentTasks != that.maximumConcurrentTasks) return false; if (inheritLogging != that.inheritLogging) return false; if (!Objects.equals(workingDirectoriesRoot, that.workingDirectoriesRoot)) return false; // Probably incorrect - comparing Object[] arrays with Arrays.equals if (!Arrays.equals(envVarsToInherit, that.envVarsToInherit)) return false; if (!Objects.equals(javaCmd, that.javaCmd)) return false; if (!Objects.equals(javaOpts, that.javaOpts)) return false; if (!portRange.equals(that.portRange)) return false; if (!Objects.equals(debugPort, that.debugPort)) return false; if (!Objects.equals(debugAddress, that.debugAddress)) return false; if (debugSuspend != that.debugSuspend) return false; if (!docker.equals(that.docker)) return false; if (!Objects.equals(hostname, that.hostname)) return false; if (!Objects.equals(javaHomePath, that.javaHomePath)) return false; if (!Objects.equals(appAdmin, that.appAdmin)) return false; if (!Objects.equals(startupProbe, that.startupProbe)) return false; return Objects.equals(healthProbe, that.healthProbe); } @Override public int hashCode() { int result = workingDirectoriesRoot != null ? workingDirectoriesRoot.hashCode() : 0; result = 31 * result + (deleteFilesOnExit ? 1 : 0); result = 31 * result + Arrays.hashCode(envVarsToInherit); result = 31 * result + (javaCmd != null ? javaCmd.hashCode() : 0); result = 31 * result + shutdownTimeout; result = 31 * result + (javaOpts != null ? javaOpts.hashCode() : 0); result = 31 * result + (useSpringApplicationJson ? 1 : 0); result = 31 * result + portRange.hashCode(); result = 31 * result + maximumConcurrentTasks; result = 31 * result + (debugPort != null ? debugPort.hashCode() : 0); result = 31 * result + (debugAddress != null ? debugAddress.hashCode() : 0); result = 31 * result + (debugSuspend != null ? debugSuspend.hashCode() : 0); result = 31 * result + (inheritLogging ? 1 : 0); result = 31 * result + docker.hashCode(); result = 31 * result + (hostname != null ? hostname.hashCode() : 0); result = 31 * result + (javaHomePath != null ? javaHomePath.hashCode() : 0); result = 31 * result + (appAdmin != null ? appAdmin.hashCode() : 0); result = 31 * result + (startupProbe != null ? startupProbe.hashCode() : 0); result = 31 * result + (healthProbe != null ? healthProbe.hashCode() : 0); return result; } } ================================================ FILE: spring-cloud-deployer-local/src/main/java/org/springframework/cloud/deployer/spi/local/LocalDeployerUtils.java ================================================ /* * Copyright 2017-2018 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.cloud.deployer.spi.local; import java.util.Locale; /** * Deployer utility functions. * * @author Janne Valkealahti * @author Michael Minella * */ public class LocalDeployerUtils { /** * Checks if jvm is running on windows. * * @return true if windows detected */ protected static boolean isWindows() { String osName = System.getProperty("os.name"); return osName != null && osName.toLowerCase(Locale.ROOT).startsWith("windows"); } } ================================================ FILE: spring-cloud-deployer-local/src/main/java/org/springframework/cloud/deployer/spi/local/LocalTaskLauncher.java ================================================ /* * Copyright 2016-2022 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.cloud.deployer.spi.local; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.Inet4Address; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import jakarta.annotation.PreDestroy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; import org.springframework.cloud.deployer.spi.task.LaunchState; import org.springframework.cloud.deployer.spi.task.TaskLauncher; import org.springframework.cloud.deployer.spi.task.TaskStatus; import org.springframework.util.FileCopyUtils; import org.springframework.util.StringUtils; /** * A {@link TaskLauncher} implementation that spins off a new JVM process per task launch. * * @author Eric Bottard * @author Marius Bogoevici * @author Mark Fisher * @author Janne Valkealahti * @author Thomas Risberg * @author Oleg Zhurakousky * @author Michael Minella * @author Christian Tzolov * @author David Turanski * @author Glenn Renfro * @author Ben Blinebury */ public class LocalTaskLauncher extends AbstractLocalDeployerSupport implements TaskLauncher { private static final Logger logger = LoggerFactory.getLogger(LocalTaskLauncher.class); private static final String JMX_DEFAULT_DOMAIN_KEY = "spring.jmx.default-domain"; private final Map running = new ConcurrentHashMap<>(); private final Map> taskInstanceHistory = new ConcurrentHashMap<>(); /** * Instantiates a new local task launcher. * * @param properties the properties */ public LocalTaskLauncher(LocalDeployerProperties properties) { super(properties); } @Override public String launch(AppDeploymentRequest request) { if (this.maxConcurrentExecutionsReached()) { throw new IllegalStateException( String.format("Cannot launch task %s. The maximum concurrent task executions is at its limit [%d].", request.getDefinition().getName(), this.getMaximumConcurrentTasks()) ); } String taskLaunchId = request.getDefinition().getName() + "-" + UUID.randomUUID().toString(); pruneTaskInstanceHistory(request.getDefinition().getName(), taskLaunchId); HashMap args = new HashMap<>(); args.putAll(request.getDefinition().getProperties()); args.put(JMX_DEFAULT_DOMAIN_KEY, taskLaunchId); args.put("endpoints.shutdown.enabled", "true"); args.put("endpoints.jmx.unique-names", "true"); try { Path workingDirectory = createWorkingDirectory(request.getDeploymentProperties(), taskLaunchId); boolean useDynamicPort = isDynamicPort(request); int port = calcServerPort(request, useDynamicPort, args); ProcessBuilder builder = buildProcessBuilder(request, args, Optional.empty(), taskLaunchId).inheritIO(); TaskInstance instance = new TaskInstance(builder, workingDirectory, port); if (this.shouldInheritLogging(request)) { instance.start(builder); logger.info("launching task {}\n Logs will be inherited.", taskLaunchId); } else { instance.start(builder, getLocalDeployerProperties().isDeleteFilesOnExit()); logger.info("launching task {}\n Logs will be in {}", taskLaunchId, workingDirectory); } running.put(taskLaunchId, instance); } catch (IOException e) { throw new RuntimeException("Exception trying to launch " + request, e); } return taskLaunchId; } private void pruneTaskInstanceHistory(String taskDefinitionName, String taskLaunchId) { CopyOnWriteArrayList oldTaskInstanceIds = taskInstanceHistory.get(taskDefinitionName); if (oldTaskInstanceIds == null) { oldTaskInstanceIds = new CopyOnWriteArrayList<>(); taskInstanceHistory.put(taskDefinitionName, oldTaskInstanceIds); } for (String oldTaskInstanceId : oldTaskInstanceIds) { TaskInstance oldTaskInstance = running.get(oldTaskInstanceId); if (oldTaskInstance != null && oldTaskInstance.getState() != LaunchState.running && oldTaskInstance.getState() != LaunchState.launching) { running.remove(oldTaskInstanceId); oldTaskInstanceIds.remove(oldTaskInstanceId); } else { oldTaskInstanceIds.remove(oldTaskInstanceId); } } oldTaskInstanceIds.add(taskLaunchId); } private boolean isDynamicPort(AppDeploymentRequest request) { boolean isServerPortKeyonArgs = isServerPortKeyPresentOnArgs(request) != null; return !request.getDefinition().getProperties().containsKey(SERVER_PORT_KEY) && !isServerPortKeyonArgs; } @Override public void cancel(String id) { TaskInstance instance = running.get(id); if (instance != null) { instance.cancelled = true; if (isAlive(instance.getProcess())) { shutdownAndWait(instance); } } } @Override public TaskStatus status(String id) { TaskInstance instance = running.get(id); if (instance != null) { return new TaskStatus(id, instance.getState(), instance.getAttributes()); } return new TaskStatus(id, LaunchState.unknown, null); } @Override public String getLog(String id) { TaskInstance instance = running.get(id); if (instance != null) { StringBuilder stringBuilder = new StringBuilder(); String stderr = instance.getStdErr(); if (StringUtils.hasText(stderr)) { stringBuilder.append("stderr:\n"); stringBuilder.append(stderr); } String stdout = instance.getStdOut(); if (StringUtils.hasText(stdout)) { stringBuilder.append("stdout:\n"); stringBuilder.append(stdout); } return stringBuilder.toString(); } else { return "Log could not be retrieved as the task instance is not running."; } } @Override public void cleanup(String id) { } @Override public void destroy(String appName) { } @Override public RuntimeEnvironmentInfo environmentInfo() { return super.createRuntimeEnvironmentInfo(TaskLauncher.class, this.getClass()); } @Override public int getMaximumConcurrentTasks() { return getLocalDeployerProperties().getMaximumConcurrentTasks(); } @Override public int getRunningTaskExecutionCount() { int runningExecutionCount = 0; for (TaskInstance taskInstance: running.values()) { if (taskInstance.getProcess().isAlive()) { runningExecutionCount++; } } return runningExecutionCount; } private synchronized boolean maxConcurrentExecutionsReached() { return getRunningTaskExecutionCount() >= getMaximumConcurrentTasks(); } @PreDestroy public void shutdown() throws Exception { for (String taskLaunchId : running.keySet()) { cancel(taskLaunchId); } taskInstanceHistory.clear(); } private Path createWorkingDirectory(Map deploymentProperties, String taskLaunchId) throws IOException { LocalDeployerProperties localDeployerPropertiesToUse = bindDeploymentProperties(deploymentProperties); Path workingDirectoryRoot = Files.isSymbolicLink(localDeployerPropertiesToUse.getWorkingDirectoriesRoot()) ? Files.readSymbolicLink(localDeployerPropertiesToUse.getWorkingDirectoriesRoot()) : Files.createDirectories(localDeployerPropertiesToUse.getWorkingDirectoriesRoot()); Path workingDirectory = Files.createDirectories(workingDirectoryRoot.resolve(Long.toString(System.nanoTime())).resolve(taskLaunchId)); if (localDeployerPropertiesToUse.isDeleteFilesOnExit()) { workingDirectory.toFile().deleteOnExit(); } return workingDirectory; } private static class TaskInstance implements Instance { private Process process; private final Path workDir; private File stdout; private File stderr; private final URL baseUrl; private boolean cancelled; private TaskInstance(ProcessBuilder builder, Path workDir, int port) throws IOException { builder.directory(workDir.toFile()); this.workDir = workDir; this.baseUrl = new URL("http", Inet4Address.getLocalHost().getHostAddress(), port, ""); if (logger.isDebugEnabled()) { logger.debug("Local Task Launcher Commands: " + String.join(",", builder.command()) + ", Environment: " + builder.environment()); } } @Override public URL getBaseUrl() { return this.baseUrl; } @Override public Process getProcess() { return this.process; } public LaunchState getState() { if (cancelled) { return LaunchState.cancelled; } Integer exit = getProcessExitValue(process); // TODO: consider using exit code mapper concept from batch if (exit != null) { if (exit == 0) { return LaunchState.complete; } else { return LaunchState.failed; } } try { HttpURLConnection urlConnection = (HttpURLConnection) baseUrl.openConnection(); urlConnection.setConnectTimeout(100); urlConnection.connect(); urlConnection.disconnect(); return LaunchState.running; } catch (IOException e) { return LaunchState.launching; } } public String getStdOut() { try { return FileCopyUtils.copyToString(new InputStreamReader(new FileInputStream(this.stdout))); } catch (IOException e) { return "Log retrieval returned " + e.getMessage(); } } public String getStdErr() { try { return FileCopyUtils.copyToString(new InputStreamReader(new FileInputStream(this.stderr))); } catch (IOException e) { return "Log retrieval returned " + e.getMessage(); } } /** * Will start the process while redirecting 'out' and 'err' streams to the 'out' and 'err' * streams of this process. */ private void start(ProcessBuilder builder) throws IOException { if (logger.isDebugEnabled()) { logger.debug("Local Task Launcher Commands: " + String.join(",", builder.command()) + ", Environment: " + builder.environment()); } this.process = builder.start(); } private void start(ProcessBuilder builder, boolean deleteOnExit) throws IOException { String workDirPath = workDir.toFile().getAbsolutePath(); this.stdout = Files.createFile(Paths.get(workDirPath, "stdout.log")).toFile(); this.stderr = Files.createFile(Paths.get(workDirPath, "stderr.log")).toFile(); builder.redirectOutput(this.stdout); builder.redirectError(this.stderr); this.process = builder.start(); if(deleteOnExit) { this.stdout.deleteOnExit(); this.stderr.deleteOnExit(); } } private Map getAttributes() { HashMap result = new HashMap<>(); result.put("working.dir", workDir.toFile().getAbsolutePath()); if(this.stdout != null) { result.put("stdout", stdout.getAbsolutePath()); } if(this.stderr != null) { result.put("stderr", stderr.getAbsolutePath()); } result.put("url", baseUrl.toString()); return result; } } /** * Returns the process exit value. We explicitly use Integer instead of int * to indicate that if {@code NULL} is returned, the process is still running. * * @param process the process * @return the process exit value or {@code NULL} if process is still alive */ private static Integer getProcessExitValue(Process process) { try { return process.exitValue(); } catch (IllegalThreadStateException e) { // process is still alive return null; } } } ================================================ FILE: spring-cloud-deployer-local/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ org.springframework.cloud.deployer.spi.local.LocalDeployerAutoConfiguration ================================================ FILE: spring-cloud-deployer-local/src/test/java/org/springframework/cloud/deployer/spi/local/DebugAddressTests.java ================================================ /* * Copyright 2020-2021 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.cloud.deployer.spi.local; import java.util.Optional; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** * @author Christian Tzolov */ public class DebugAddressTests { @Test public void testDebugEmptyConfiguration() { Optional debugAddress = DebugAddress.from(new LocalDeployerProperties(), 0); assertThat(debugAddress.isPresent()).isFalse(); } @Test public void testDebugPort() { LocalDeployerProperties properties = new LocalDeployerProperties(); properties.setDebugPort(20075); Optional debugAddress = DebugAddress.from(properties, 0); assertThat(debugAddress.isPresent()).isTrue(); assertThat(debugAddress.get().getHost()).isNull(); assertThat(debugAddress.get().getPort()).isEqualTo("20075"); assertThat(debugAddress.get().getAddress()).isEqualTo("20075"); } @Test public void testDebugPortWithInstance() { LocalDeployerProperties properties = new LocalDeployerProperties(); properties.setDebugPort(20075); Optional debugAddress = DebugAddress.from(properties, 100); assertThat(debugAddress.isPresent()).isTrue(); assertThat(debugAddress.get().getHost()).isNull(); assertThat(debugAddress.get().getPort()).isEqualTo("20175"); assertThat(debugAddress.get().getAddress()).isEqualTo("20175"); } @Test public void testDebugPortInvalidValue() { LocalDeployerProperties properties = new LocalDeployerProperties(); properties.setDebugPort(-666); Optional debugAddress = DebugAddress.from(properties, 0); assertThat(debugAddress.isPresent()).isFalse(); } @Test public void testDebugAddressPortOnly() { LocalDeployerProperties properties = new LocalDeployerProperties(); properties.setDebugAddress("20075"); Optional debugAddress = DebugAddress.from(properties, 100); assertThat(debugAddress.isPresent()).isTrue(); assertThat(debugAddress.get().getHost()).isNull(); assertThat(debugAddress.get().getPort()).isEqualTo("20175"); assertThat(debugAddress.get().getAddress()).isEqualTo("20175"); } @Test public void testDebugAddressWildcardHost() { LocalDeployerProperties properties = new LocalDeployerProperties(); properties.setDebugAddress("*:20075"); Optional debugAddress = DebugAddress.from(properties, 100); assertThat(debugAddress.isPresent()).isTrue(); assertThat(debugAddress.get().getHost()).isEqualTo("*"); assertThat(debugAddress.get().getPort()).isEqualTo("20175"); assertThat(debugAddress.get().getAddress()).isEqualTo("*:20175"); } @Test public void testDebugAddressWithIP() { LocalDeployerProperties properties = new LocalDeployerProperties(); properties.setDebugAddress("127.0.0.1:20075"); Optional debugAddress = DebugAddress.from(properties, 100); assertThat(debugAddress.isPresent()).isTrue(); assertThat(debugAddress.get().getHost()).isEqualTo("127.0.0.1"); assertThat(debugAddress.get().getPort()).isEqualTo("20175"); assertThat(debugAddress.get().getAddress()).isEqualTo("127.0.0.1:20175"); } @Test public void testDebugAddressWithHostname() { LocalDeployerProperties properties = new LocalDeployerProperties(); properties.setDebugAddress("localhost:20075"); Optional debugAddress = DebugAddress.from(properties, 100); assertThat(debugAddress.isPresent()).isTrue(); assertThat(debugAddress.get().getHost()).isEqualTo("localhost"); assertThat(debugAddress.get().getPort()).isEqualTo("20175"); assertThat(debugAddress.get().getAddress()).isEqualTo("localhost:20175"); } @Test public void testDebugAddressWithInvalidIP() { LocalDeployerProperties properties = new LocalDeployerProperties(); properties.setDebugAddress("127.0.:20075"); Optional debugAddress = DebugAddress.from(properties, 100); assertThat(debugAddress.isPresent()).isFalse(); } } ================================================ FILE: spring-cloud-deployer-local/src/test/java/org/springframework/cloud/deployer/spi/local/DeployerSocketUtilsTests.java ================================================ /* * Copyright 2016-2022 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.cloud.deployer.spi.local; import java.util.SortedSet; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** * Unit tests for {@link DeployerSocketUtils}. * * @author Sam Brannen * @author Gary Russell * @author Glenn Renfro */ class DeployerSocketUtilsTests { @Test void findAvailableTcpPortWithZeroMinPort() { assertThatIllegalArgumentException().isThrownBy(() -> DeployerSocketUtils.findAvailableTcpPort(0)); } @Test void findAvailableTcpPortWithNegativeMinPort() { assertThatIllegalArgumentException().isThrownBy(() -> DeployerSocketUtils.findAvailableTcpPort(-500)); } @Test void findAvailableTcpPortWithMin() { int port = DeployerSocketUtils.findAvailableTcpPort(50000); assertPortInRange(port, 50000, DeployerSocketUtils.PORT_RANGE_MAX); } @Test void find4AvailableTcpPortsInRange() { findAvailableTcpPorts(4, 30000, 35000); } @Test void find50AvailableTcpPortsInRange() { findAvailableTcpPorts(50, 40000, 45000); } @Test void findAvailableTcpPortsWithRequestedNumberGreaterThanSizeOfRange() { assertThatIllegalArgumentException().isThrownBy(() -> findAvailableTcpPorts(50, 45000, 45010)); } // Helpers private void findAvailableTcpPorts(int numRequested, int minPort, int maxPort) { SortedSet ports = DeployerSocketUtils.findAvailableTcpPorts(numRequested, minPort, maxPort); assertAvailablePorts(ports, numRequested, minPort, maxPort); } private void assertPortInRange(int port, int minPort, int maxPort) { assertThat(port >= minPort).as("port [" + port + "] >= " + minPort).isTrue(); assertThat(port <= maxPort).as("port [" + port + "] <= " + maxPort).isTrue(); } private void assertAvailablePorts(SortedSet ports, int numRequested, int minPort, int maxPort) { assertThat(ports.size()).as("number of ports requested").isEqualTo(numRequested); for (int port : ports) { assertPortInRange(port, minPort, maxPort); } } } ================================================ FILE: spring-cloud-deployer-local/src/test/java/org/springframework/cloud/deployer/spi/local/DockerCommandBuilderTests.java ================================================ /* * Copyright 2017-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.cloud.deployer.spi.local; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Optional; import org.junit.jupiter.api.Test; import org.springframework.cloud.deployer.resource.docker.DockerResource; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.core.io.Resource; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link DockerCommandBuilder}. * * @author Eric Bottard * @author Christian Tzolov */ public class DockerCommandBuilderTests { @Test public void testContainerName() { AppDefinition appDefinition = new AppDefinition("foo", null); Resource resource = new DockerResource("foo/bar"); Map deploymentProperties = Collections.singletonMap(DockerCommandBuilder.DOCKER_CONTAINER_NAME_KEY, "gogo"); AppDeploymentRequest request = new AppDeploymentRequest(appDefinition, resource, deploymentProperties); ProcessBuilder builder = new DockerCommandBuilder(null) .buildExecutionCommand(request, new HashMap<>(), "deployerId", Optional.of(1), new LocalDeployerProperties(), Optional.empty()); assertThat(builder.command()).containsAnyElementsOf(Arrays.asList("docker", "run", "--rm", "--name=gogo-1", "foo/bar", "deployerId=deployerId")); } @Test public void testContainerNameWithDockerNetwork() { AppDefinition appDefinition = new AppDefinition("foo", null); Resource resource = new DockerResource("foo/bar"); Map deploymentProperties = Collections.singletonMap(DockerCommandBuilder.DOCKER_CONTAINER_NAME_KEY, "gogo"); AppDeploymentRequest request = new AppDeploymentRequest(appDefinition, resource, deploymentProperties); ProcessBuilder builder = new DockerCommandBuilder("scdf_default") .buildExecutionCommand(request, new HashMap<>(), "deployerId", Optional.of(1), new LocalDeployerProperties(), Optional.empty()); assertThat(builder.command()).containsAnyElementsOf(Arrays.asList("docker", "run", "--network", "scdf_default", "--rm", "--name=gogo-1", "foo/bar")); } @Test public void testContainerNameWithDockerNetworkAndKeepContainers() { AppDefinition appDefinition = new AppDefinition("foo", null); Resource resource = new DockerResource("foo/bar"); Map deploymentProperties = Collections.singletonMap(DockerCommandBuilder.DOCKER_CONTAINER_NAME_KEY, "gogo"); AppDeploymentRequest request = new AppDeploymentRequest(appDefinition, resource, deploymentProperties); LocalDeployerProperties localDeployerProperties = new LocalDeployerProperties(); localDeployerProperties.getDocker().setDeleteContainerOnExit(false); ProcessBuilder builder = new DockerCommandBuilder("scdf_default") .buildExecutionCommand(request, new HashMap<>(), "deployerId", Optional.of(1), localDeployerProperties, Optional.empty()); assertThat(builder.command()).containsAnyElementsOf(Arrays.asList("docker", "run", "--network", "scdf_default", "--name=gogo-1", "foo/bar")); assertThat(builder.command()).doesNotContain("--rm"); } @Test public void testUseLocalDeployerPropertiesToKeepStoppedContainer() { AppDefinition appDefinition = new AppDefinition("foo", null); Resource resource = new DockerResource("foo/bar"); Map deploymentProperties = Collections.singletonMap(DockerCommandBuilder.DOCKER_CONTAINER_NAME_KEY, "gogo"); AppDeploymentRequest request = new AppDeploymentRequest(appDefinition, resource, deploymentProperties); LocalDeployerProperties localDeployerProperties = new LocalDeployerProperties(); localDeployerProperties.getDocker().setDeleteContainerOnExit(false); ProcessBuilder builder = new DockerCommandBuilder("scdf_default") .buildExecutionCommand(request, new HashMap<>(), "deployerId", Optional.of(1), localDeployerProperties, Optional.empty()); assertThat(builder.command()).containsAnyElementsOf(Arrays.asList("docker", "run", "--network", "scdf_default", "--name=gogo-1", "foo/bar")); assertThat(builder.command()).doesNotContain("--rm"); } @Test public void testSpringApplicationJSON() { LocalDeployerProperties properties = new LocalDeployerProperties(); LocalAppDeployer deployer = new LocalAppDeployer(properties); AppDefinition definition = new AppDefinition("foo", Collections.singletonMap("foo", "bar")); Resource resource = new DockerResource("foo/bar"); Map deploymentProperties = new HashMap<>(); deploymentProperties.put(LocalDeployerProperties.DEBUG_ADDRESS, "*:9999"); deploymentProperties.put(LocalDeployerProperties.DEBUG_SUSPEND, "y"); deploymentProperties.put(LocalDeployerProperties.INHERIT_LOGGING, "true"); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, deploymentProperties); ProcessBuilder builder = deployer.buildProcessBuilder(request, request.getDefinition().getProperties(), Optional.of(1), "foo"); String SAJ = LocalDeployerUtils.isWindows() ? "SPRING_APPLICATION_JSON={\\\"foo\\\":\\\"bar\\\"}" : "SPRING_APPLICATION_JSON={\"foo\":\"bar\"}"; assertThat(builder.command()).contains("-e", SAJ); } @Test public void testContainerPortMappings(){ AppDefinition appDefinition = new AppDefinition("foo", null); Resource resource = new DockerResource("foo/bar"); Map deploymentProperties = Collections.emptyMap(); AppDeploymentRequest request = new AppDeploymentRequest(appDefinition, resource, deploymentProperties); String goodMapping1 = "9090:9090"; String goodMapping2 = "6090:7090"; String incompleteMapping = "8888"; LocalDeployerProperties localDeployerProperties = new LocalDeployerProperties(); localDeployerProperties.getDocker().setPortMappings(goodMapping1 + "," + goodMapping2 + "," + incompleteMapping); ProcessBuilder builder = new DockerCommandBuilder("scdf_default") .buildExecutionCommand(request, new HashMap<>(), "deployerId", Optional.of(1), localDeployerProperties, Optional.empty()); assertThat(builder.command()).contains(goodMapping1, goodMapping2); assertThat(builder.command()).doesNotContain(incompleteMapping); } @Test public void testContainerVolumeMount(){ AppDefinition appDefinition = new AppDefinition("foo", null); Resource resource = new DockerResource("foo/bar"); Map deploymentProperties = Collections.emptyMap(); AppDeploymentRequest request = new AppDeploymentRequest(appDefinition, resource, deploymentProperties); String goodMapping1 = "/tmp:/tmp"; String goodMapping2 = "/opt:/opt"; String incompleteMapping = "/dev/null"; LocalDeployerProperties localDeployerProperties = new LocalDeployerProperties(); localDeployerProperties.getDocker().setVolumeMounts(goodMapping1 + "," + goodMapping2 + "," + incompleteMapping); ProcessBuilder builder = new DockerCommandBuilder("scdf_default") .buildExecutionCommand(request, new HashMap<>(), "deployerId", Optional.of(1), localDeployerProperties, Optional.empty()); assertThat(builder.command()).contains(goodMapping1, goodMapping2); assertThat(builder.command()).doesNotContain(incompleteMapping); } @Test public void testContainerAdditionalHost(){ AppDefinition appDefinition = new AppDefinition("foo", null); Resource resource = new DockerResource("foo/bar"); Map deploymentProperties = Collections.emptyMap(); AppDeploymentRequest request = new AppDeploymentRequest(appDefinition, resource, deploymentProperties); String goodMapping1 = "host.docker.internal:host-gateway"; String incompleteMapping = "host.docker.internal"; LocalDeployerProperties localDeployerProperties = new LocalDeployerProperties(); localDeployerProperties.getDocker().setAdditionalHosts(goodMapping1 + "," + incompleteMapping); ProcessBuilder builder = new DockerCommandBuilder("scdf_default") .buildExecutionCommand(request, new HashMap<>(), "deployerId", Optional.of(1), localDeployerProperties, Optional.empty()); assertThat(builder.command()).contains(goodMapping1); assertThat(builder.command()).doesNotContain(incompleteMapping); } } ================================================ FILE: spring-cloud-deployer-local/src/test/java/org/springframework/cloud/deployer/spi/local/JavaExecutionCommandBuilderTests.java ================================================ /* * Copyright 2015-2021 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.cloud.deployer.spi.local; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.cloud.deployer.spi.app.AppDeployer; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.UrlResource; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class JavaExecutionCommandBuilderTests { private JavaCommandBuilder commandBuilder; private List args; private Map deploymentProperties; private LocalDeployerProperties localDeployerProperties; @BeforeEach public void setUp() { args = new ArrayList<>(); deploymentProperties = new HashMap<>(); localDeployerProperties = new LocalDeployerProperties(); commandBuilder = new JavaCommandBuilder(localDeployerProperties); } @Test public void testDirectJavaMemoryOption() { deploymentProperties.put(AppDeployer.MEMORY_PROPERTY_KEY, "1024m"); commandBuilder.addJavaOptions(args, deploymentProperties, localDeployerProperties); assertThat(args).hasSize(1); assertThat(args.get(0)).isEqualTo("-Xmx1024m"); } @Test public void testDirectJavaMemoryOptionWithG() { deploymentProperties.put(AppDeployer.MEMORY_PROPERTY_KEY, "1g"); commandBuilder.addJavaOptions(args, deploymentProperties, localDeployerProperties); assertThat(args).hasSize(1); assertThat(args.get(0)).isEqualTo("-Xmx1024m"); } @Test public void testJavaMemoryOption() { deploymentProperties.put(LocalDeployerProperties.PREFIX + ".javaOpts", "-Xmx1024m"); commandBuilder.addJavaOptions(args, deploymentProperties, localDeployerProperties); assertThat(args).hasSize(1); assertThat(args.get(0)).isEqualTo("-Xmx1024m"); } @Test public void testJavaMemoryOptionWithKebabCase() { deploymentProperties.put(LocalDeployerProperties.PREFIX + ".java-opts", "-Xmx1024m"); commandBuilder.addJavaOptions(args, deploymentProperties, localDeployerProperties); assertThat(args).hasSize(1); assertThat(args.get(0)).isEqualTo("-Xmx1024m"); } @Test public void testJavaCmdOption() throws Exception { Map properties = new HashMap<>(); properties.put(LocalDeployerProperties.PREFIX + ".javaCmd", "/test/java"); Resource resource = mock(Resource.class); when(resource.getFile()).thenReturn(new File("/")); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(mock(AppDefinition.class), resource, properties); ProcessBuilder builder = commandBuilder.buildExecutionCommand(appDeploymentRequest, new HashMap<>(), "deployerId", Optional.of(1), new LocalDeployerProperties(), Optional.empty()); assertThat(builder.command().get(0)).isEqualTo("/test/java"); } @Test public void testJavaCmdOptionWithKebabCase() throws Exception { Map properties = new HashMap<>(); properties.put(LocalDeployerProperties.PREFIX + ".java-cmd", "/test/java"); Resource resource = mock(Resource.class); when(resource.getFile()).thenReturn(new File("/")); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(mock(AppDefinition.class), resource, properties); ProcessBuilder builder = commandBuilder.buildExecutionCommand(appDeploymentRequest, new HashMap<>(), "deployerId", Optional.of(1), new LocalDeployerProperties(), Optional.empty()); assertThat(builder.command().get(0)).isEqualTo("/test/java"); } @Test public void testOverrideMemoryOptions() { deploymentProperties.put(AppDeployer.MEMORY_PROPERTY_KEY, "1024m"); deploymentProperties.put(LocalDeployerProperties.PREFIX + ".javaOpts", "-Xmx2048m"); commandBuilder.addJavaOptions(args, deploymentProperties, localDeployerProperties); assertThat(args).hasSize(1); assertThat(args.get(0)).isEqualTo("-Xmx2048m"); } @Test public void testDirectMemoryOptionsWithOtherOptions() { deploymentProperties.put(AppDeployer.MEMORY_PROPERTY_KEY, "1024m"); deploymentProperties.put(LocalDeployerProperties.PREFIX + ".javaOpts", "-Dtest=foo"); commandBuilder.addJavaOptions(args, deploymentProperties, localDeployerProperties); assertThat(args).hasSize(2); assertThat(args.get(0)).isEqualTo("-Xmx1024m"); assertThat(args.get(1)).isEqualTo("-Dtest=foo"); } @Test public void testMultipleOptions() { deploymentProperties.put(LocalDeployerProperties.PREFIX + ".javaOpts", "-Dtest=foo -Dbar=baz"); commandBuilder.addJavaOptions(args, deploymentProperties, localDeployerProperties); assertThat(args).hasSize(2); assertThat(args.get(0)).isEqualTo("-Dtest=foo"); assertThat(args.get(1)).isEqualTo("-Dbar=baz"); } @Test public void testConfigurationPropertiesOverride() { localDeployerProperties.setJavaOpts("-Dfoo=test -Dbaz=bar"); commandBuilder.addJavaOptions(args, deploymentProperties, localDeployerProperties); assertThat(args).hasSize(2); assertThat(args.get(0)).isEqualTo("-Dfoo=test"); assertThat(args.get(1)).isEqualTo("-Dbaz=bar"); } @Test public void testJarExecution() { AppDefinition definition = new AppDefinition("randomApp", new HashMap<>()); deploymentProperties.put(LocalDeployerProperties.PREFIX + ".javaOpts", "-Dtest=foo -Dbar=baz"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, testResource(), deploymentProperties); commandBuilder.addJavaExecutionOptions(args, appDeploymentRequest); assertThat(args).hasSize(2); assertThat(args.get(0)).isEqualTo("-jar"); assertThat(args.get(1)).contains("testResource.txt"); } @Test public void testBadResourceExecution() { assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> { AppDefinition definition = new AppDefinition("randomApp", new HashMap<>()); deploymentProperties.put(LocalDeployerProperties.PREFIX + ".javaOpts", "-Dtest=foo -Dbar=baz"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, new UrlResource("https://spring.io"), deploymentProperties); commandBuilder.addJavaExecutionOptions(args, appDeploymentRequest); }); } @Test public void testCommandBuilderSpringApplicationJson() { LocalDeployerProperties properties = new LocalDeployerProperties(); LocalAppDeployer deployer = new LocalAppDeployer(properties); AppDefinition definition = new AppDefinition("foo", Collections.singletonMap("foo", "bar")); deploymentProperties.put(LocalDeployerProperties.DEBUG_PORT, "9999"); deploymentProperties.put(LocalDeployerProperties.DEBUG_SUSPEND, "y"); deploymentProperties.put(LocalDeployerProperties.INHERIT_LOGGING, "true"); AppDeploymentRequest request = new AppDeploymentRequest(definition, testResource(), deploymentProperties); ProcessBuilder builder = deployer.buildProcessBuilder(request, definition.getProperties(), Optional.of(1), "foo"); assertThat(builder.environment().keySet()).contains(AbstractLocalDeployerSupport.SPRING_APPLICATION_JSON); assertThat(builder.environment().get(AbstractLocalDeployerSupport.SPRING_APPLICATION_JSON)).isEqualTo("{\"foo\":\"bar\"}"); } @Test public void testCommandBuilderWithSpringApplicationJson() { LocalDeployerProperties properties = new LocalDeployerProperties(); LocalAppDeployer deployer = new LocalAppDeployer(properties); Map applicationProperties = new HashMap<>(); applicationProperties.put("foo", "bar"); String SAJ = "{\"debug\":\"true\"}"; applicationProperties.put(AbstractLocalDeployerSupport.SPRING_APPLICATION_JSON, SAJ); AppDefinition definition = new AppDefinition("foo", applicationProperties); deploymentProperties.put(LocalDeployerProperties.DEBUG_PORT, "9999"); deploymentProperties.put(LocalDeployerProperties.DEBUG_SUSPEND, "y"); deploymentProperties.put(LocalDeployerProperties.INHERIT_LOGGING, "true"); AppDeploymentRequest request = new AppDeploymentRequest(definition, testResource(), deploymentProperties); ProcessBuilder builder = deployer.buildProcessBuilder(request, definition.getProperties(), Optional.of(1), "foo"); assertThat(builder.environment().keySet()).contains(AbstractLocalDeployerSupport.SPRING_APPLICATION_JSON); assertThat(builder.environment().get(AbstractLocalDeployerSupport.SPRING_APPLICATION_JSON)).isEqualTo("{\"foo\":\"bar\",\"debug\":\"true\"}"); } @Test public void testRetainEnv() { LocalDeployerProperties properties1 = new LocalDeployerProperties(); LocalAppDeployer deployer1 = new LocalAppDeployer(properties1); AppDefinition definition1 = new AppDefinition("foo", null); AppDeploymentRequest request1 = new AppDeploymentRequest(definition1, testResource(), deploymentProperties); ProcessBuilder builder1 = deployer1.buildProcessBuilder(request1, definition1.getProperties(), Optional.of(1), "foo"); List env1 = builder1.environment().keySet().stream().map(String::toLowerCase).collect(Collectors.toList()); LocalDeployerProperties properties2 = new LocalDeployerProperties(); properties2.setEnvVarsToInherit(new String[0]); LocalAppDeployer deployer2 = new LocalAppDeployer(properties2); AppDefinition definition2 = new AppDefinition("foo", null); AppDeploymentRequest request2 = new AppDeploymentRequest(definition2, testResource(), deploymentProperties); ProcessBuilder builder2 = deployer2.buildProcessBuilder(request2, definition2.getProperties(), Optional.of(1), "foo"); List env2 = builder2.environment().keySet().stream().map(String::toLowerCase).collect(Collectors.toList()); if (env1.contains("path")) { // path should be there, and it was check that something were removed assertThat(builder1.environment().keySet().size()).isGreaterThan(builder2.environment().keySet().size()); } assertThat(env2).doesNotContain("path"); } protected Resource testResource() { return new ClassPathResource("testResource.txt"); } } ================================================ FILE: spring-cloud-deployer-local/src/test/java/org/springframework/cloud/deployer/spi/local/LocalAppDeployerEnvironmentIntegrationTests.java ================================================ /* * Copyright 2018-2022 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.cloud.deployer.spi.local; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.nio.ByteBuffer; import java.time.Duration; import java.util.Collections; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.UUID; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.deployer.resource.docker.DockerResource; import org.springframework.cloud.deployer.spi.app.ActuatorOperations; import org.springframework.cloud.deployer.spi.app.AppDeployer; import org.springframework.cloud.deployer.spi.app.AppInstanceStatus; import org.springframework.cloud.deployer.spi.app.AppStatus; import org.springframework.cloud.deployer.spi.app.DeploymentState; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.local.LocalAppDeployerEnvironmentIntegrationTests.Config; import org.springframework.cloud.deployer.spi.test.AbstractAppDeployerIntegrationJUnit5Tests; import org.springframework.cloud.deployer.spi.test.AbstractIntegrationJUnit5Tests; import org.springframework.cloud.deployer.spi.test.Timeout; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.Resource; import org.springframework.web.client.RestTemplate; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; /** * Integration tests for {@link LocalAppDeployer} not using SAJ. * * Now supports running with Docker images for tests, just set this env var: * * SPRING_CLOUD_DEPLOYER_SPI_TEST_USE_DOCKER=true * * @author Eric Bottard * @author Mark Fisher * @author Oleg Zhurakousky * @author Janne Valkealahti * @author Ilayaperumal Gopinathan */ @SpringBootTest(classes = { Config.class, AbstractIntegrationJUnit5Tests.Config.class }, value = { "maven.remoteRepositories.springRepo.url=https://repo.spring.io/snapshot", "spring.cloud.deployer.local.use-spring-application-json=false" }) public class LocalAppDeployerEnvironmentIntegrationTests extends AbstractAppDeployerIntegrationJUnit5Tests { private static final String TESTAPP_DOCKER_IMAGE_NAME = "springcloud/spring-cloud-deployer-spi-test-app:latest"; @Autowired private AppDeployer appDeployer; @Autowired private ActuatorOperations actuatorOperations; @Value("${spring-cloud-deployer-spi-test-use-docker:false}") private boolean useDocker; @Override protected AppDeployer provideAppDeployer() { return appDeployer; } @Override protected Resource testApplication() { if (useDocker) { log.info("Using Docker image for tests"); return new DockerResource(TESTAPP_DOCKER_IMAGE_NAME); } return super.testApplication(); } @Override protected String randomName() { if (LocalDeployerUtils.isWindows()) { // tweak random dir name on win to be shorter String uuid = UUID.randomUUID().toString(); long l = ByteBuffer.wrap(uuid.toString().getBytes()).getLong(); return testName + Long.toString(l, Character.MAX_RADIX); } else { return super.randomName(); } } @Test public void testEnvVariablesInheritedViaEnvEndpointNoSaj() { if (useDocker) { // would not expect to be able to check anything on docker return; } Map properties = new HashMap<>(); AppDefinition definition = new AppDefinition(randomName(), properties); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = appDeployer().deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); Map instances = appDeployer().status(deploymentId).getInstances(); String url = null; if (instances.size() == 1) { url = instances.entrySet().iterator().next().getValue().getAttributes().get("url"); } String env = null; if (url != null) { RestTemplate template = new RestTemplate(); env = template.getForObject(url + "/actuator/env", String.class); } log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer().undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); assertThat(url).isNotNull(); if (LocalDeployerUtils.isWindows()) { // windows is weird, we may still get Path or PATH assertThat(env).containsIgnoringCase("path"); } else { assertThat(env).contains("\"PATH\""); // we're not using SAJ so it's i.e. // INSTANCE_INDEX not instance.index assertThat(env).contains("\"INSTANCE_INDEX\""); assertThat(env).contains("\"SPRING_APPLICATION_INDEX\""); assertThat(env).contains("\"SPRING_CLOUD_APPLICATION_GUID\""); } } @Test public void testFailureToCallShutdownOnUndeploy() { Map properties = new HashMap<>(); // disable shutdown endpoint properties.put("endpoints.shutdown.enabled", "false"); AppDefinition definition = new AppDefinition(randomName(), properties); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = appDeployer().deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer().undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); } @Test // was triggered by GH-50 and subsequently GH-55 public void testNoStdoutStderrOnInheritLoggingAndNoNPEOnGetAttributes() { Map properties = new HashMap<>(); AppDefinition definition = new AppDefinition(randomName(), properties); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, Collections.singletonMap(LocalDeployerProperties.INHERIT_LOGGING, "true")); AppDeployer deployer = appDeployer(); String deploymentId = deployer.deploy(request); AppStatus appStatus = deployer.status(deploymentId); assertThat(appStatus.getInstances()).hasSizeGreaterThan(0); for (Entry instanceStatusEntry : appStatus.getInstances().entrySet()) { Map attributes = instanceStatusEntry.getValue().getAttributes(); assertThat(attributes).doesNotContainKey("stdout"); assertThat(attributes).doesNotContainKey("stderr"); } deployer.undeploy(deploymentId); } @Test public void testInDebugModeWithSuspended() throws Exception { Map properties = new HashMap<>(); AppDefinition definition = new AppDefinition(randomName(), properties); Resource resource = testApplication(); Map deploymentProperties = new HashMap<>(); deploymentProperties.put(LocalDeployerProperties.DEBUG_PORT, "9999"); deploymentProperties.put(LocalDeployerProperties.DEBUG_SUSPEND, "y"); deploymentProperties.put(LocalDeployerProperties.INHERIT_LOGGING, "true"); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, deploymentProperties); AppDeployer deployer = appDeployer(); String deploymentId = deployer.deploy(request); Thread.sleep(5000); AppStatus appStatus = deployer.status(deploymentId); if (resource instanceof DockerResource) { try { String containerId = getCommandOutput("docker ps -q --filter ancestor="+ TESTAPP_DOCKER_IMAGE_NAME); String logOutput = getCommandOutput("docker logs "+ containerId); assertThat(logOutput).contains("Listening for transport dt_socket at address: 9999"); } catch (IOException e) { } } else { assertThat(appStatus.toString()).contains("deploying"); } deployer.undeploy(deploymentId); } @Test public void testActuatorOperations() { if (useDocker) { // would not expect to be able to check anything on docker return; } Map properties = new HashMap<>(); AppDefinition definition = new AppDefinition(randomName(), properties); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = appDeployer().deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); String id = deploymentId + "-0"; Map env = actuatorOperations .getFromActuator(deploymentId, id, "/env", Map.class); assertThat(env).containsKeys("activeProfiles", "propertySources"); Map status = actuatorOperations .getFromActuator(deploymentId, id, "/health", Map.class); assertThat(status.get("status")).isEqualTo("UP"); Map loggers = actuatorOperations .getFromActuator(deploymentId, id, "/loggers/org.springframework", Map.class); assertThat(loggers).isNotNull(); assertThat(loggers.get("configuredLevel")).isNull(); actuatorOperations.postToActuator(deploymentId, id,"/loggers/org.springframework", Collections.singletonMap("configuredLevel", "debug"), Object.class); loggers = actuatorOperations .getFromActuator(deploymentId, id, "/loggers/org.springframework", Map.class); assertThat(((String)loggers.get("configuredLevel")).toLowerCase(Locale.ROOT)).isEqualTo("debug"); } private String getCommandOutput(String cmd) throws IOException { Process process = Runtime.getRuntime().exec(cmd); BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream())); return stdInput.lines().findFirst().get(); } @Configuration @EnableConfigurationProperties(LocalDeployerProperties.class) public static class Config { @Bean public AppDeployer appDeployer(LocalDeployerProperties properties) { return new LocalAppDeployer(properties); } @Bean ActuatorOperations actuatorOperations(AppDeployer appDeployer, LocalDeployerProperties properties) { return new LocalActuatorTemplate(new RestTemplate(), appDeployer, properties.getAppAdmin()); } } } ================================================ FILE: spring-cloud-deployer-local/src/test/java/org/springframework/cloud/deployer/spi/local/LocalAppDeployerIntegrationTests.java ================================================ /* * Copyright 2016-2021 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.cloud.deployer.spi.local; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.nio.ByteBuffer; import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.UUID; import java.util.stream.Collectors; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.deployer.resource.docker.DockerResource; import org.springframework.cloud.deployer.spi.app.AppDeployer; import org.springframework.cloud.deployer.spi.app.AppInstanceStatus; import org.springframework.cloud.deployer.spi.app.AppStatus; import org.springframework.cloud.deployer.spi.app.DeploymentState; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.local.LocalAppDeployerIntegrationTests.Config; import org.springframework.cloud.deployer.spi.test.AbstractAppDeployerIntegrationJUnit5Tests; import org.springframework.cloud.deployer.spi.test.AbstractIntegrationJUnit5Tests; import org.springframework.cloud.deployer.spi.test.Timeout; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.Resource; import org.springframework.test.annotation.DirtiesContext; import org.springframework.web.client.RestTemplate; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; /** * Integration tests for {@link LocalAppDeployer}. *

* Now supports running with Docker images for tests, just set this env var: *

* SPRING_CLOUD_DEPLOYER_SPI_TEST_USE_DOCKER=true * * @author Eric Bottard * @author Mark Fisher * @author Oleg Zhurakousky * @author Janne Valkealahti * @author Ilayaperumal Gopinathan */ @SpringBootTest(classes = { Config.class, AbstractIntegrationJUnit5Tests.Config.class }, properties = { "maven.remoteRepositories.springRepo.url=https://repo.spring.io/snapshot" }) @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) public class LocalAppDeployerIntegrationTests extends AbstractAppDeployerIntegrationJUnit5Tests { private static final String TESTAPP_DOCKER_IMAGE_NAME = "springcloud/spring-cloud-deployer-spi-test-app:latest"; @Autowired private AppDeployer appDeployer; @Value("${spring-cloud-deployer-spi-test-use-docker:false}") private boolean useDocker; @Override protected AppDeployer provideAppDeployer() { return appDeployer; } @Override protected Resource testApplication() { if (useDocker) { log.info("Using Docker image for tests"); return new DockerResource(TESTAPP_DOCKER_IMAGE_NAME); } return super.testApplication(); } @Override protected String randomName() { if (LocalDeployerUtils.isWindows()) { // tweak random dir name on win to be shorter String uuid = UUID.randomUUID().toString(); long l = ByteBuffer.wrap(uuid.getBytes()).getLong(); return testName + Long.toString(l, Character.MAX_RADIX); } else { return super.randomName(); } } @Test public void testEnvVariablesInheritedViaEnvEndpoint() { if (useDocker) { // would not expect to be able to check anything on docker return; } Map properties = new HashMap<>(); AppDefinition definition = new AppDefinition(randomName(), properties); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); log.info("Deploying {}...", request.getDefinition().getName()); AppDeployer appDeployer = appDeployer(); String deploymentId = appDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer.status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); Map instances = appDeployer.status(deploymentId).getInstances(); String url = null; if (instances.size() == 1) { url = instances.entrySet().iterator().next().getValue().getAttributes().get("url"); } String env = null; if (url != null) { RestTemplate template = new RestTemplate(); env = template.getForObject(url + "/actuator/env", String.class); } log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer.status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); assertThat(url).isNotNull(); if (LocalDeployerUtils.isWindows()) { // windows is weird, we may still get Path or PATH assertThat(env).containsIgnoringCase("path"); } else { assertThat(env).contains("\"PATH\""); // we're defaulting to SAJ so it's i.e. // instance.index not INSTANCE_INDEX assertThat(env).contains("\"instance.index\""); assertThat(env).contains("\"spring.application.index\""); assertThat(env).contains("\"spring.cloud.application.guid\""); assertThat(env).contains("\"spring.cloud.stream.instanceIndex\""); } } @Test public void testAppLogRetrieval() { Map properties = new HashMap<>(); AppDefinition definition = new AppDefinition(randomName(), properties); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = appDeployer().deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); String logContent = appDeployer().getLog(deploymentId); assertThat(logContent).contains("Starting DeployerIntegrationTestApplication"); } // TODO: remove when these two are forced in tck tests @Test public void testScale() { doTestScale(false); } @Test public void testScaleWithIndex() { doTestScale(true); } @Test public void testFailureToCallShutdownOnUndeploy() { Map properties = new HashMap<>(); // disable shutdown endpoint properties.put("endpoints.shutdown.enabled", "false"); AppDefinition definition = new AppDefinition(randomName(), properties); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); log.info("Deploying {}...", request.getDefinition().getName()); AppDeployer appDeployer = appDeployer(); String deploymentId = appDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer.status(deploymentId).getState()).isEqualTo(DeploymentState.deployed) ); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer.status(deploymentId).getState()).isEqualTo(DeploymentState.unknown) ); } @Test // was triggered by GH-50 and subsequently GH-55 public void testNoStdoutStderrOnInheritLoggingAndNoNPEOnGetAttributes() { Map properties = new HashMap<>(); AppDefinition definition = new AppDefinition(randomName(), properties); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, Collections.singletonMap(LocalDeployerProperties.INHERIT_LOGGING, "true")); AppDeployer deployer = appDeployer(); String deploymentId = deployer.deploy(request); AppStatus appStatus = deployer.status(deploymentId); assertThat(appStatus.getInstances()).hasSizeGreaterThan(0); for (Entry instanceStatusEntry : appStatus.getInstances().entrySet()) { Map attributes = instanceStatusEntry.getValue().getAttributes(); assertThat(attributes).doesNotContainKey("stdout"); assertThat(attributes).doesNotContainKey("stderr"); } deployer.undeploy(deploymentId); } @Test public void testInDebugModeWithSuspended() throws Exception { Map properties = new HashMap<>(); AppDefinition definition = new AppDefinition(randomName(), properties); Resource resource = testApplication(); Map deploymentProperties = new HashMap<>(); deploymentProperties.put(LocalDeployerProperties.DEBUG_PORT, "9999"); deploymentProperties.put(LocalDeployerProperties.DEBUG_SUSPEND, "y"); deploymentProperties.put(LocalDeployerProperties.INHERIT_LOGGING, "true"); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, deploymentProperties); AppDeployer deployer = appDeployer(); String deploymentId = deployer.deploy(request); Thread.sleep(5000); AppStatus appStatus = deployer.status(deploymentId); if (resource instanceof DockerResource) { try { String containerId = getCommandOutput("docker ps -q --filter ancestor=" + TESTAPP_DOCKER_IMAGE_NAME); String logOutput = getCommandOutput("docker logs " + containerId); assertThat(logOutput).contains("Listening for transport dt_socket at address: 9999"); } catch (IOException e) { log.debug("Exception:ignored:" + e); } } else { assertThat(appStatus.toString()).contains("deploying"); } deployer.undeploy(deploymentId); } @Test public void testInDebugModeWithSuspendedUseCamelCase() throws Exception { Map properties = new HashMap<>(); AppDefinition definition = new AppDefinition(randomName(), properties); Resource resource = testApplication(); Map deploymentProperties = new HashMap<>(); deploymentProperties.put(LocalDeployerProperties.PREFIX + ".debugPort", "8888"); deploymentProperties.put(LocalDeployerProperties.PREFIX + ".debugSuspend", "y"); deploymentProperties.put(LocalDeployerProperties.PREFIX + ".inheritLogging", "true"); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, deploymentProperties); AppDeployer deployer = appDeployer(); String deploymentId = deployer.deploy(request); Thread.sleep(5000); AppStatus appStatus = deployer.status(deploymentId); if (resource instanceof DockerResource) { try { String containerId = getCommandOutput("docker ps -q --filter ancestor=" + TESTAPP_DOCKER_IMAGE_NAME); String logOutput = getCommandOutput("docker logs " + containerId); assertThat(logOutput).contains("Listening for transport dt_socket at address: 8888"); String containerPorts = getCommandOutputAll("docker port " + containerId); assertThat(containerPorts).contains("8888/tcp -> 0.0.0.0:8888"); } catch (IOException e) { log.debug("Exception:ignored:" + e); } } else { assertThat(appStatus.toString()).contains("deploying"); } deployer.undeploy(deploymentId); } @Test public void testUseDefaultDeployerProperties() throws IOException { LocalDeployerProperties localDeployerProperties = new LocalDeployerProperties(); Path tmpPath = new File(System.getProperty("java.io.tmpdir")).toPath(); Path customWorkDirRoot = tmpPath.resolve("test-default-directory"); localDeployerProperties.setWorkingDirectoriesRoot(customWorkDirRoot.toFile().getAbsolutePath()); // Create a new LocalAppDeployer using a working directory that is different from the default value. AppDeployer appDeployer = new LocalAppDeployer(localDeployerProperties); List beforeDirs = getBeforePaths(customWorkDirRoot); Map properties = new HashMap<>(); properties.put("server.port", "0"); AppDefinition definition = new AppDefinition(randomName(), properties); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); // Deploy String deploymentId = appDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer.status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); timeout = undeploymentTimeout(); // Undeploy appDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer.status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); List afterDirs = getAfterPaths(customWorkDirRoot); assertThat(afterDirs).as("Additional working directory not created").hasSize(beforeDirs.size() + 1); } @Test public void testZeroPortReportsDeployed() throws IOException { Map properties = new HashMap<>(); properties.put("server.port", "0"); Path tmpPath = new File(System.getProperty("java.io.tmpdir")).toPath(); Path customWorkDirRoot = tmpPath.resolve("spring-cloud-deployer-app-workdir"); Map deploymentProperties = new HashMap<>(); deploymentProperties.put(LocalDeployerProperties.PREFIX + ".working-directories-root", customWorkDirRoot.toFile().getAbsolutePath()); AppDefinition definition = new AppDefinition(randomName(), properties); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, deploymentProperties); List beforeDirs = getBeforePaths(customWorkDirRoot); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = appDeployer().deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer().undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); List afterDirs = getAfterPaths(customWorkDirRoot); assertThat(afterDirs.size()).as("Additional working directory not created").isEqualTo(beforeDirs.size() + 1); } @Test public void testStartupProbeFail() { Map deploymentProperties = new HashMap<>(); deploymentProperties.put(LocalDeployerProperties.PREFIX + ".startup-probe.path", "/fake"); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, deploymentProperties); log.info("Deploying {}...", request.getDefinition().getName()); AppDeployer appDeployer = appDeployer(); String deploymentId = appDeployer.deploy(request); await().atMost(Duration.ofSeconds(30)).until(() -> appDeployer.status(deploymentId).getState().equals(DeploymentState.deploying) ); await().during(Duration.ofSeconds(10)).atMost(Duration.ofSeconds(12)).until(() -> appDeployer.status(deploymentId).getState().equals(DeploymentState.deploying) ); log.info("Undeploying {}...", deploymentId); Timeout timeout = undeploymentTimeout(); appDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer.status(deploymentId).getState()).isEqualTo(DeploymentState.unknown) ); } @Test public void testStartupProbeSucceed() { Map deploymentProperties = new HashMap<>(); deploymentProperties.put(LocalDeployerProperties.PREFIX + ".startup-probe.path", "/actuator/info"); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, deploymentProperties); log.info("Deploying {}...", request.getDefinition().getName()); AppDeployer appDeployer = appDeployer(); String deploymentId = appDeployer.deploy(request); await().atMost(Duration.ofSeconds(30)).until(() -> appDeployer.status(deploymentId).getState().equals(DeploymentState.deploying) ); await().atMost(Duration.ofSeconds(12)).until(() -> appDeployer.status(deploymentId).getState().equals(DeploymentState.deployed) ); log.info("Undeploying {}...", deploymentId); Timeout timeout = undeploymentTimeout(); appDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer.status(deploymentId).getState()).isEqualTo(DeploymentState.unknown) ); } @Test public void testHealthProbeFail() { Map deploymentProperties = new HashMap<>(); deploymentProperties.put(LocalDeployerProperties.PREFIX + ".health-probe.path", "/fake"); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, deploymentProperties); log.info("Deploying {}...", request.getDefinition().getName()); AppDeployer appDeployer = appDeployer(); String deploymentId = appDeployer.deploy(request); await() .atMost(Duration.ofSeconds(30)) .until(() -> { DeploymentState state = appDeployer.status(deploymentId).getState(); log.info("Awaiting failed. State={}", state); return DeploymentState.failed.equals(state); }); log.info("Undeploying {}...", deploymentId); Timeout timeout = undeploymentTimeout(); appDeployer.undeploy(deploymentId); await() .pollDelay(Duration.ofMillis(timeout.pause)) .pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer.status(deploymentId).getState()).isEqualTo(DeploymentState.unknown) ); } @Test public void testHealthProbeSucceed() { Map deploymentProperties = new HashMap<>(); deploymentProperties.put(LocalDeployerProperties.PREFIX + ".health-probe.path", "/actuator/info"); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, deploymentProperties); log.info("Deploying {}...", request.getDefinition().getName()); AppDeployer appDeployer = appDeployer(); String deploymentId = appDeployer.deploy(request); await() .atMost(Duration.ofSeconds(30)) .until(() -> { DeploymentState state = appDeployer.status(deploymentId).getState(); log.info("Awaiting deployed. State={}", state); return DeploymentState.deployed.equals(state); } ); log.info("Undeploying {}...", deploymentId); Timeout timeout = undeploymentTimeout(); appDeployer.undeploy(deploymentId); await() .pollDelay(Duration.ofMillis(timeout.pause)) .pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { DeploymentState state = appDeployer.status(deploymentId).getState(); log.info("Awaiting unknown. State={}", state); assertThat(state).isEqualTo(DeploymentState.unknown); }); } @SuppressWarnings("resource") private List getAfterPaths(Path customWorkDirRoot) throws IOException { if (!Files.exists(customWorkDirRoot)) { return new ArrayList<>(); } return Files.walk(customWorkDirRoot, 1) .filter(Files::isDirectory) .filter(path -> !path.getFileName().toString().startsWith(".")) .collect(Collectors.toList()); } private List getBeforePaths(Path customWorkDirRoot) throws IOException { List beforeDirs = new ArrayList<>(); beforeDirs.add(customWorkDirRoot); if (Files.exists(customWorkDirRoot)) { beforeDirs = Files.walk(customWorkDirRoot, 1) .filter(Files::isDirectory) .collect(Collectors.toList()); } return beforeDirs; } private String getCommandOutput(String cmd) throws IOException { Process process = Runtime.getRuntime().exec(cmd); BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream())); return stdInput.lines().findFirst().orElse(null); } private String getCommandOutputAll(String cmd) throws IOException { Process process = Runtime.getRuntime().exec(cmd); BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream())); return stdInput.lines().collect(Collectors.joining()); } @Configuration @EnableConfigurationProperties(LocalDeployerProperties.class) public static class Config { @Bean public AppDeployer appDeployer(LocalDeployerProperties properties) { return new LocalAppDeployer(properties); } } } ================================================ FILE: spring-cloud-deployer-local/src/test/java/org/springframework/cloud/deployer/spi/local/LocalDeployerPropertiesTests.java ================================================ /* * Copyright 2019-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.cloud.deployer.spi.local; import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledOnOs; import org.junit.jupiter.api.condition.OS; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.core.env.StandardEnvironment; import org.springframework.core.env.SystemEnvironmentPropertySource; import static org.assertj.core.api.Assertions.assertThat; public class LocalDeployerPropertiesTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner(); @Test @EnabledOnOs(OS.LINUX) public void defaultNoPropertiesSet() { this.contextRunner .withUserConfiguration(Config1.class) .run((context) -> { LocalDeployerProperties properties = context.getBean(LocalDeployerProperties.class); assertThat(properties.getDebugPort()).isNull(); assertThat(properties.getDebugSuspend()).isEqualTo(LocalDeployerProperties.DebugSuspendType.y); assertThat(properties.isDeleteFilesOnExit()).isTrue(); assertThat(properties.getEnvVarsToInherit()).containsExactlyInAnyOrder("TMP", "LANG", "LANGUAGE", "LC_.*", "PATH", "SPRING_APPLICATION_JSON"); assertThat(properties.isInheritLogging()).isFalse(); assertThat(properties.getJavaCommand("2")).contains("java"); assertThat(properties.getJavaOpts()).isNull(); assertThat(properties.getMaximumConcurrentTasks()).isEqualTo(20); assertThat(properties.getPortRange()).isNotNull(); assertThat(properties.getPortRange().getLow()).isEqualTo(20000); assertThat(properties.getPortRange().getHigh()).isEqualTo(61000); assertThat(properties.getShutdownTimeout()).isEqualTo(30); assertThat(properties.isUseSpringApplicationJson()).isTrue(); assertThat(properties.getDocker().getNetwork()).isEqualTo("bridge"); assertThat(properties.getStartupProbe()).isNotNull(); assertThat(properties.getStartupProbe().getPath()).isNull(); assertThat(properties.getHealthProbe()).isNotNull(); assertThat(properties.getHealthProbe().getPath()).isNull(); }); } @Test public void setAllProperties() { this.contextRunner .withInitializer(context -> { Map map = new HashMap<>(); map.put("spring.cloud.deployer.local.debug-port", "8888"); map.put("spring.cloud.deployer.local.debug-suspend", "n"); map.put("spring.cloud.deployer.local.delete-files-on-exit", false); map.put("spring.cloud.deployer.local.env-vars-to-inherit", "FOO,BAR"); map.put("spring.cloud.deployer.local.inherit-logging", true); map.put("spring.cloud.deployer.local.java-home-path.2", "foobar1"); map.put("spring.cloud.deployer.local.java-opts", "foobar2"); map.put("spring.cloud.deployer.local.maximum-concurrent-tasks", 1234); map.put("spring.cloud.deployer.local.port-range.low", 2345); map.put("spring.cloud.deployer.local.port-range.high", 2346); map.put("spring.cloud.deployer.local.shutdown-timeout", 3456); map.put("spring.cloud.deployer.local.use-spring-application-json", false); map.put("spring.cloud.deployer.local.docker.network", "spring-cloud-dataflow-server_default"); map.put("spring.cloud.deployer.local.docker.port-mappings", "9091:5678"); map.put("spring.cloud.deployer.local.docker.volume-mounts", "/tmp:/opt"); map.put("spring.cloud.deployer.local.startup-probe.path", "/path1"); map.put("spring.cloud.deployer.local.health-probe.path", "/path2"); map.put("spring.cloud.deployer.local.app-admin.user", "user"); map.put("spring.cloud.deployer.local.app-admin.password", "password"); context.getEnvironment().getPropertySources().addLast(new SystemEnvironmentPropertySource( StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, map)); }) .withUserConfiguration(Config1.class) .run((context) -> { LocalDeployerProperties properties = context.getBean(LocalDeployerProperties.class); assertThat(properties.getDebugPort()).isEqualTo(8888); assertThat(properties.getDebugSuspend()).isEqualTo(LocalDeployerProperties.DebugSuspendType.n); assertThat(properties.isDeleteFilesOnExit()).isFalse(); assertThat(properties.getEnvVarsToInherit()).containsExactly("FOO", "BAR"); assertThat(properties.isInheritLogging()).isTrue(); assertThat(properties.getJavaHomePath().get("2")).contains("foobar1"); assertThat(properties.getJavaOpts()).contains("foobar2"); assertThat(properties.getMaximumConcurrentTasks()).isEqualTo(1234); assertThat(properties.getPortRange()).isNotNull(); assertThat(properties.getPortRange().getLow()).isEqualTo(2345); assertThat(properties.getPortRange().getHigh()).isEqualTo(2346); assertThat(properties.getShutdownTimeout()).isEqualTo(3456); assertThat(properties.isUseSpringApplicationJson()).isFalse(); assertThat(properties.getDocker().getNetwork()).isEqualTo("spring-cloud-dataflow-server_default"); assertThat(properties.getDocker().getPortMappings()).isEqualTo("9091:5678"); assertThat(properties.getDocker().getVolumeMounts()).isEqualTo("/tmp:/opt"); assertThat(properties.getStartupProbe().getPath()).isEqualTo("/path1"); assertThat(properties.getHealthProbe().getPath()).isEqualTo("/path2"); assertThat(properties.getAppAdmin().getUser()).isEqualTo("user"); assertThat(properties.getAppAdmin().getPassword()).isEqualTo("password"); }); } @Test public void setAllPropertiesCamelCase() { this.contextRunner .withInitializer(context -> { Map map = new HashMap<>(); map.put("spring.cloud.deployer.local.debugPort", "8888"); map.put("spring.cloud.deployer.local.debugSuspend", "n"); map.put("spring.cloud.deployer.local.deleteFilesOnExit", false); map.put("spring.cloud.deployer.local.envVarsToInherit", "FOO,BAR"); map.put("spring.cloud.deployer.local.inheritLogging", true); map.put("spring.cloud.deployer.local.javaHomePath.2", "foobar1"); map.put("spring.cloud.deployer.local.javaOpts", "foobar2"); map.put("spring.cloud.deployer.local.maximumConcurrentTasks", 1234); map.put("spring.cloud.deployer.local.portRange.low", 2345); map.put("spring.cloud.deployer.local.portRange.high", 2346); map.put("spring.cloud.deployer.local.shutdownTimeout", 3456); map.put("spring.cloud.deployer.local.useSpringApplicationJson", false); map.put("spring.cloud.deployer.local.docker.network", "spring-cloud-dataflow-server_default"); map.put("spring.cloud.deployer.local.docker.portMappings", "9091:5678"); map.put("spring.cloud.deployer.local.docker.volumeMounts", "/tmp:/opt"); map.put("spring.cloud.deployer.local.appAdmin.user", "user"); map.put("spring.cloud.deployer.local.appAdmin.password", "password"); context.getEnvironment().getPropertySources().addLast(new SystemEnvironmentPropertySource( StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, map)); }) .withUserConfiguration(Config1.class) .run((context) -> { LocalDeployerProperties properties = context.getBean(LocalDeployerProperties.class); assertThat(properties.getDebugPort()).isEqualTo(8888); assertThat(properties.getDebugSuspend()).isEqualTo(LocalDeployerProperties.DebugSuspendType.n); assertThat(properties.isDeleteFilesOnExit()).isFalse(); assertThat(properties.getEnvVarsToInherit()).containsExactly("FOO", "BAR"); assertThat(properties.isInheritLogging()).isTrue(); assertThat(properties.getJavaHomePath().get("2")).contains("foobar1"); assertThat(properties.getJavaOpts()).contains("foobar2"); assertThat(properties.getMaximumConcurrentTasks()).isEqualTo(1234); assertThat(properties.getPortRange()).isNotNull(); assertThat(properties.getPortRange().getLow()).isEqualTo(2345); assertThat(properties.getPortRange().getHigh()).isEqualTo(2346); assertThat(properties.getShutdownTimeout()).isEqualTo(3456); assertThat(properties.isUseSpringApplicationJson()).isFalse(); assertThat(properties.getDocker().getNetwork()).isEqualTo("spring-cloud-dataflow-server_default"); assertThat(properties.getDocker().getPortMappings()).isEqualTo("9091:5678"); assertThat(properties.getDocker().getVolumeMounts()).isEqualTo("/tmp:/opt"); assertThat(properties.getAppAdmin().getUser()).isEqualTo("user"); assertThat(properties.getAppAdmin().getPassword()).isEqualTo("password"); }); } @Test @EnabledOnOs(OS.WINDOWS) public void testOnWindows() { this.contextRunner .withInitializer(context -> { Map map = new HashMap<>(); map.put("spring.cloud.deployer.local.working-directories-root", "file:/C:/tmp"); context.getEnvironment().getPropertySources().addLast(new SystemEnvironmentPropertySource( StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, map)); }) .withUserConfiguration(Config1.class) .run((context) -> { LocalDeployerProperties properties = context.getBean(LocalDeployerProperties.class); assertThat(properties.getWorkingDirectoriesRoot()).isNotNull(); assertThat(properties.getWorkingDirectoriesRoot().toString()).isEqualTo("C:\\tmp"); }); } @Test @EnabledOnOs(OS.LINUX) public void testOnLinux() { this.contextRunner .withInitializer(context -> { Map map = new HashMap<>(); map.put("spring.cloud.deployer.local.working-directories-root", "/tmp"); context.getEnvironment().getPropertySources().addLast(new SystemEnvironmentPropertySource( StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, map)); }) .withUserConfiguration(Config1.class) .run((context) -> { LocalDeployerProperties properties = context.getBean(LocalDeployerProperties.class); assertThat(properties.getWorkingDirectoriesRoot()).isNotNull(); assertThat(properties.getWorkingDirectoriesRoot().toString()).isEqualTo("/tmp"); }); } @Test @EnabledOnOs(OS.LINUX) public void testCopyProperties() { LocalDeployerProperties from = new LocalDeployerProperties(); LocalDeployerProperties to = new LocalDeployerProperties(from); assertThat(from).isEqualTo(to); from = new LocalDeployerProperties(); from.setDebugPort(1234); from.setDebugSuspend(LocalDeployerProperties.DebugSuspendType.y); from.getDocker().setNetwork("network"); from.setDeleteFilesOnExit(true); from.setEnvVarsToInherit(new String[]{"foobar"}); from.setInheritLogging(false); from.getJavaHomePath().put("2", "javaCmd"); from.setJavaOpts("javaOpts"); from.setMaximumConcurrentTasks(234); from.getPortRange().setHigh(2345); from.getPortRange().setLow(2344); from.setShutdownTimeout(345); from.setUseSpringApplicationJson(false); from.setWorkingDirectoriesRoot(Paths.get("/first")); to = new LocalDeployerProperties(from); assertThat(from).isEqualTo(to); } @EnableConfigurationProperties({LocalDeployerProperties.class}) private static class Config1 { } } ================================================ FILE: spring-cloud-deployer-local/src/test/java/org/springframework/cloud/deployer/spi/local/LocalDeployerSupportTests.java ================================================ /* * Copyright 2017-2021 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.cloud.deployer.spi.local; import java.net.MalformedURLException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; /** * Tests for the AbstractLocalDeployerSupport * * @author Thomas Risberg */ public class LocalDeployerSupportTests { private LocalDeployerProperties localDeployerProperties; private AbstractLocalDeployerSupport localDeployerSupport; @BeforeEach public void setUp() { localDeployerProperties = new LocalDeployerProperties(); localDeployerSupport = new AbstractLocalDeployerSupport(this.localDeployerProperties) {}; } @Test public void testAppPropsAsSAJ() throws MalformedURLException { AppDeploymentRequest appDeploymentRequest = createAppDeploymentRequest(); HashMap envVarsToUse = new HashMap<>(appDeploymentRequest.getDefinition().getProperties()); Map environmentVariables = localDeployerSupport.formatApplicationProperties(appDeploymentRequest, envVarsToUse); assertThat(environmentVariables).hasSize(1); assertThat(environmentVariables).containsEntry(AbstractLocalDeployerSupport.SPRING_APPLICATION_JSON, "{\"test.foo\":\"foo\",\"test.bar\":\"bar\"}"); } @Test public void testCalcServerPort() throws MalformedURLException { Map applicationProperties = new HashMap<>(); Map deploymentPropertites = new HashMap<>(); List commandLineArgs = new ArrayList<>(); // test adding to application properties applicationProperties.put("server.port", "9292"); AppDefinition definition = new AppDefinition("randomApp", applicationProperties); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, testResource(), deploymentPropertites, commandLineArgs); int portToUse = localDeployerSupport.calcServerPort(appDeploymentRequest, false, new HashMap<>()); assertThat(portToUse).isEqualTo(9292); // test adding to command line args, which has higher precedence than application properties commandLineArgs.add(LocalTaskLauncher.SERVER_PORT_KEY_COMMAND_LINE_ARG + 9191); appDeploymentRequest = new AppDeploymentRequest(definition, testResource(), deploymentPropertites, commandLineArgs); portToUse = localDeployerSupport.calcServerPort(appDeploymentRequest, false, new HashMap<>()); assertThat(portToUse).isEqualTo(9191); // test using dynamic port assignment portToUse = localDeployerSupport.calcServerPort(appDeploymentRequest, true, new HashMap<>()); assertThat(portToUse).isNotEqualTo(9191); assertThat(portToUse).isNotEqualTo(9292); } @Test public void testShutdownPropertyConfiguresRequestFactory() throws Exception { LocalDeployerProperties properties = new LocalDeployerProperties(); properties.setShutdownTimeout(1); AbstractLocalDeployerSupport abstractLocalDeployerSupport = new AbstractLocalDeployerSupport(properties) {}; Object restTemplate = ReflectionTestUtils.getField(abstractLocalDeployerSupport, "restTemplate"); Object requestFactory = ReflectionTestUtils.getField(restTemplate, "requestFactory"); Object connectTimeout = ReflectionTestUtils.getField(requestFactory, "connectTimeout"); Object readTimeout = ReflectionTestUtils.getField(requestFactory, "readTimeout"); assertThat(connectTimeout).isEqualTo(1000); assertThat(readTimeout).isEqualTo(1000); } @Test public void testShutdownPropertyNotConfiguresRequestFactory() throws Exception { LocalDeployerProperties properties = new LocalDeployerProperties(); properties.setShutdownTimeout(-1); AbstractLocalDeployerSupport abstractLocalDeployerSupport = new AbstractLocalDeployerSupport(properties) {}; Object restTemplate = ReflectionTestUtils.getField(abstractLocalDeployerSupport, "restTemplate"); Object requestFactory = ReflectionTestUtils.getField(restTemplate, "requestFactory"); Object connectTimeout = ReflectionTestUtils.getField(requestFactory,"connectTimeout"); Object readTimeout = ReflectionTestUtils.getField(requestFactory, "readTimeout"); assertThat(connectTimeout).isEqualTo(-1); assertThat(readTimeout).isEqualTo(-1); } protected AppDeploymentRequest createAppDeploymentRequest() throws MalformedURLException { return createAppDeploymentRequest(new HashMap<>()); } protected AppDeploymentRequest createAppDeploymentRequest(Map depProps) throws MalformedURLException { Map appProperties = new HashMap<>(); appProperties.put("test.foo", "foo"); appProperties.put("test.bar", "bar"); AppDefinition definition = new AppDefinition("randomApp", appProperties); return new AppDeploymentRequest(definition, testResource(), depProps); } protected Resource testResource() { return new ClassPathResource("testResource.txt"); } } ================================================ FILE: spring-cloud-deployer-local/src/test/java/org/springframework/cloud/deployer/spi/local/LocalTaskLauncherIntegrationTests.java ================================================ /* * Copyright 2016-2022 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.cloud.deployer.spi.local; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Duration; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.stream.Collectors; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.io.TempDir; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.deployer.resource.docker.DockerResource; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.local.LocalTaskLauncherIntegrationTests.Config; import org.springframework.cloud.deployer.spi.task.LaunchState; import org.springframework.cloud.deployer.spi.task.TaskLauncher; import org.springframework.cloud.deployer.spi.test.AbstractIntegrationJUnit5Tests; import org.springframework.cloud.deployer.spi.test.AbstractTaskLauncherIntegrationJUnit5Tests; import org.springframework.cloud.deployer.spi.test.Timeout; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.Resource; import org.springframework.util.FileSystemUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; /** * Integration tests for {@link LocalTaskLauncher}. * * Now supports running with Docker images for tests, just set this env var: * * SPRING_CLOUD_DEPLOYER_SPI_TEST_USE_DOCKER=true * * @author Eric Bottard * @author Janne Valkealahti * @author David Turanski * @author Glenn Renfro * @author Ilayaperumal Gopinathan * @author Ben Blinebury * */ @SpringBootTest(classes = {Config.class, AbstractIntegrationJUnit5Tests.Config.class}, value = { "maven.remoteRepositories.springRepo.url=https://repo.spring.io/snapshot" }) @ExtendWith(OutputCaptureExtension.class) public class LocalTaskLauncherIntegrationTests extends AbstractTaskLauncherIntegrationJUnit5Tests { private static final String SYMBOLIC_LINK = "symbolic_link.txt"; @Autowired private TaskLauncher taskLauncher; @Value("${spring-cloud-deployer-spi-test-use-docker:false}") private boolean useDocker; @Override protected TaskLauncher provideTaskLauncher() { return taskLauncher; } @Override protected Resource testApplication() { if (useDocker) { log.info("Using Docker image for tests"); return new DockerResource("springcloud/spring-cloud-deployer-spi-test-app:latest"); } return super.testApplication(); } @Override protected String randomName() { if (LocalDeployerUtils.isWindows()) { // tweak random dir name on win to be shorter String uuid = UUID.randomUUID().toString(); long l = ByteBuffer.wrap(uuid.toString().getBytes()).getLong(); return testName + Long.toString(l, Character.MAX_RADIX); } else { return super.randomName(); } } @Test public void testPassingServerPortViaCommandLineArgs(CapturedOutput output){ Map appProperties = new HashMap<>(); appProperties.put("killDelay", "0"); appProperties.put("exitCode", "0"); AppDefinition definition = new AppDefinition(this.randomName(), appProperties); basicLaunchAndValidation(definition, null); assertThat(output).contains("Logs will be in"); } @Test public void testBasicLaunchWithSymbolicLink(CapturedOutput output) throws Exception { Map appProperties = new HashMap<>(); appProperties.put("killDelay", "0"); appProperties.put("exitCode", "0"); Path symlink = createSymbolicLink(); AppDefinition definition = new AppDefinition(this.randomName(), appProperties); Map deploymentProperties = Collections.singletonMap("spring.cloud.deployer.local.workingDirectoriesRoot", SYMBOLIC_LINK); basicLaunchAndValidation(definition, deploymentProperties); assertThat(output).contains("Logs will be in"); Files.delete(Paths.get(symlink.toString())); } @TempDir private File tempDirectory; private Path createSymbolicLink() throws IOException { File testFile = new File(tempDirectory, "testFile.txt"); Path target = testFile.toPath(); Path link = Paths.get(SYMBOLIC_LINK); if (Files.exists(link)) { Files.delete(link); } return Files.createSymbolicLink(link, target); } @Test public void testInheritLoggingAndWorkDir(CapturedOutput output) throws IOException { Map appProperties = new HashMap<>(); appProperties.put("killDelay", "0"); appProperties.put("exitCode", "0"); Map deploymentProperties = new HashMap<>(); deploymentProperties.put(LocalDeployerProperties.INHERIT_LOGGING, "true"); Path tmpPath = new File(System.getProperty("java.io.tmpdir")).toPath(); Path customWorkDirRoot = tmpPath.resolve("spring-cloud-deployer-task-workdir"); deploymentProperties.put(LocalDeployerProperties.PREFIX + ".working-directories-root", customWorkDirRoot.toFile().getAbsolutePath()); AppDefinition definition = new AppDefinition(this.randomName(), appProperties); List beforeDirs = new ArrayList<>(); beforeDirs.add(customWorkDirRoot); if (Files.exists(customWorkDirRoot)) { beforeDirs = Files.walk(customWorkDirRoot, 1) .filter(path -> Files.isDirectory(path)) .collect(Collectors.toList()); } basicLaunchAndValidation(definition, deploymentProperties); assertThat(output).contains("Logs will be inherited."); List afterDirs = Files.walk(customWorkDirRoot, 1) .filter(path -> Files.isDirectory(path)) .collect(Collectors.toList()); assertThat(afterDirs).as("Additional working directory not created").hasSize(beforeDirs.size() + 1); // clean up if test passed FileSystemUtils.deleteRecursively(customWorkDirRoot); } @Test public void testAppLogRetrieval() { Map appProperties = new HashMap<>(); appProperties.put("killDelay", "0"); appProperties.put("exitCode", "0"); AppDefinition definition = new AppDefinition(randomName(), appProperties); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); String launchId1 = taskLauncher().launch(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(taskLauncher().status(launchId1).getState()).isEqualTo(LaunchState.complete); }); String logContent = taskLauncher().getLog(launchId1); assertThat(logContent).contains("Starting DeployerIntegrationTestApplication"); } @Test public void testDeleteHistoryOnReLaunch() { Map appProperties = new HashMap<>(); appProperties.put("killDelay", "0"); appProperties.put("exitCode", "0"); AppDefinition definition = new AppDefinition(randomName(), appProperties); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); String launchId1 = taskLauncher().launch(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(taskLauncher().status(launchId1).getState()).isEqualTo(LaunchState.complete); }); String launchId2 = taskLauncher().launch(request); assertThat(launchId2).isNotEqualTo(launchId1); timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(taskLauncher().status(launchId2).getState()).isEqualTo(LaunchState.complete); }); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(taskLauncher().status(launchId1).getState()).isEqualTo(LaunchState.unknown); }); String launchId3 = taskLauncher().launch(request); assertThat(launchId3).isNotEqualTo(launchId1); assertThat(launchId3).isNotEqualTo(launchId2); timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(taskLauncher().status(launchId3).getState()).isEqualTo(LaunchState.complete); }); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(taskLauncher().status(launchId1).getState()).isEqualTo(LaunchState.unknown); }); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(taskLauncher().status(launchId2).getState()).isEqualTo(LaunchState.unknown); }); taskLauncher().destroy(definition.getName()); } private void basicLaunchAndValidation(AppDefinition definition, Map deploymentProperties) { List commandLineArgs = new ArrayList<>(1); // Test to ensure no issues parsing server.port command line arg. commandLineArgs.add(LocalTaskLauncher.SERVER_PORT_KEY_COMMAND_LINE_ARG + DeployerSocketUtils.findAvailableTcpPort(LocalTaskLauncher.DEFAULT_SERVER_PORT)); AppDeploymentRequest request = new AppDeploymentRequest(definition, this.testApplication(), deploymentProperties, commandLineArgs); this.log.info("Launching {}...", request.getDefinition().getName()); String launchId = this.taskLauncher().launch(request); Timeout timeout = this.deploymentTimeout(); await().pollInterval(Duration.ofMillis(1)) .atMost(Duration.ofSeconds(30)) .untilAsserted(() -> { assertThat(taskLauncher.getRunningTaskExecutionCount()).isEqualTo(1); }); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(taskLauncher().status(launchId).getState()).isEqualTo(LaunchState.complete); }); this.taskLauncher().destroy(definition.getName()); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(taskLauncher.getRunningTaskExecutionCount()).isEqualTo(0); }); } @Configuration @EnableConfigurationProperties(LocalDeployerProperties.class) public static class Config { @Bean public TaskLauncher taskLauncher(LocalDeployerProperties properties) { return new LocalTaskLauncher(properties); } } } ================================================ FILE: spring-cloud-deployer-local/src/test/java/org/springframework/cloud/deployer/spi/local/RandomPortRangeContextTests.java ================================================ /* * Copyright 2018-2025 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.cloud.deployer.spi.local; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import static org.assertj.core.api.Assertions.assertThat; /** * @author Christian Tzolov */ public class RandomPortRangeContextTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(LocalDeployerAutoConfiguration.class)); @Test public void defaultProtRangeProperties() { this.contextRunner .withUserConfiguration(LocalDeployerAutoConfiguration.class) .run((context) -> { assertThat(context).hasSingleBean(LocalDeployerProperties.class); assertThat(context).hasBean("localDeployerAutoConfiguration"); assertThat(context).getBean(LocalDeployerProperties.class) .hasFieldOrPropertyWithValue("portRange.low", 20000); assertThat(context).getBean(LocalDeployerProperties.class) .hasFieldOrPropertyWithValue("portRange.high", 61000); }); } @Test public void presetProtRangeProperties() { this.contextRunner .withUserConfiguration(LocalDeployerAutoConfiguration.class) .withPropertyValues("spring.cloud.deployer.local.portRange.low=20001", "spring.cloud.deployer.local.portRange.high=20003") .run((context) -> { assertThat(context).hasSingleBean(LocalDeployerProperties.class); assertThat(context).hasBean("localDeployerAutoConfiguration"); assertThat(context).getBean(LocalDeployerProperties.class) .hasFieldOrPropertyWithValue("portRange.low", 20001); assertThat(context).getBean(LocalDeployerProperties.class) .hasFieldOrPropertyWithValue("portRange.high", 20003); }); } } ================================================ FILE: spring-cloud-deployer-local/src/test/java/org/springframework/cloud/deployer/spi/local/RandomPortRangeTests.java ================================================ /* * Copyright 2018 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.cloud.deployer.spi.local; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.cloud.deployer.resource.docker.DockerResource; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.core.io.ClassPathResource; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; /** * @author Christian Tzolov */ @ExtendWith(MockitoExtension.class) public class RandomPortRangeTests { private AbstractLocalDeployerSupport localDeployerSupport; @Mock AppDeploymentRequest appDeploymentRequest; @BeforeEach public void setUp() { LocalDeployerProperties properties = new LocalDeployerProperties(); properties.getPortRange().setLow(30001); properties.getPortRange().setHigh(30213); properties.getDocker().getPortRange().setLow(40001); properties.getDocker().getPortRange().setHigh(40213); localDeployerSupport = new AbstractLocalDeployerSupport(properties) {}; } @Test public void portTests() { when(appDeploymentRequest.getResource()).thenReturn(new ClassPathResource("")); for (int i = 0; i < 30; i++) { int port = localDeployerSupport.getRandomPort(appDeploymentRequest); assertThat(port).isGreaterThanOrEqualTo(30001); assertThat(port).isLessThanOrEqualTo(30213 + 5); } } @Test public void portTests2() { when(appDeploymentRequest.getResource()).thenReturn(new DockerResource("/test:test")); for (int i = 0; i < 5; i++) { int port = localDeployerSupport.getRandomPort(appDeploymentRequest); assertThat(port).isGreaterThanOrEqualTo(40001); assertThat(port).isLessThanOrEqualTo(40213 + 5); } } } ================================================ FILE: spring-cloud-deployer-local/src/test/java/org/springframework/cloud/deployer/spi/local/RandomPortTests.java ================================================ /* * Copyright 2018 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.cloud.deployer.spi.local; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.core.io.ClassPathResource; import static org.mockito.Mockito.when; /** * @author Mark Pollack */ @ExtendWith(MockitoExtension.class) public class RandomPortTests { private AbstractLocalDeployerSupport localDeployerSupport; @Mock AppDeploymentRequest appDeploymentRequest; @BeforeEach public void setUp() { localDeployerSupport = new AbstractLocalDeployerSupport(new LocalDeployerProperties()) {}; when(appDeploymentRequest.getResource()).thenReturn(new ClassPathResource("")); } @Test public void portTests() { //No exception should be thrown for (int i = 0; i < 100; i++) { localDeployerSupport.getRandomPort(appDeploymentRequest); } } } ================================================ FILE: spring-cloud-deployer-local/src/test/resources/testResource.txt ================================================ ================================================ FILE: spring-cloud-deployer-resource-docker/pom.xml ================================================ 4.0.0 spring-cloud-deployer-resource-docker jar Spring Cloud Deployer Resource Docker org.springframework.cloud spring-cloud-deployer-parent 3.0.0-SNAPSHOT .. org.springframework spring-core org.junit.jupiter junit-jupiter test org.assertj assertj-core test ================================================ FILE: spring-cloud-deployer-resource-docker/src/main/java/org/springframework/cloud/deployer/resource/docker/DockerResource.java ================================================ /* * Copyright 2016 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.cloud.deployer.resource.docker; import java.io.IOException; import java.io.InputStream; import java.net.URI; import org.springframework.core.io.AbstractResource; import org.springframework.core.io.Resource; import org.springframework.util.Assert; /** * A {@link Resource} implementation for resolving a Docker image. * * Note: {@link #getInputStream()} throws {@code UnsupportedOperationException}. * * @author Thomas Risberg */ public class DockerResource extends AbstractResource { public final static String URI_SCHEME = "docker"; private URI uri; /** * Create a new {@code DockerResource} from an image name. * @param imageName the name of the image in a docker registry. */ public DockerResource(String imageName) { Assert.hasText(imageName, "An image name is required"); this.uri = URI.create(URI_SCHEME + ":" + imageName); } /** * Create a new {@code DockerResource} from a URI * @param uri a URI */ public DockerResource(URI uri) { Assert.notNull(uri, "A URI is required"); Assert.isTrue("docker".equals(uri.getScheme()), "A 'docker' scheme is required"); this.uri = uri; } @Override public String getDescription() { return "Docker Resource [" + uri + "]"; } /** * This implementation currently throws {@code UnsupportedOperationException} */ @Override public InputStream getInputStream() throws IOException { throw new UnsupportedOperationException("getInputStream not supported"); } @Override public URI getURI() throws IOException { return uri; } } ================================================ FILE: spring-cloud-deployer-resource-docker/src/main/java/org/springframework/cloud/deployer/resource/docker/DockerResourceLoader.java ================================================ /* * Copyright 2016 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.cloud.deployer.resource.docker; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; /** * A {@link ResourceLoader} that loads {@link DockerResource}s from locations of the format * {@literal docker::} where the value for "repository:tag" conforms to the rules * for referencing Docker images. * * @author Thomas Risberg */ public class DockerResourceLoader implements ResourceLoader { private final ClassLoader classLoader = ClassUtils.getDefaultClassLoader(); /** * Returns a {@link DockerResource} for the provided location. * * @param location the image location. May optionally be preceded by {@value DockerResource#URI_SCHEME} * followed by a colon, e.g. {@literal docker:springcloud/app-name:tag} * @return the {@link DockerResource} */ @Override public Resource getResource(String location) { Assert.hasText(location, "image location is required"); String image = location.replaceFirst(DockerResource.URI_SCHEME + ":\\/*", ""); return new DockerResource(image); } /** * Returns the {@link ClassLoader} for this ResourceLoader. */ @Override public ClassLoader getClassLoader() { return this.classLoader; } } ================================================ FILE: spring-cloud-deployer-resource-docker/src/test/java/org/springframework/cloud/deployer/resource/docker/DockerResourceLoaderTests.java ================================================ /* * Copyright 2016 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.cloud.deployer.resource.docker; import java.io.IOException; import org.springframework.core.io.Resource; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; /** * Tests for the {@link DockerResourceLoader}. * * @author Thomas Risberg */ public class DockerResourceLoaderTests { @Test public void verifyImageUri() throws IOException { String location = "docker:springcloud/test-app:v1"; DockerResourceLoader loader = new DockerResourceLoader(); Resource resource = loader.getResource(location); assertEquals(DockerResource.class, resource.getClass()); DockerResource dockerResource = (DockerResource) resource; assertEquals(location, dockerResource.getURI().toString()); assertEquals("springcloud/test-app:v1", dockerResource.getURI().getSchemeSpecificPart()); assertEquals("docker", dockerResource.getURI().getScheme().toString()); } @Test public void verifyImageUriWithSlashes() throws IOException { String location = "docker://springcloud/test-app:v1"; DockerResourceLoader loader = new DockerResourceLoader(); Resource resource = loader.getResource(location); assertEquals(DockerResource.class, resource.getClass()); DockerResource dockerResource = (DockerResource) resource; assertEquals("docker:springcloud/test-app:v1", dockerResource.getURI().toString()); assertEquals("springcloud/test-app:v1", dockerResource.getURI().getSchemeSpecificPart()); assertEquals("docker", dockerResource.getURI().getScheme().toString()); } @Test public void verifyImageUriWithoutPrefix() throws IOException { String location = "springcloud/test-app:v1"; DockerResourceLoader loader = new DockerResourceLoader(); Resource resource = loader.getResource(location); assertEquals(DockerResource.class, resource.getClass()); DockerResource dockerResource = (DockerResource) resource; assertEquals(DockerResource.URI_SCHEME + ":" + location, dockerResource.getURI().toString()); assertEquals("springcloud/test-app:v1", dockerResource.getURI().getSchemeSpecificPart()); assertEquals("docker", dockerResource.getURI().getScheme().toString()); } } ================================================ FILE: spring-cloud-deployer-resource-docker/src/test/java/org/springframework/cloud/deployer/resource/docker/DockerResourceTests.java ================================================ /* * Copyright 2016 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.cloud.deployer.resource.docker; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import org.junit.jupiter.api.Test; import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; import static org.junit.jupiter.api.Assertions.assertEquals; /** * Tests for the {@link DockerResource}. * * @author Thomas Risberg */ public class DockerResourceTests { String image = "sringcloud/hello-kube:latest"; @Test public void testResource() throws IOException, URISyntaxException { DockerResource r = new DockerResource(image); assertEquals(image, r.getURI().getSchemeSpecificPart()); } @Test public void testUri() throws IOException, URISyntaxException { DockerResource r = new DockerResource(URI.create(DockerResource.URI_SCHEME + ":" + image)); assertEquals(image, r.getURI().getSchemeSpecificPart()); } @Test public void testInvalidUri() { assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> { DockerResource r = new DockerResource(URI.create("http:" + image)); }); } } ================================================ FILE: spring-cloud-deployer-resource-maven/pom.xml ================================================ 4.0.0 spring-cloud-deployer-resource-maven jar Spring Cloud Deployer Resource Maven org.springframework.cloud spring-cloud-deployer-parent 3.0.0-SNAPSHOT .. org.springframework spring-core org.apache.maven maven-model-builder ${maven.version} javax.inject javax.inject org.apache.maven maven-resolver-provider ${maven.version} javax.inject javax.inject org.apache.maven.resolver maven-resolver-connector-basic ${maven-resolver.version} org.apache.maven.resolver maven-resolver-transport-file ${maven-resolver.version} org.apache.maven.resolver maven-resolver-transport-http ${maven-resolver.version} org.apache.maven.resolver maven-resolver-transport-wagon ${maven-resolver.version} org.apache.maven.resolver maven-resolver-impl ${maven-resolver.version} commons-io commons-io org.apache.maven.wagon wagon-http org.slf4j slf4j-api org.springframework.boot spring-boot-starter-web test org.springframework.boot spring-boot-starter-test test org.springframework.security spring-security-web test org.springframework.security spring-security-config test ================================================ FILE: spring-cloud-deployer-resource-maven/src/main/java/org/springframework/cloud/deployer/resource/maven/LoggingRepositoryListener.java ================================================ /* * Copyright 2019 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.cloud.deployer.resource.maven; import org.eclipse.aether.AbstractRepositoryListener; import org.eclipse.aether.RepositoryEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author Corneil du Plessis */ public class LoggingRepositoryListener extends AbstractRepositoryListener { private static final Logger logger = LoggerFactory.getLogger(LoggingRepositoryListener.class); public void artifactDeployed(RepositoryEvent event) { println("artifactDeployed", event.getArtifact() + " to " + event.getRepository()); } public void artifactDeploying(RepositoryEvent event) { println("artifactDeploying", event.getArtifact() + " to " + event.getRepository()); } public void artifactDescriptorInvalid(RepositoryEvent event) { println("artifactDescriptorInvalid", "for " + event.getArtifact() + ": " + event.getException().getMessage()); } public void artifactDescriptorMissing(RepositoryEvent event) { println("artifactDescriptorMissing", "for " + event.getArtifact()); } public void artifactInstalled(RepositoryEvent event) { println("artifactInstalled", event.getArtifact() + " to " + event.getFile()); } public void artifactInstalling(RepositoryEvent event) { println("artifactInstalling", event.getArtifact() + " to " + event.getFile()); } public void artifactResolved(RepositoryEvent event) { println("artifactResolved", event.getArtifact() + " from " + event.getRepository()); } public void artifactDownloading(RepositoryEvent event) { println("artifactDownloading", event.getArtifact() + " from " + event.getRepository()); } public void artifactDownloaded(RepositoryEvent event) { println("artifactDownloaded", event.getArtifact() + " from " + event.getRepository()); } public void artifactResolving(RepositoryEvent event) { println("artifactResolving", event.getArtifact().toString()); } public void metadataDeployed(RepositoryEvent event) { println("metadataDeployed", event.getMetadata() + " to " + event.getRepository()); } public void metadataDeploying(RepositoryEvent event) { println("metadataDeploying", event.getMetadata() + " to " + event.getRepository()); } public void metadataInstalled(RepositoryEvent event) { println("metadataInstalled", event.getMetadata() + " to " + event.getFile()); } public void metadataInstalling(RepositoryEvent event) { println("metadataInstalling", event.getMetadata() + " to " + event.getFile()); } public void metadataInvalid(RepositoryEvent event) { println("metadataInvalid", event.getMetadata().toString()); } public void metadataResolved(RepositoryEvent event) { println("metadataResolved", event.getMetadata() + " from " + event.getRepository()); } public void metadataResolving(RepositoryEvent event) { println("metadataResolving", event.getMetadata() + " from " + event.getRepository()); } private void println(String event, String message) { logger.info("Aether Repository - " + event + ": " + message); } } ================================================ FILE: spring-cloud-deployer-resource-maven/src/main/java/org/springframework/cloud/deployer/resource/maven/MavenArtifactResolver.java ================================================ /* * Copyright 2015-2023 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.cloud.deployer.resource.maven; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.stream.Collectors; import org.apache.maven.repository.internal.MavenRepositorySystemUtils; import org.eclipse.aether.ConfigurationProperties; import org.eclipse.aether.DefaultRepositorySystemSession; import org.eclipse.aether.RepositorySystem; import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.artifact.DefaultArtifact; import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory; import org.eclipse.aether.impl.DefaultServiceLocator; import org.eclipse.aether.repository.Authentication; import org.eclipse.aether.repository.AuthenticationContext; import org.eclipse.aether.repository.AuthenticationDigest; import org.eclipse.aether.repository.LocalRepository; import org.eclipse.aether.repository.Proxy; import org.eclipse.aether.repository.RemoteRepository; import org.eclipse.aether.repository.RepositoryPolicy; import org.eclipse.aether.resolution.ArtifactRequest; import org.eclipse.aether.resolution.ArtifactResolutionException; import org.eclipse.aether.resolution.ArtifactResult; import org.eclipse.aether.resolution.VersionRangeRequest; import org.eclipse.aether.resolution.VersionRangeResolutionException; import org.eclipse.aether.resolution.VersionRangeResult; import org.eclipse.aether.spi.connector.RepositoryConnectorFactory; import org.eclipse.aether.spi.connector.transport.TransporterFactory; import org.eclipse.aether.transport.file.FileTransporterFactory; import org.eclipse.aether.transport.http.HttpTransporterFactory; import org.eclipse.aether.transport.wagon.WagonConfigurator; import org.eclipse.aether.transport.wagon.WagonProvider; import org.eclipse.aether.transport.wagon.WagonTransporterFactory; import org.eclipse.aether.util.artifact.JavaScopes; import org.eclipse.aether.util.repository.DefaultProxySelector; import org.eclipse.aether.version.Version; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.util.Assert; /** * Resolves a {@link MavenResource} using remoteRepositories = new LinkedList<>(); private final Authentication proxyAuthentication; /** * Create an instance using the provided properties. * * @param properties the properties for the maven repositories, proxies, and authentication */ public MavenArtifactResolver(MavenProperties properties) { Assert.notNull(properties, "MavenProperties must not be null"); Assert.notNull(properties.getLocalRepository(), "Local repository path cannot be null"); this.properties = properties; if (logger.isDebugEnabled()) { logger.debug("Configured local repository: " + properties.getLocalRepository()); logger.debug("Configured remote repositories: " + configuredRemoteRepositoriesDescription()); } if (isProxyEnabled() && proxyHasCredentials()) { final String username = this.properties.getProxy().getAuth().getUsername(); final String password = this.properties.getProxy().getAuth().getPassword(); this.proxyAuthentication = newAuthentication(username, password); } else { this.proxyAuthentication = null; } File localRepository = new File(this.properties.getLocalRepository()); if (!localRepository.exists()) { boolean created = localRepository.mkdirs(); // May have been created by another thread after above check. Double check. Assert.isTrue(created || localRepository.exists(), "Unable to create directory for local repository: " + localRepository); } Map defaultRepoUrlsToIds = defaultRemoteRepos(); for (Map.Entry entry : this.properties.getRemoteRepositories() .entrySet()) { MavenProperties.RemoteRepository remoteRepository = entry.getValue(); RemoteRepository.Builder remoteRepositoryBuilder = new RemoteRepository.Builder( entry.getKey(), DEFAULT_CONTENT_TYPE, remoteRepository.getUrl()); // Update policies when set. if (remoteRepository.getPolicy() != null) { remoteRepositoryBuilder.setPolicy(new RepositoryPolicy(remoteRepository.getPolicy().isEnabled(), remoteRepository.getPolicy().getUpdatePolicy(), remoteRepository.getPolicy().getChecksumPolicy())); } if (remoteRepository.getReleasePolicy() != null) { remoteRepositoryBuilder .setReleasePolicy(new RepositoryPolicy(remoteRepository.getReleasePolicy().isEnabled(), remoteRepository.getReleasePolicy().getUpdatePolicy(), remoteRepository.getReleasePolicy().getChecksumPolicy())); } if (remoteRepository.getSnapshotPolicy() != null) { remoteRepositoryBuilder .setSnapshotPolicy(new RepositoryPolicy(remoteRepository.getSnapshotPolicy().isEnabled(), remoteRepository.getSnapshotPolicy().getUpdatePolicy(), remoteRepository.getSnapshotPolicy().getChecksumPolicy())); } if (remoteRepositoryHasCredentials(remoteRepository)) { final String username = remoteRepository.getAuth().getUsername(); final String password = remoteRepository.getAuth().getPassword(); remoteRepositoryBuilder.setAuthentication(newAuthentication(username, password)); } // do not add default repo if explicitly configured defaultRepoUrlsToIds.remove(remoteRepository.getUrl()); RemoteRepository repo = proxyRepoIfProxyEnabled(remoteRepositoryBuilder.build()); this.remoteRepositories.add(repo); } if (!defaultRepoUrlsToIds.isEmpty() && this.properties.isIncludeDefaultRemoteRepos()) { List defaultRepos = new ArrayList<>(); defaultRepoUrlsToIds.forEach((url, id) -> { if (logger.isDebugEnabled()) { logger.debug("Adding {} ({}) to remote repositories list", id, url); } RemoteRepository defaultRepo = proxyRepoIfProxyEnabled(new RemoteRepository.Builder(id, DEFAULT_CONTENT_TYPE, url).build()); defaultRepos.add(defaultRepo); }); this.remoteRepositories.addAll(0, defaultRepos); } if (logger.isDebugEnabled()) { logger.debug("Using remote repositories: {}", actualRemoteRepositoriesDescription()); } this.repositorySystem = newRepositorySystem(); } /** * Gets the default repos to automatically add. * @return map of default repos (repo url to repo id) */ protected Map defaultRemoteRepos() { Map defaultRepos = new LinkedHashMap<>(); defaultRepos.put("https://repo.maven.apache.org/maven2", "mavenCentral-default"); defaultRepos.put("https://repo.spring.io/snapshot", "springSnapshot-default"); defaultRepos.put("https://repo.spring.io/milestone", "springMilestone-default"); return defaultRepos; } private RemoteRepository proxyRepoIfProxyEnabled(RemoteRepository remoteRepo) { if (!isProxyEnabled()) { return remoteRepo; } Proxy proxy; MavenProperties.Proxy proxyProperties = this.properties.getProxy(); if (this.proxyAuthentication != null) { proxy = new Proxy( proxyProperties.getProtocol(), proxyProperties.getHost(), proxyProperties.getPort(), this.proxyAuthentication); } else { // if proxy does not require authentication proxy = new Proxy( proxyProperties.getProtocol(), proxyProperties.getHost(), proxyProperties.getPort()); } DefaultProxySelector proxySelector = new DefaultProxySelector(); proxySelector.add(proxy, this.properties.getProxy().getNonProxyHosts()); proxy = proxySelector.getProxy(remoteRepo); RemoteRepository.Builder remoteRepositoryBuilder = new RemoteRepository.Builder(remoteRepo); remoteRepositoryBuilder.setProxy(proxy); return remoteRepositoryBuilder.build(); } /** * Check if the proxy settings are provided. * * @return boolean true if the proxy settings are provided. */ private boolean isProxyEnabled() { return (this.properties.getProxy() != null && this.properties.getProxy().getHost() != null && this.properties.getProxy().getPort() > 0); } /** * Check if the proxy setting has username/password set. * * @return boolean true if both the username/password are set */ private boolean proxyHasCredentials() { return (this.properties.getProxy() != null && this.properties.getProxy().getAuth() != null && this.properties.getProxy().getAuth().getUsername() != null && this.properties.getProxy().getAuth().getPassword() != null); } /** * Check if the {@link MavenProperties.RemoteRepository} setting has username/password set. * * @return boolean true if both the username/password are set */ private boolean remoteRepositoryHasCredentials(MavenProperties.RemoteRepository remoteRepository) { return remoteRepository != null && remoteRepository.getAuth() != null && remoteRepository.getAuth().getUsername() != null && remoteRepository.getAuth().getPassword() != null; } /** * Create an {@link Authentication} given a username/password * * @param username * @param password * @return a configured {@link Authentication} */ private Authentication newAuthentication(final String username, final String password) { return new Authentication() { @Override public void fill(AuthenticationContext context, String key, Map data) { context.put(AuthenticationContext.USERNAME, username); context.put(AuthenticationContext.PASSWORD, password); } @Override public void digest(AuthenticationDigest digest) { digest.update(AuthenticationContext.USERNAME, username, AuthenticationContext.PASSWORD, password); } }; } DefaultRepositorySystemSession newRepositorySystemSession() { return this.newRepositorySystemSession(this.repositorySystem, this.properties.getLocalRepository()); } /* * Create a session to manage remote and local synchronization. */ private DefaultRepositorySystemSession newRepositorySystemSession(RepositorySystem system, String localRepoPath) { DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession(); LocalRepository localRepo = new LocalRepository(localRepoPath); session.setLocalRepositoryManager(system.newLocalRepositoryManager(session, localRepo)); session.setOffline(this.properties.isOffline()); session.setUpdatePolicy(this.properties.getUpdatePolicy()); session.setChecksumPolicy(this.properties.getChecksumPolicy()); if (this.properties.isEnableRepositoryListener()) { session.setRepositoryListener(new LoggingRepositoryListener()); } if (this.properties.getConnectTimeout() != null) { session.setConfigProperty(ConfigurationProperties.CONNECT_TIMEOUT, this.properties.getConnectTimeout()); } if (this.properties.getRequestTimeout() != null) { session.setConfigProperty(ConfigurationProperties.REQUEST_TIMEOUT, this.properties.getRequestTimeout()); } if (isProxyEnabled()) { DefaultProxySelector proxySelector = new DefaultProxySelector(); Proxy proxy = new Proxy(this.properties.getProxy().getProtocol(), this.properties.getProxy().getHost(), this.properties.getProxy().getPort(), this.proxyAuthentication); proxySelector.add(proxy, this.properties.getProxy().getNonProxyHosts()); session.setProxySelector(proxySelector); } // wagon configs for (Entry entry : this.properties.getRemoteRepositories().entrySet()) { session.setConfigProperty("aether.connector.wagon.config." + entry.getKey(), entry.getValue().getWagon()); } return session; } /* * Aether's components implement {@link org.eclipse.aether.spi.locator.Service} to ease manual wiring. * Using the prepopulated {@link DefaultServiceLocator}, we need to register the repository connector * and transporter factories */ private RepositorySystem newRepositorySystem() { DefaultServiceLocator locator = MavenRepositorySystemUtils.newServiceLocator(); locator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class); locator.addService(TransporterFactory.class, FileTransporterFactory.class); if (properties.isUseWagon()) { locator.addService(WagonProvider.class, StaticWagonProvider.class); locator.addService(WagonConfigurator.class, StaticWagonConfigurator.class); locator.addService(TransporterFactory.class, WagonTransporterFactory.class); } else { locator.addService(TransporterFactory.class, HttpTransporterFactory.class); } locator.setErrorHandler(new DefaultServiceLocator.ErrorHandler() { @Override public void serviceCreationFailed(Class type, Class impl, Throwable exception) { throw new RuntimeException(exception); } }); return locator.getService(RepositorySystem.class); } /** * Gets the list of configured remote repositories * @return unmodifiable list of configured remote repositories */ List remoteRepositories() { return Collections.unmodifiableList(this.remoteRepositories); } private String actualRemoteRepositoriesDescription() { return this.remoteRepositories.stream().map((repo) -> String.format("%s (%s)", repo.getId(), repo.getUrl())) .collect(Collectors.joining(", ", "[", "]")); } private String configuredRemoteRepositoriesDescription() { return this.properties.getRemoteRepositories().entrySet().stream() .map((e) -> String.format("%s (%s)", e.getKey(), e.getValue().getUrl())) .collect(Collectors.joining(", ", "[", "]")); } List getVersions(String coordinates) { Artifact artifact = new DefaultArtifact(coordinates); VersionRangeRequest rangeRequest = new VersionRangeRequest(); rangeRequest.setArtifact(artifact); rangeRequest.setRepositories(this.remoteRepositories); try { VersionRangeResult versionResult = this.repositorySystem.resolveVersionRange(newRepositorySystemSession(), rangeRequest); List versions = new ArrayList<>(); for (Version version: versionResult.getVersions()) { versions.add(version.toString()); } return versions; } catch (VersionRangeResolutionException e) { throw new IllegalStateException(e); } } /** * Resolve an artifact and return its location in the local repository. Aether performs the normal * Maven resolution process ensuring that the latest update is cached to the local repository. * In addition, if the {@code MavenProperties.resolvePom} flag is true, * the POM is also resolved and cached. * @param resource the {@link MavenResource} representing the artifact * @return a {@link FileSystemResource} representing the resolved artifact in the local repository * @throws IllegalStateException if the artifact does not exist or the resolution fails */ Resource resolve(MavenResource resource) { Assert.notNull(resource, "MavenResource must not be null"); validateCoordinates(resource); RepositorySystemSession session = newRepositorySystemSession(this.repositorySystem, this.properties.getLocalRepository()); try { List artifactRequests = new ArrayList<>(2); if (properties.isResolvePom()) { artifactRequests.add(new ArtifactRequest(toPomArtifact(resource), this.remoteRepositories, JavaScopes.RUNTIME)); } artifactRequests.add(new ArtifactRequest(toJarArtifact(resource), this.remoteRepositories, JavaScopes.RUNTIME)); List results = this.repositorySystem.resolveArtifacts(session, artifactRequests); return toResource(results.get(results.size() - 1)); } catch (ArtifactResolutionException ex) { String errorMsg = String.format("Failed to resolve %s using remote repo(s): %s", resource, actualRemoteRepositoriesDescription()); throw new IllegalStateException(errorMsg, ex); } } private void validateCoordinates(MavenResource resource) { Assert.hasText(resource.getGroupId(), "groupId must not be blank."); Assert.hasText(resource.getArtifactId(), "artifactId must not be blank."); Assert.hasText(resource.getExtension(), "extension must not be blank."); Assert.hasText(resource.getVersion(), "version must not be blank."); } public FileSystemResource toResource(ArtifactResult resolvedArtifact) { return new FileSystemResource(resolvedArtifact.getArtifact().getFile()); } private Artifact toJarArtifact(MavenResource resource) { return toArtifact(resource, resource.getExtension()); } private Artifact toPomArtifact(MavenResource resource) { return toArtifact(resource, "pom"); } private Artifact toArtifact(MavenResource resource, String extension) { return new DefaultArtifact(resource.getGroupId(), resource.getArtifactId(), resource.getClassifier() != null ? resource.getClassifier() : "", extension, resource.getVersion()); } } ================================================ FILE: spring-cloud-deployer-resource-maven/src/main/java/org/springframework/cloud/deployer/resource/maven/MavenProperties.java ================================================ /* * Copyright 2015-2017 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.cloud.deployer.resource.maven; import java.io.File; import java.util.HashMap; import java.util.Map; import java.util.TreeMap; /** * Configuration Properties for Maven. * * @author Ilayaperumal Gopinathan * @author Eric Bottard * @author Mark Fisher * @author Donovan Muller */ public class MavenProperties { /** * Default file path to a locally available maven repository. */ private static String DEFAULT_LOCAL_REPO = System.getProperty("user.home") + File.separator + ".m2" + File.separator + "repository"; /** * Whether default remote repositories should be automatically included in the list of remote repositories. */ private boolean includeDefaultRemoteRepos = true; /** * File path to a locally available maven repository, where artifacts will be downloaded. */ private String localRepository = DEFAULT_LOCAL_REPO; /** * Locations of remote maven repositories from which artifacts will be downloaded, if not available locally. */ private Map remoteRepositories = new TreeMap<>(); /** * Whether the resolver should operate in offline mode. */ private boolean offline; /** * Proxy configuration properties. */ private Proxy proxy; /** * The connect timeout. If null, the underlying default will be used. * @see {@link org.eclipse.aether.ConfigurationProperties#CONNECT_TIMEOUT} */ private Integer connectTimeout; /** * The request timeout. If null, the underlying default will be used. * @see {@link org.eclipse.aether.ConfigurationProperties#REQUEST_TIMEOUT} */ private Integer requestTimeout; /** * In addition to resolving the JAR artifact, if true, resolve the POM artifact. * This is consistent with the way that Maven resolves artifacts. */ private boolean resolvePom; /** * Add the ConsoleRepositoryListener to the session for debugging of artifact resolution. */ private boolean enableRepositoryListener = false; boolean isIncludeDefaultRemoteRepos() { return includeDefaultRemoteRepos; } void setIncludeDefaultRemoteRepos(boolean includeDefaultRemoteRepos) { this.includeDefaultRemoteRepos = includeDefaultRemoteRepos; } /** * Use maven wagon based transport for http based artifacts. */ private boolean useWagon; public void setUseWagon(boolean useWagon) { this.useWagon = useWagon; } public boolean isUseWagon() { return useWagon; } public boolean isEnableRepositoryListener() { return enableRepositoryListener; } public void setEnableRepositoryListener(boolean enableRepositoryListener) { this.enableRepositoryListener = enableRepositoryListener; } public String updatePolicy; public String checksumPolicy; public String getUpdatePolicy() { return updatePolicy; } public void setUpdatePolicy(String updatePolicy) { this.updatePolicy = updatePolicy; } public String getChecksumPolicy() { return checksumPolicy; } public void setChecksumPolicy(String checksumPolicy) { this.checksumPolicy = checksumPolicy; } public Map getRemoteRepositories() { return remoteRepositories; } public void setRemoteRepositories(final Map remoteRepositories) { this.remoteRepositories = new TreeMap<>(remoteRepositories); } public void setLocalRepository(String localRepository) { this.localRepository = localRepository; } public String getLocalRepository() { return localRepository; } public boolean isOffline() { return offline; } public void setOffline(Boolean offline) { this.offline = offline; } public Integer getConnectTimeout() { return this.connectTimeout; } public void setConnectTimeout(Integer connectTimeout) { this.connectTimeout = connectTimeout; } public Integer getRequestTimeout() { return this.requestTimeout; } public void setRequestTimeout(Integer requestTimeout) { this.requestTimeout = requestTimeout; } public Proxy getProxy() { return this.proxy; } public void setProxy(Proxy proxy) { this.proxy = proxy; } public boolean isResolvePom() { return resolvePom; } public void setResolvePom(final boolean resolvePom) { this.resolvePom = resolvePom; } public static class Proxy { /** * Protocol to use for proxy settings. */ private String protocol = "http"; /** * Host for the proxy. */ private String host; /** * Port for the proxy. */ private int port; /** * List of non proxy hosts. */ private String nonProxyHosts; private Authentication auth; public String getProtocol() { return this.protocol; } public void setProtocol(String protocol) { this.protocol = protocol; } public String getHost() { return this.host; } public void setHost(String host) { this.host = host; } public int getPort() { return this.port; } public void setPort(int port) { this.port = port; } public String getNonProxyHosts() { return this.nonProxyHosts; } public void setNonProxyHosts(String nonProxyHosts) { this.nonProxyHosts = nonProxyHosts; } public Authentication getAuth() { return this.auth; } public void setAuth(Authentication auth) { this.auth = auth; } } public static enum WagonHttpMethod { // directly maps to http methods in org.apache.maven.wagon.shared.http.HttpConfiguration all, get, put, head; } public static class WagonHttpMethodProperties { // directly maps to settings in org.apache.maven.wagon.shared.http.HttpMethodConfiguration private boolean usePreemptive; private boolean useDefaultHeaders; private Integer connectionTimeout; private Integer readTimeout; private Map headers = new HashMap<>(); private Map params = new HashMap<>(); public boolean isUsePreemptive() { return usePreemptive; } public void setUsePreemptive(boolean usePreemptive) { this.usePreemptive = usePreemptive; } public boolean isUseDefaultHeaders() { return useDefaultHeaders; } public void setUseDefaultHeaders(boolean useDefaultHeaders) { this.useDefaultHeaders = useDefaultHeaders; } public Integer getConnectionTimeout() { return connectionTimeout; } public void setConnectionTimeout(Integer connectionTimeout) { this.connectionTimeout = connectionTimeout; } public Integer getReadTimeout() { return readTimeout; } public void setReadTimeout(Integer readTimeout) { this.readTimeout = readTimeout; } public Map getHeaders() { return headers; } public void setHeaders(Map headers) { this.headers = headers; } public Map getParams() { return params; } public void setParams(Map params) { this.params = params; } } public static class Wagon { private Map http = new HashMap<>(); public Map getHttp() { return http; } public void setHttp(Map http) { this.http = http; } } public static class RemoteRepository { /** * URL of the remote maven repository. E.g. https://my.repo.com */ private String url; private Authentication auth; private RepositoryPolicy policy; private RepositoryPolicy snapshotPolicy; private RepositoryPolicy releasePolicy; private Wagon wagon = new Wagon(); public RemoteRepository() { } public RemoteRepository(final String url) { this.url = url; } public RemoteRepository(final String url, final Authentication auth) { this.url = url; this.auth = auth; } public Wagon getWagon() { return wagon; } public void setWagon(Wagon wagon) { this.wagon = wagon; } public String getUrl() { return url; } public void setUrl(final String url) { this.url = url; } public Authentication getAuth() { return auth; } public void setAuth(final Authentication auth) { this.auth = auth; } public RepositoryPolicy getPolicy() { return policy; } public void setPolicy(RepositoryPolicy policy) { this.policy = policy; } public RepositoryPolicy getSnapshotPolicy() { return snapshotPolicy; } public void setSnapshotPolicy(RepositoryPolicy snapshotPolicy) { this.snapshotPolicy = snapshotPolicy; } public RepositoryPolicy getReleasePolicy() { return releasePolicy; } public void setReleasePolicy(RepositoryPolicy releasePolicy) { this.releasePolicy = releasePolicy; } } public static class RepositoryPolicy { private boolean enabled = true; private String updatePolicy = "always"; private String checksumPolicy = "warn"; public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public String getUpdatePolicy() { return updatePolicy; } public void setUpdatePolicy(String updatePolicy) { this.updatePolicy = updatePolicy; } public String getChecksumPolicy() { return checksumPolicy; } public void setChecksumPolicy(String checksumPolicy) { this.checksumPolicy = checksumPolicy; } } public static class Authentication { private String username; private String password; public Authentication() { } public Authentication(String username, String password) { this.username = username; this.password = password; } public String getUsername() { return this.username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return this.password; } public void setPassword(String password) { this.password = password; } } } ================================================ FILE: spring-cloud-deployer-resource-maven/src/main/java/org/springframework/cloud/deployer/resource/maven/MavenResource.java ================================================ /* * Copyright 2015-2016 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.cloud.deployer.resource.maven; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.springframework.core.io.AbstractResource; import org.springframework.core.io.Resource; import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** * A {@link Resource} implementation for resolving an artifact via maven coordinates. *

* The {@code MavenResource} class contains * Maven coordinates for a jar file containing an app/library, or a Bill of Materials pom. *

* To create a new instance, either use {@link Builder} to set the individual fields: *

 * new MavenResource.Builder()
 *     .setGroupId("org.springframework.sample")
 *     .setArtifactId("some-app")
 *     .setExtension("jar") //optional
 *     .setClassifier("exec") //optional
 *     .setVersion("2.0.0")
 *     .build()
 * 
* ...or use {@link #parse(String)} to parse the coordinates as a colon delimited string: * <groupId>:<artifactId>[:<extension>[:<classifier>]]:<version> *
 * MavenResource.parse("org.springframework.sample:some-app:2.0.0);
 * MavenResource.parse("org.springframework.sample:some-app:jar:exec:2.0.0);
 * 
* @author David Turanski * @author Mark Fisher * @author Patrick Peralta * @author Venil Noronha * @author Ilayaperumal Gopinathan */ public class MavenResource extends AbstractResource { public static String URI_SCHEME = "maven"; /** * The default extension for the artifact. */ final static String DEFAULT_EXTENSION = "jar"; /** * String representing an empty classifier. */ final static String EMPTY_CLASSIFIER = ""; /** * Group ID for artifact; generally this includes the name of the * organization that generated the artifact. */ private final String groupId; /** * Artifact ID; generally this includes the name of the app or library. */ private final String artifactId; /** * Extension of the artifact. */ private final String extension; /** * Classifier of the artifact. */ private final String classifier; /** * Version of the artifact. */ private final String version; private final MavenArtifactResolver resolver; /** * Construct a {@code MavenResource} object. * * @param groupId group ID for artifact * @param artifactId artifact ID * @param extension the file extension * @param classifier artifact classifier - can be null * @param version artifact version * @param properties Maven configuration properties */ private MavenResource(String groupId, String artifactId, String extension, String classifier, String version, MavenProperties properties) { Assert.hasText(groupId, "groupId must not be blank"); Assert.hasText(artifactId, "artifactId must not be blank"); Assert.hasText(extension, "extension must not be blank"); Assert.hasText(version, "version must not be blank"); this.groupId = groupId; this.artifactId = artifactId; this.extension = extension; this.classifier = classifier == null ? EMPTY_CLASSIFIER : classifier; this.version = version; this.resolver = new MavenArtifactResolver(properties != null ? properties : new MavenProperties()); } /** * @see #groupId * @return the group id of the maven resource */ public String getGroupId() { return groupId; } /** * @see #artifactId * @return the artifact id of the maven resource */ public String getArtifactId() { return artifactId; } /** * @see #extension * @return the extension of the maven resource */ public String getExtension() { return extension; } /** * @see #version * @return the classifier of the maven resource */ public String getClassifier() { return classifier; } /** * @see #version * @return the version of the maven resource */ public String getVersion() { return version; } @Override public String getDescription() { return this.toString(); } @Override public InputStream getInputStream() throws IOException { return resolver.resolve(this).getInputStream(); } @Override public File getFile() throws IOException { return resolver.resolve(this).getFile(); } @Override public String getFilename() { return StringUtils.hasLength(classifier) ? String.format("%s-%s-%s.%s", artifactId, version, classifier, extension) : String.format("%s-%s.%s", artifactId, version, extension); } @Override public boolean exists() { try { return super.exists(); } catch (Exception e) { // Resource.exists() has no throws clause, so return false return false; } } @Override public final boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof MavenResource)) { return false; } MavenResource that = (MavenResource) o; return this.groupId.equals(that.groupId) && this.artifactId.equals(that.artifactId) && this.extension.equals(that.extension) && this.classifier.equals(that.classifier) && this.version.equals(that.version); } @Override public int hashCode() { int result = groupId.hashCode(); result = 31 * result + artifactId.hashCode(); result = 31 * result + extension.hashCode(); if (StringUtils.hasLength(classifier)) { result = 31 * result + classifier.hashCode(); } result = 31 * result + version.hashCode(); return result; } /** * Returns the coordinates encoded as * <groupId>:<artifactId>[:<extension>[:<classifier>]]:<version>, * conforming to the Aether convention. */ @Override public String toString() { return StringUtils.hasLength(classifier) ? String.format("%s:%s:%s:%s:%s", groupId, artifactId, extension, classifier, version) : String.format("%s:%s:%s:%s", groupId, artifactId, extension, version); } @Override public URI getURI() throws IOException { return URI.create(URI_SCHEME + "://" + toString()); } /** * Create a {@link MavenResource} for the provided coordinates and default properties. * * @param coordinates coordinates encoded as <groupId>:<artifactId>[:<extension>[:<classifier>]]:<version>, * conforming to the Aether convention. * @return the {@link MavenResource} */ public static MavenResource parse(String coordinates) { return parse(coordinates, null); } /** * Create a {@link MavenResource} for the provided coordinates and properties. * * @param coordinates coordinates encoded as <groupId>:<artifactId>[:<extension>[:<classifier>]]:<version>, * conforming to the Aether convention. * @param properties the properties for the repositories, proxies, and authentication * @return the {@link MavenResource} */ public static MavenResource parse(String coordinates, MavenProperties properties) { Assert.hasText(coordinates, "coordinates are required"); Pattern p = Pattern.compile("([^: ]+):([^: ]+)(:([^: ]*)(:([^: ]+))?)?:([^: ]+)"); Matcher m = p.matcher(coordinates); Assert.isTrue(m.matches(), "Bad artifact coordinates " + coordinates + ", expected format is :[:[:]]:"); String groupId = m.group(1); String artifactId = m.group(2); String extension = StringUtils.hasLength(m.group(4)) ? m.group(4) : DEFAULT_EXTENSION; String classifier = StringUtils.hasLength(m.group(6)) ? m.group(6) : EMPTY_CLASSIFIER; String version = m.group(7); return new MavenResource(groupId, artifactId, extension, classifier, version, properties); } /** * Get all the available versions on this maven co-ordinate. * @param coordinates the co-ordinate with the version constraint added. * Example: org.springframework.cloud.stream.app:http-source-rabbit:[0,) * @return the list of all the available versions */ public List getVersions(String coordinates) { return this.resolver.getVersions(coordinates); } public static class Builder { private String groupId; private String artifactId; private String extension = DEFAULT_EXTENSION; private String classifier = EMPTY_CLASSIFIER; private String version; private final MavenProperties properties; public Builder() { this(null); } public Builder(MavenProperties properties) { this.properties = properties; } public Builder groupId(String groupId) { this.groupId = groupId; return this; } public Builder artifactId(String artifactId) { this.artifactId = artifactId; return this; } public Builder extension(String extension) { this.extension = extension; return this; } public Builder classifier(String classifier) { this.classifier = classifier; return this; } public Builder version(String version) { this.version = version; return this; } public MavenResource build() { return new MavenResource(groupId, artifactId, extension, classifier, version, properties); } } } ================================================ FILE: spring-cloud-deployer-resource-maven/src/main/java/org/springframework/cloud/deployer/resource/maven/MavenResourceLoader.java ================================================ /* * Copyright 2016 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.cloud.deployer.resource.maven; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; /** * A {@link ResourceLoader} that loads {@link MavenResource}s from locations of the format * {@literal maven://} where the value for "coordinates" conforms to the rules * described on {@link MavenResource#parse(String)} * * @author Mark Fisher */ public class MavenResourceLoader implements ResourceLoader { private static final String URI_SCHEME = "maven"; private final MavenProperties properties; private final ClassLoader classLoader = ClassUtils.getDefaultClassLoader(); /** * Create a {@link MavenResourceLoader} that uses the provided {@link MavenProperties}. * * @param properties the {@link MavenProperties} to use when instantiating {@link MavenResource}s */ public MavenResourceLoader(MavenProperties properties) { Assert.notNull(properties, "MavenProperties must not be null"); this.properties = properties; } /** * Returns a {@link MavenResource} for the provided location. * * @param location the coordinates conforming to the rules described on * {@link MavenResource#parse(String)}. May optionally be preceded by {@value #URI_SCHEME} * followed by a colon and zero or more forward slashes, e.g. * {@literal maven://group:artifact:version} * @return the {@link MavenResource} */ @Override public Resource getResource(String location) { Assert.hasText(location, "location is required"); String coordinates = location.replaceFirst(URI_SCHEME + ":\\/*", ""); return MavenResource.parse(coordinates, this.properties); } /** * Returns the {@link ClassLoader} for this ResourceLoader. */ @Override public ClassLoader getClassLoader() { return this.classLoader; } } ================================================ FILE: spring-cloud-deployer-resource-maven/src/main/java/org/springframework/cloud/deployer/resource/maven/StaticWagonConfigurator.java ================================================ /* * Copyright 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.cloud.deployer.resource.maven; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import org.apache.maven.wagon.Wagon; import org.apache.maven.wagon.providers.http.HttpWagon; import org.apache.maven.wagon.shared.http.HttpConfiguration; import org.apache.maven.wagon.shared.http.HttpMethodConfiguration; import org.eclipse.aether.transport.wagon.WagonConfigurator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.deployer.resource.maven.MavenProperties.WagonHttpMethod; import org.springframework.cloud.deployer.resource.maven.MavenProperties.WagonHttpMethodProperties; /** * Simple implementation of a {@link WagonConfigurator} which creates and supports * those providers we need. Maven resolver itself only provides PlexusWagonConfigurator * which is more involved with actual maven pom configuration and would not * suit our needs as things get a bit crazy with it due to its use of Guice. * * @author Janne Valkealahti * @author Corneil du Plessis * */ public class StaticWagonConfigurator implements WagonConfigurator { private static final Logger logger = LoggerFactory.getLogger(StaticWagonConfigurator.class); @Override public void configure(Wagon wagon, Object configuration) throws Exception { logger.debug("Configuring wagon {} with {}", wagon, configuration); if (wagon instanceof HttpWagon && configuration instanceof MavenProperties.Wagon) { HttpWagon httpWagon = (HttpWagon)wagon; Map httpMethodProperties = ((MavenProperties.Wagon) configuration) .getHttp(); HttpConfiguration httpConfiguration = new HttpConfiguration(); for (Entry entry : httpMethodProperties.entrySet()) { switch (entry.getKey()) { case all: httpConfiguration.setAll(buildConfig(entry.getValue())); break; case get: httpConfiguration.setGet(buildConfig(entry.getValue())); break; case head: httpConfiguration.setHead(buildConfig(entry.getValue())); break; case put: httpConfiguration.setPut(buildConfig(entry.getValue())); break; default: break; } } httpWagon.setHttpConfiguration(httpConfiguration); } } private static HttpMethodConfiguration buildConfig(WagonHttpMethodProperties properties) { HttpMethodConfiguration config = new HttpMethodConfiguration(); config.setUsePreemptive(properties.isUsePreemptive()); config.setUseDefaultHeaders(properties.isUseDefaultHeaders()); if (properties.getConnectionTimeout() != null) { config.setConnectionTimeout(properties.getConnectionTimeout()); } if (properties.getReadTimeout() != null) { config.setReadTimeout(properties.getReadTimeout()); } Properties params = new Properties(); params.putAll(properties.getParams()); config.setParams(params); Properties headers = new Properties(); headers.putAll(properties.getHeaders()); config.setHeaders(headers); return config; } } ================================================ FILE: spring-cloud-deployer-resource-maven/src/main/java/org/springframework/cloud/deployer/resource/maven/StaticWagonProvider.java ================================================ /* * Copyright 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.cloud.deployer.resource.maven; import org.apache.maven.wagon.Wagon; import org.apache.maven.wagon.providers.http.HttpWagon; import org.eclipse.aether.transport.wagon.WagonProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Simple implementation of a {@link WagonProvider} which creates and supports * those providers we need. Maven resolver itself only provides PlexusWagonProvider * which is more involved with actual maven pom configuration and would not * suit our needs as things get a bit crazy with it due to its use of Guice. * * @author Janne Valkealahti */ public class StaticWagonProvider implements WagonProvider { private static final Logger logger = LoggerFactory.getLogger(StaticWagonProvider.class); public StaticWagonProvider() { } public Wagon lookup( String roleHint ) throws Exception { logger.debug("Looking up wagon for roleHint {}", roleHint); if ("https".equals(roleHint)) { return new HttpWagon(); } else if ("http".equals(roleHint)) { return new HttpWagon(); } throw new IllegalArgumentException("No wagon available for " + roleHint); } public void release(Wagon wagon) { // nothing to do now } } ================================================ FILE: spring-cloud-deployer-resource-maven/src/test/java/org/springframework/cloud/deployer/resource/maven/MavenArtifactResolverTests.java ================================================ /* * Copyright 2016-2023 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.cloud.deployer.resource.maven; import java.net.UnknownHostException; import java.util.Collections; import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.springframework.core.io.Resource; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; import static org.assertj.core.api.AssertionsForClassTypes.tuple; import static org.junit.jupiter.params.provider.Arguments.arguments; /** * @author Eric Chen * @author Chris Bono */ class MavenArtifactResolverTests { @Test void resolveFailsOnProxyWithUnknownHostException() { String location = "maven://foo:bar:1.0.1"; MavenResourceLoader loader = new MavenResourceLoader(new MavenProperties()); Resource resource = loader.getResource(location); assertThat(MavenResource.class).isEqualTo(resource.getClass()); MavenResource mavenResource = (MavenResource) resource; MavenArtifactResolver resolver = new MavenArtifactResolver(mavenPropertiesWithProxyRepo()); assertThatThrownBy(() -> resolver.resolve(mavenResource)) .rootCause() .isInstanceOf(UnknownHostException.class) .hasMessageStartingWith("proxy.example.com:"); } private MavenProperties mavenPropertiesWithProxyRepo() { MavenProperties mavenProperties = new MavenProperties(); mavenProperties.setLocalRepository("~/.m2"); mavenProperties.setIncludeDefaultRemoteRepos(false); MavenProperties.RemoteRepository remoteRepo2 = new MavenProperties.RemoteRepository(); remoteRepo2.setUrl("http://myrepo.com:99999"); mavenProperties.getRemoteRepositories().put("repo2", remoteRepo2); MavenProperties.Proxy proxy = new MavenProperties.Proxy(); proxy.setHost("proxy.example.com"); proxy.setPort(8080); proxy.setNonProxyHosts("apache*|*.springframework.org|127.0.0.1|localhost"); mavenProperties.setProxy(proxy); return mavenProperties; } @Test void resolveFailsOnNoProxyWithIllegalArgumentException() { String location = "maven://foo:bar:1.0.1"; MavenResourceLoader loader = new MavenResourceLoader(new MavenProperties()); Resource resource = loader.getResource(location); assertThat(MavenResource.class).isEqualTo(resource.getClass()); MavenResource mavenResource = (MavenResource) resource; MavenArtifactResolver resolver = new MavenArtifactResolver(mavenPropertiesWithNoProxyRepo()); assertThatThrownBy(() -> resolver.resolve(mavenResource)) .hasRootCauseInstanceOf(IllegalArgumentException.class) .hasRootCauseMessage("port out of range:99999"); } private MavenProperties mavenPropertiesWithNoProxyRepo() { MavenProperties mavenProperties = new MavenProperties(); mavenProperties.setLocalRepository("~/.m2"); mavenProperties.setIncludeDefaultRemoteRepos(false); MavenProperties.RemoteRepository remoteRepo1 = new MavenProperties.RemoteRepository(); remoteRepo1.setUrl("http://localhost:99999"); mavenProperties.getRemoteRepositories().put("repo1", remoteRepo1); MavenProperties.Proxy proxy = new MavenProperties.Proxy(); proxy.setHost("http://proxy.example.com"); proxy.setPort(8080); proxy.setNonProxyHosts("apache*|*.springframework.org|127.0.0.1|localhost"); mavenProperties.setProxy(proxy); return mavenProperties; } @Test void resolveFailsWithErrorMessageIncludingRemoteRepos() { String location = "maven://one:car:1.0.1"; MavenResourceLoader loader = new MavenResourceLoader(new MavenProperties()); Resource resource = loader.getResource(location); assertThat(MavenResource.class).isEqualTo(resource.getClass()); MavenResource mavenResource = (MavenResource) resource; MavenProperties mavenProps = new MavenProperties(); mavenProps.setIncludeDefaultRemoteRepos(false); MavenProperties.RemoteRepository remoteRepo = new MavenProperties.RemoteRepository(); remoteRepo.setUrl("http://myrepo.com:99999"); mavenProps.getRemoteRepositories().put("repo2", remoteRepo); MavenArtifactResolver resolver = new MavenArtifactResolver(mavenProps); assertThatThrownBy(() -> resolver.resolve(mavenResource)) .hasMessage("Failed to resolve one:car:jar:1.0.1 using remote repo(s): [repo2 (http://myrepo.com:99999)]"); } @Test void defaultReposAddedWhenNoOtherRemoteRepos() { MavenProperties mavenProps = new MavenProperties(); MavenArtifactResolver mavenResolver = new MavenArtifactResolver(mavenProps); assertThat(mavenResolver.remoteRepositories()) .extracting("id", "url") .containsExactly(tuple("mavenCentral-default", "https://repo.maven.apache.org/maven2"), tuple("springSnapshot-default", "https://repo.spring.io/snapshot"), tuple("springMilestone-default", "https://repo.spring.io/milestone")); } @Test void defaultReposAddedInFrontOfOtherRemoteRepos() { MavenProperties mavenProps = new MavenProperties(); mavenProps.setRemoteRepositories(Collections.singletonMap("myRepo", new MavenProperties.RemoteRepository("https://my.custom.repo/snapshot"))); MavenArtifactResolver mavenResolver = new MavenArtifactResolver(mavenProps); assertThat(mavenResolver.remoteRepositories()) .extracting("id", "url") .containsExactly(tuple("mavenCentral-default", "https://repo.maven.apache.org/maven2"), tuple("springSnapshot-default", "https://repo.spring.io/snapshot"), tuple("springMilestone-default", "https://repo.spring.io/milestone"), tuple("myRepo", "https://my.custom.repo/snapshot")); } @ParameterizedTest @MethodSource("defaultReposIdAndUrlProvider") void defaultRepoAddedWhenNotAlreadyConfigured(String defaultRepoId, String defaultRepoUrl) { MavenProperties mavenProps = new MavenProperties(); mavenProps.setRemoteRepositories(Collections.singletonMap("myRepo", new MavenProperties.RemoteRepository(defaultRepoUrl + "/foo"))); MavenArtifactResolver mavenResolver = new MavenArtifactResolver(mavenProps); assertThat(mavenResolver.remoteRepositories()) .extracting("id", "url") .hasSize(4) .contains(tuple("myRepo", defaultRepoUrl + "/foo")) .contains(tuple(defaultRepoId, defaultRepoUrl)); } @ParameterizedTest @MethodSource("defaultReposIdAndUrlProvider") void defaultRepoNotAddedWhenAlreadyConfigured(String defaultRepoId, String defaultRepoUrl) { MavenProperties mavenProps = new MavenProperties(); mavenProps.setRemoteRepositories(Collections.singletonMap("myRepo", new MavenProperties.RemoteRepository(defaultRepoUrl))); MavenArtifactResolver mavenResolver = new MavenArtifactResolver(mavenProps); assertThat(mavenResolver.remoteRepositories()) .extracting("id", "url") .hasSize(3) .contains(tuple("myRepo", defaultRepoUrl)) .doesNotContain(tuple(defaultRepoId, defaultRepoUrl)); } static Stream defaultReposIdAndUrlProvider() { return Stream.of( arguments("mavenCentral-default", "https://repo.maven.apache.org/maven2"), arguments("springSnapshot-default", "https://repo.spring.io/snapshot"), arguments("springMilestone-default", "https://repo.spring.io/milestone") ); } @Test void defaultReposNotAddedWhenPropertyIsDisabledWithNoOtherRepos() { MavenProperties mavenProps = new MavenProperties(); mavenProps.setIncludeDefaultRemoteRepos(false); MavenArtifactResolver mavenResolver = new MavenArtifactResolver(mavenProps); assertThat(mavenResolver.remoteRepositories()).isEmpty(); } @Test void defaultReposNotAddedWhenPropertyIsDisabledWithOtherRepo() { MavenProperties mavenProps = new MavenProperties(); mavenProps.setIncludeDefaultRemoteRepos(false); mavenProps.setRemoteRepositories(Collections.singletonMap("myRepo", new MavenProperties.RemoteRepository("https://repo.snap.io/snapshot"))); MavenArtifactResolver mavenResolver = new MavenArtifactResolver(mavenProps); assertThat(mavenResolver.remoteRepositories()) .extracting("url") .containsExactly("https://repo.snap.io/snapshot"); } @Test void defaultReposAddedAndProxiedWhenProxyEnabled() { MavenProperties.Proxy proxy = new MavenProperties.Proxy(); proxy.setHost("proxy.example.com"); proxy.setPort(8080); MavenProperties mavenProps = new MavenProperties(); mavenProps.setProxy(proxy); MavenArtifactResolver mavenResolver = new MavenArtifactResolver(mavenProps); assertThat(mavenResolver.remoteRepositories()) .extracting("url", "proxy.type", "proxy.host", "proxy.port") .containsExactly(tuple("https://repo.maven.apache.org/maven2", "http", "proxy.example.com", 8080), tuple("https://repo.spring.io/snapshot", "http", "proxy.example.com", 8080), tuple("https://repo.spring.io/milestone", "http", "proxy.example.com", 8080)); } } ================================================ FILE: spring-cloud-deployer-resource-maven/src/test/java/org/springframework/cloud/deployer/resource/maven/MavenExtension.java ================================================ /* * Copyright 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.cloud.deployer.resource.maven; import java.util.Objects; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.User.UserBuilder; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.HttpStatusEntryPoint; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * Simple junit5 extension which bootstraps a server to simulate various * scenarios for artifact resolving via http. * * @author Janne Valkealahti * @author Corneil du Plessis */ public class MavenExtension implements AfterEachCallback, BeforeEachCallback { private ConfigurableApplicationContext context; public int getPort() { return Integer.parseInt(Objects.requireNonNull(this.context.getEnvironment().getProperty("local.server.port"))); } @Override public void beforeEach(ExtensionContext context) throws Exception { SpringApplication application = new SpringApplication(ServerConfig.class); this.context = application.run("--server.port=0"); } @Override public void afterEach(ExtensionContext context) throws Exception { if (this.context != null) { this.context.close(); } this.context = null; } @SpringBootApplication static class ServerConfig { } @RestController @RequestMapping("/public") static class PublicRepoController { @GetMapping(path = "/org/example/app/1.0.0.RELEASE/app-1.0.0.RELEASE.jar") public byte[] app100release() { return new byte[0]; } } @RestController @RequestMapping("/private") static class PrivateRepoController { @GetMapping(path = "/org/example/secured/1.0.0.RELEASE/secured-1.0.0.RELEASE.jar") public byte[] secured100release() { return new byte[0]; } } @RestController @RequestMapping("/preemptive") @EnableWebSecurity static class PreemptiveRepoController { @GetMapping(path = "/org/example/preemptive/1.0.0.RELEASE/preemptive-1.0.0.RELEASE.jar") public byte[] preemptive100release() { return new byte[0]; } } @Configuration static class BasicSecurityConfig { @Bean public UserDetailsService userDetailsService() { UserBuilder users = User.builder(); InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); manager.createUser(users.username("user").password("{noop}password").roles("USER").build()); return manager; } @Bean public SecurityFilterChain configureBasic(HttpSecurity http) throws Exception { return http.authorizeHttpRequests(authorise -> authorise .requestMatchers("/public/**").permitAll() .requestMatchers("/private/**").hasRole("USER") .anyRequest().authenticated() ).httpBasic(Customizer.withDefaults()).build(); } } @Configuration @Order(1) static class PreemptiveSecurityConfig { @Bean protected ExceptionHandlingConfigurer configure(HttpSecurity http) throws Exception { // We add basic auth for /preemptive so server returns 403 as // exception handling is changed to force 403. // normal maven behaviour is that it needs 401 to continue with a challenge. // This is where preemptive auth takes place as client should send auth // with every request. return http .securityMatcher("/preemptive/**") .authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().hasRole("USER") ) .httpBasic() .and() .exceptionHandling() .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.FORBIDDEN)); } } } ================================================ FILE: spring-cloud-deployer-resource-maven/src/test/java/org/springframework/cloud/deployer/resource/maven/MavenPropertiesTests.java ================================================ /* * Copyright 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.cloud.deployer.resource.maven; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.cloud.deployer.resource.maven.MavenProperties.WagonHttpMethod; import org.springframework.core.env.StandardEnvironment; import org.springframework.core.env.SystemEnvironmentPropertySource; import static org.assertj.core.api.Assertions.assertThat; public class MavenPropertiesTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner(); @Test public void testDefaults() { this.contextRunner .withUserConfiguration(Config1.class) .run((context) -> { MavenProperties properties = context.getBean(MavenProperties.class); assertThat(properties.isUseWagon()).isFalse(); }); } @Test public void testPreemtiveEnabled() { this.contextRunner .withInitializer(context -> { Map map = new HashMap<>(); map.put("maven.use-wagon", "true"); context.getEnvironment().getPropertySources().addLast(new SystemEnvironmentPropertySource( StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, map)); }) .withUserConfiguration(Config1.class) .run((context) -> { MavenProperties properties = context.getBean(MavenProperties.class); assertThat(properties.isUseWagon()).isTrue(); }); } @Test public void testRemoteRepositories() { this.contextRunner .withInitializer(context -> { Map map = new HashMap<>(); map.put("maven.remote-repositories.repo1.url", "url1"); map.put("maven.remote-repositories.repo1.wagon.http.all.use-preemptive", "true"); map.put("maven.remote-repositories.repo1.wagon.http.all.use-default-headers", "true"); map.put("maven.remote-repositories.repo1.wagon.http.all.connection-timeout", "2"); map.put("maven.remote-repositories.repo1.wagon.http.all.read-timeout", "3"); map.put("maven.remote-repositories.repo1.wagon.http.all.headers.header1", "value1"); map.put("maven.remote-repositories.repo1.wagon.http.all.params.http.connection.timeout", "1"); context.getEnvironment().getPropertySources().addLast(new SystemEnvironmentPropertySource( StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, map)); }) .withUserConfiguration(Config1.class) .run((context) -> { MavenProperties properties = context.getBean(MavenProperties.class); assertThat(properties.getRemoteRepositories().size()).isEqualTo(1); assertThat(properties.getRemoteRepositories()).containsOnlyKeys("repo1"); assertThat(properties.getRemoteRepositories().get("repo1").getWagon().getHttp().size()) .isEqualTo(1); assertThat(properties.getRemoteRepositories().get("repo1").getWagon().getHttp() .get(WagonHttpMethod.all).isUsePreemptive()).isTrue(); assertThat(properties.getRemoteRepositories().get("repo1").getWagon().getHttp() .get(WagonHttpMethod.all).isUseDefaultHeaders()).isTrue(); assertThat(properties.getRemoteRepositories().get("repo1").getWagon().getHttp() .get(WagonHttpMethod.all).getConnectionTimeout()).isEqualTo(2); assertThat(properties.getRemoteRepositories().get("repo1").getWagon().getHttp() .get(WagonHttpMethod.all).getReadTimeout()).isEqualTo(3); assertThat(properties.getRemoteRepositories().get("repo1").getWagon().getHttp() .get(WagonHttpMethod.all).getHeaders()).containsOnlyKeys("header1"); assertThat(properties.getRemoteRepositories().get("repo1").getWagon().getHttp() .get(WagonHttpMethod.all).getHeaders().get("header1")).isEqualTo("value1"); assertThat(properties.getRemoteRepositories().get("repo1").getWagon().getHttp() .get(WagonHttpMethod.all).getParams()).containsOnlyKeys("http.connection.timeout"); assertThat(properties.getRemoteRepositories().get("repo1").getWagon().getHttp() .get(WagonHttpMethod.all).getParams().get("http.connection.timeout")).isEqualTo("1"); }); } @EnableConfigurationProperties({ MavenConfigurationProperties.class }) private static class Config1 { } @ConfigurationProperties(prefix = "maven") public static class MavenConfigurationProperties extends MavenProperties { } } ================================================ FILE: spring-cloud-deployer-resource-maven/src/test/java/org/springframework/cloud/deployer/resource/maven/MavenResourceLoaderTests.java ================================================ /* * Copyright 2016 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.cloud.deployer.resource.maven; import org.junit.jupiter.api.Test; import org.springframework.core.io.Resource; import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; import static org.junit.jupiter.api.Assertions.assertEquals; /** * Tests for the {@link MavenResourceLoader}. * * @author Mark Fisher */ public class MavenResourceLoaderTests { @Test public void verifyCoordinates() { String location = "maven://foo:bar:1.0.1"; MavenResourceLoader loader = new MavenResourceLoader(new MavenProperties()); Resource resource = loader.getResource(location); assertEquals(MavenResource.class, resource.getClass()); MavenResource mavenResource = (MavenResource) resource; assertEquals("foo", mavenResource.getGroupId()); assertEquals("bar", mavenResource.getArtifactId()); assertEquals("1.0.1", mavenResource.getVersion()); } @Test public void invalidPrefix() { assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> { MavenResourceLoader loader = new MavenResourceLoader(new MavenProperties()); loader.getResource("foo://bar"); }); } } ================================================ FILE: spring-cloud-deployer-resource-maven/src/test/java/org/springframework/cloud/deployer/resource/maven/MavenResourceTests.java ================================================ /* * Copyright 2016-2021 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.cloud.deployer.resource.maven; import java.io.File; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.HashMap; import java.util.List; import java.util.Map; import org.eclipse.aether.RepositorySystem; import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.repository.RemoteRepository; import org.eclipse.aether.repository.RepositoryPolicy; import org.junit.jupiter.api.Test; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; import static org.junit.jupiter.api.Assertions.assertEquals; /** * Tests for {@link MavenResource} * * @author Venil Noronha * @author Janne Valkealahti * @author Mark Fisher * @author Ilayaperumal Gopinathan */ public class MavenResourceTests { @Test public void mavenResourceFilename() throws IOException { MavenResource resource = new MavenResource.Builder() .artifactId("timestamp-task") .groupId("org.springframework.cloud.task.app") .version("1.0.0.BUILD-SNAPSHOT") .build(); assertThat(resource.getFilename()).as("getFilename() returned null").isNotNull(); assertEquals("timestamp-task-1.0.0.BUILD-SNAPSHOT.jar", resource.getFilename(), "getFilename() doesn't match the expected filename"); assertEquals("maven://org.springframework.cloud.task.app:timestamp-task:jar:1.0.0.BUILD-SNAPSHOT", resource.getURI().toString(), "getURI doesn't match the expected URI"); } @Test public void resourceExists() { MavenProperties mavenProperties = new MavenProperties(); Map remoteRepositoryMap = new HashMap<>(); remoteRepositoryMap.put("default", new MavenProperties.RemoteRepository("https://repo.spring.io/libs-snapshot")); mavenProperties.setRemoteRepositories(remoteRepositoryMap); MavenResource resource = MavenResource .parse("org.springframework.cloud.task.app:timestamp-task:jar:1.0.0.BUILD-SNAPSHOT", mavenProperties); assertEquals(resource.exists(), true); } @Test public void resourceDoesNotExist() { MavenProperties mavenProperties = new MavenProperties(); Map remoteRepositoryMap = new HashMap<>(); remoteRepositoryMap.put("default", new MavenProperties.RemoteRepository("https://repo.spring.io/libs-snapshot")); mavenProperties.setRemoteRepositories(remoteRepositoryMap); MavenResource resource = MavenResource .parse("org.springframework.cloud.task.app:doesnotexist:jar:1.0.0.BUILD-SNAPSHOT", mavenProperties); assertEquals(resource.exists(), false); } @Test public void coordinatesParsed() { MavenResource resource = MavenResource .parse("org.springframework.cloud.task.app:timestamp-task:jar:exec:1.0.0.BUILD-SNAPSHOT"); assertEquals("timestamp-task-1.0.0.BUILD-SNAPSHOT-exec.jar", resource.getFilename(), "getFilename() doesn't match the expected filename"); resource = MavenResource.parse("org.springframework.cloud.task.app:timestamp-task:jar:1.0.0.BUILD-SNAPSHOT"); assertEquals("timestamp-task-1.0.0.BUILD-SNAPSHOT.jar", resource.getFilename(), "getFilename() doesn't match the expected filename"); } @Test public void mavenResourceRetrievedFromNonDefaultRemoteRepository() throws Exception { String coordinates = "org.springframework.cloud.task.app:timestamp-task:jar:1.0.0.BUILD-SNAPSHOT"; MavenProperties properties = new MavenProperties(); String tempLocalRepo = System.getProperty("java.io.tmpdir") + File.separator + ".m2-test1"; new File(tempLocalRepo).deleteOnExit(); properties.setLocalRepository(tempLocalRepo); Map remoteRepositoryMap = new HashMap<>(); remoteRepositoryMap.put("default", new MavenProperties.RemoteRepository("https://repo.spring.io/libs-snapshot")); properties.setRemoteRepositories(remoteRepositoryMap); MavenResource resource = MavenResource.parse(coordinates, properties); assertEquals("timestamp-task-1.0.0.BUILD-SNAPSHOT.jar", resource.getFilename(), "getFilename() doesn't match the expected filename"); } @Test public void localResolutionFailsIfNotCached() { assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> { String tempLocalRepo = System.getProperty("java.io.tmpdir") + File.separator + ".m2-test2"; new File(tempLocalRepo).deleteOnExit(); MavenProperties properties = new MavenProperties(); properties.setLocalRepository(tempLocalRepo); properties.setOffline(true); MavenResource resource = new MavenResource.Builder(properties) .artifactId("timestamp-task") .groupId("org.springframework.cloud.task.app") .version("1.0.0.BUILD-SNAPSHOT") .build(); resource.getFile(); }); } @Test public void localResolutionSucceedsIfCached() throws Exception { String coordinates = "org.springframework.cloud.task.app:timestamp-task:jar:1.0.0.BUILD-SNAPSHOT"; MavenProperties properties1 = new MavenProperties(); String tempLocalRepo = System.getProperty("java.io.tmpdir") + File.separator + ".m2-test3"; new File(tempLocalRepo).deleteOnExit(); properties1.setLocalRepository(tempLocalRepo); Map remoteRepositoryMap = new HashMap<>(); remoteRepositoryMap.put("default", new MavenProperties.RemoteRepository("https://repo.spring.io/libs-snapshot")); properties1.setRemoteRepositories(remoteRepositoryMap); MavenResource resource = MavenResource.parse(coordinates, properties1); resource.getFile(); // no remotes; should not fail anymore MavenProperties properties2 = new MavenProperties(); properties2.setLocalRepository(tempLocalRepo); properties2.setOffline(true); resource = new MavenResource.Builder(properties2) .artifactId("timestamp-task") .groupId("org.springframework.cloud.task.app") .version("1.0.0.BUILD-SNAPSHOT") .build(); resource.getFile(); } @Test public void testGetVersions() throws Exception { String coordinates = "org.springframework.cloud.task.app:timestamp-task:jar:1.0.0.BUILD-SNAPSHOT"; MavenProperties properties = new MavenProperties(); String tempLocalRepo = System.getProperty("java.io.tmpdir") + File.separator + ".m2-test3"; new File(tempLocalRepo).deleteOnExit(); properties.setLocalRepository(tempLocalRepo); Map remoteRepositoryMap = new HashMap<>(); remoteRepositoryMap.put("default", new MavenProperties.RemoteRepository("https://repo.spring.io/libs-snapshot")); properties.setRemoteRepositories(remoteRepositoryMap); MavenResource resource = MavenResource.parse(coordinates, properties); Assert.isTrue(!resource.getVersions("org.springframework.cloud.task.app:timestamp-task:jar:[0,)").isEmpty(), "Versions shouldn't be empty"); } @Test public void checkRepositoryPolicies() { MavenProperties mavenProperties = new MavenProperties(); mavenProperties.setChecksumPolicy("always"); mavenProperties.setUpdatePolicy("fail"); mavenProperties.setIncludeDefaultRemoteRepos(false); Map remoteRepositoryMap = new HashMap<>(); MavenProperties.RemoteRepository remoteRepo1 = new MavenProperties.RemoteRepository( "https://repo.spring.io/libs-snapshot"); MavenProperties.RepositoryPolicy snapshotPolicy = new MavenProperties.RepositoryPolicy(); snapshotPolicy.setEnabled(true); snapshotPolicy.setUpdatePolicy("always"); snapshotPolicy.setChecksumPolicy("warn"); remoteRepo1.setSnapshotPolicy(snapshotPolicy); MavenProperties.RepositoryPolicy releasePolicy = new MavenProperties.RepositoryPolicy(); releasePolicy.setEnabled(true); releasePolicy.setUpdatePolicy("interval"); releasePolicy.setChecksumPolicy("ignore"); remoteRepo1.setReleasePolicy(releasePolicy); remoteRepositoryMap.put("repo1", remoteRepo1); MavenProperties.RemoteRepository remoteRepo2 = new MavenProperties.RemoteRepository( "https://repo.spring.io/libs-milestone"); MavenProperties.RepositoryPolicy policy = new MavenProperties.RepositoryPolicy(); policy.setEnabled(true); policy.setUpdatePolicy("daily"); policy.setChecksumPolicy("fail"); remoteRepo2.setPolicy(policy); remoteRepositoryMap.put("repo2", remoteRepo2); mavenProperties.setRemoteRepositories(remoteRepositoryMap); MavenArtifactResolver artifactResolver = new MavenArtifactResolver(mavenProperties); Field remoteRepositories = ReflectionUtils.findField(MavenArtifactResolver.class, "remoteRepositories"); ReflectionUtils.makeAccessible(remoteRepositories); List remoteRepositoryList = (List) ReflectionUtils .getField(remoteRepositories, artifactResolver); Field repositorySystem = ReflectionUtils.findField(MavenArtifactResolver.class, "repositorySystem"); ReflectionUtils.makeAccessible(repositorySystem); RepositorySystem repositorySystem1 = (RepositorySystem) ReflectionUtils.getField(repositorySystem, artifactResolver); Method repositorySystemSessionMethod = ReflectionUtils.findMethod(MavenArtifactResolver.class, "newRepositorySystemSession", RepositorySystem.class, String.class); ReflectionUtils.makeAccessible(repositorySystemSessionMethod); RepositorySystemSession repositorySystemSession = (RepositorySystemSession) ReflectionUtils.invokeMethod(repositorySystemSessionMethod, artifactResolver, repositorySystem1, "file://local"); assertEquals("always", repositorySystemSession.getChecksumPolicy()); assertEquals("fail", repositorySystemSession.getUpdatePolicy()); for (RemoteRepository remoteRepository : remoteRepositoryList) { assertEquals(2, remoteRepositoryList.size()); assertEquals(true, remoteRepositoryList.get(0).getId().equals("repo1") || remoteRepositoryList.get(0).getId().equals("repo2")); assertEquals(true, remoteRepositoryList.get(1).getId().equals("repo2") || remoteRepositoryList.get(1).getId().equals("repo1")); if (remoteRepository.getId().equals("repo1")) { RepositoryPolicy snapshotPolicy1 = remoteRepository.getPolicy(true); assertEquals(true, snapshotPolicy1.isEnabled()); assertEquals("always", snapshotPolicy1.getUpdatePolicy()); assertEquals("warn", snapshotPolicy1.getChecksumPolicy()); RepositoryPolicy releasePolicy1 = remoteRepository.getPolicy(false); assertEquals(true, releasePolicy1.isEnabled()); assertEquals("interval", releasePolicy1.getUpdatePolicy()); assertEquals("ignore", releasePolicy1.getChecksumPolicy()); } else if (remoteRepository.getId().equals("repo2")) { RepositoryPolicy snapshotPolicy2 = remoteRepository.getPolicy(true); assertEquals(true, snapshotPolicy2.isEnabled()); assertEquals("daily", snapshotPolicy2.getUpdatePolicy()); assertEquals("fail", snapshotPolicy2.getChecksumPolicy()); RepositoryPolicy releasePolicy2 = remoteRepository.getPolicy(false); assertEquals(true, releasePolicy2.isEnabled()); assertEquals("daily", releasePolicy2.getUpdatePolicy()); assertEquals("fail", releasePolicy2.getChecksumPolicy()); } } MavenResource resource = MavenResource .parse("org.springframework.cloud.task.app:timestamp-task:jar:1.0.0.BUILD-SNAPSHOT", mavenProperties); assertEquals(resource.exists(), false); } } ================================================ FILE: spring-cloud-deployer-resource-maven/src/test/java/org/springframework/cloud/deployer/resource/maven/WagonHttpTests.java ================================================ /* * Copyright 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.cloud.deployer.resource.maven; import java.nio.file.Path; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; import org.springframework.cloud.deployer.resource.maven.MavenProperties.Authentication; import org.springframework.cloud.deployer.resource.maven.MavenProperties.RemoteRepository; import org.springframework.cloud.deployer.resource.maven.MavenProperties.WagonHttpMethod; import org.springframework.cloud.deployer.resource.maven.MavenProperties.WagonHttpMethodProperties; import static org.assertj.core.api.Assertions.assertThat; /** * Tests for Maven Wagon resolver. * * @author Janne Valkealahti * @author Corneil du Plessis */ public class WagonHttpTests { @RegisterExtension static MavenExtension server = new MavenExtension(); @Test public void resourceDoesNotExistWagon(@TempDir Path tempDir) { MavenProperties mavenProperties = new MavenProperties(); mavenProperties.setLocalRepository(tempDir.toAbsolutePath().toString()); mavenProperties.setUseWagon(true); Map remoteRepositoryMap = new HashMap<>(); remoteRepositoryMap.put("default", new MavenProperties.RemoteRepository("http://localhost:" + server.getPort() + "/public")); mavenProperties.setRemoteRepositories(remoteRepositoryMap); MavenResource resource = MavenResource .parse("org.example:doesnotexist:jar:1.0.0.RELEASE", mavenProperties); assertThat(resource.exists()).isFalse(); } @Test public void resourceDoesExistWagon(@TempDir Path tempDir) { MavenProperties mavenProperties = new MavenProperties(); mavenProperties.setLocalRepository(tempDir.toAbsolutePath().toString()); mavenProperties.setUseWagon(true); Map remoteRepositoryMap = new HashMap<>(); remoteRepositoryMap.put("default", new MavenProperties.RemoteRepository("http://localhost:" + server.getPort() + "/public")); mavenProperties.setRemoteRepositories(remoteRepositoryMap); MavenResource resource = MavenResource .parse("org.example:app:jar:1.0.0.RELEASE", mavenProperties); assertThat(resource.exists()).isTrue(); } @Test public void resourceDoesExistWithAuth(@TempDir Path tempDir) { MavenProperties mavenProperties = new MavenProperties(); mavenProperties.setLocalRepository(tempDir.toAbsolutePath().toString()); mavenProperties.setUseWagon(true); Map remoteRepositories = new HashMap<>(); RemoteRepository remoteRepository = new RemoteRepository("http://localhost:" + server.getPort() + "/private"); Authentication auth = new Authentication("user", "password"); remoteRepository.setAuth(auth); remoteRepositories.put("default", remoteRepository); mavenProperties.setRemoteRepositories(remoteRepositories); MavenResource resource = MavenResource .parse("org.example:secured:jar:1.0.0.RELEASE", mavenProperties); assertThat(resource.exists()).isTrue(); } @Test public void resourceDoesExistWithPreemptiveAuth(@TempDir Path tempDir) { MavenProperties mavenProperties = new MavenProperties(); mavenProperties.setLocalRepository(tempDir.toAbsolutePath().toString()); mavenProperties.setUseWagon(true); Map remoteRepositories = new HashMap<>(); RemoteRepository remoteRepository = new RemoteRepository("http://localhost:" + server.getPort() + "/preemptive"); WagonHttpMethodProperties wagonHttpMethodProperties = new WagonHttpMethodProperties(); wagonHttpMethodProperties.setUsePreemptive(true); Map headers = new HashMap<>(); headers.put("Foo", "Bar"); Map params = new HashMap<>(); params.put("http.connection.stalecheck", "true"); wagonHttpMethodProperties.setHeaders(headers); wagonHttpMethodProperties.setParams(params); remoteRepository.getWagon().getHttp().put(WagonHttpMethod.all, wagonHttpMethodProperties); Authentication auth = new Authentication("user", "password"); remoteRepository.setAuth(auth); remoteRepositories.put("default", remoteRepository); mavenProperties.setRemoteRepositories(remoteRepositories); MavenResource resource = MavenResource .parse("org.example:preemptive:jar:1.0.0.RELEASE", mavenProperties); assertThat(resource.exists()).isTrue(); } } ================================================ FILE: spring-cloud-deployer-resource-maven/src/test/resources/application.properties ================================================ logging.level.org.springframework.cloud.deployer=debug logging.level.org.springframework.web=debug logging.level.org.apache.http.headers=debug ================================================ FILE: spring-cloud-deployer-resource-support/pom.xml ================================================ 4.0.0 spring-cloud-deployer-resource-support jar Spring Cloud Deployer Resource Support org.springframework.cloud spring-cloud-deployer-parent 3.0.0-SNAPSHOT .. org.springframework spring-context org.slf4j slf4j-api org.junit.jupiter junit-jupiter test org.slf4j slf4j-simple test org.assertj assertj-core test ================================================ FILE: spring-cloud-deployer-resource-support/src/main/java/org/springframework/cloud/deployer/resource/registry/InMemoryUriRegistry.java ================================================ /* * Copyright 2016 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.cloud.deployer.resource.registry; import java.net.URI; import java.util.Collections; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.springframework.util.Assert; /** * In-memory (non persistent) {@link UriRegistry} implementation. * * @author Patrick Peralta */ public class InMemoryUriRegistry implements UriRegistry { private final Map map = new ConcurrentHashMap<>(); @Override public URI find(String key) { Assert.hasLength(key, "key required"); URI uri = this.map.get(key); if (uri == null) { throw new IllegalArgumentException("No URI found for " + key); } return uri; } @Override public Map findAll() { return Collections.unmodifiableMap(this.map); } @Override public void register(String key, URI uri) { this.map.put(key, uri); } @Override public void unregister(String key) { this.map.remove(key); } } ================================================ FILE: spring-cloud-deployer-resource-support/src/main/java/org/springframework/cloud/deployer/resource/registry/UriRegistry.java ================================================ /* * Copyright 2016 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.cloud.deployer.resource.registry; import java.net.URI; import java.util.Map; /** * Registry for storing and finding {@link URI}s via a string key. * * @author Patrick Peralta */ public interface UriRegistry { /** * Return a {@link URI} for a string key. * * @param key the key for the URI * @return the {@code URI} for the given key * @throws IllegalArgumentException if no URI is registered with the key */ URI find(String key); /** * Return all registered {@code URI}s. * * @return map of keys to {@code URI}s. */ Map findAll(); /** * Register a {@link URI} with a string key. Existing * registrations will be overwritten. * * @param key the key for the URI * @param uri the {@code URI} to associate with the key */ void register(String key, URI uri); /** * Remove the registration for a string key. * * @param key the key for the {@code URI} to unregister */ void unregister(String key); } ================================================ FILE: spring-cloud-deployer-resource-support/src/main/java/org/springframework/cloud/deployer/resource/registry/UriRegistryPopulator.java ================================================ /* * Copyright 2016-2017 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.cloud.deployer.resource.registry; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; import java.util.HashMap; import java.util.Map; import java.util.Properties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ResourceLoaderAware; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** * Utility class for populating a {@link UriRegistry} via a * {@link Properties} file. One or more URI strings indicating the * location of property files is supplied via the constructor, * and the files themselves are loaded via the {@link Resource} * provided by {@link #resourceLoader}. * * @author Patrick Peralta * @author Ilayaperumal Gopinathan */ public class UriRegistryPopulator implements ResourceLoaderAware { private static final Logger logger = LoggerFactory.getLogger(UriRegistryPopulator.class); private volatile ResourceLoader resourceLoader; @Override public void setResourceLoader(ResourceLoader resourceLoader) { this.resourceLoader = resourceLoader; } /** * Populate the provided registry with the contents of * the property files indicated by {@code resourceUris}. * * @param overwrite if {@code true}, overwrites any pre-existing registrations with the same key * @param registry the registry to populate * @param resourceUris string(s) indicating the URIs to load properties from * @return the registered URI values in the map with the keys being the property names */ public Map populateRegistry(boolean overwrite, UriRegistry registry, String... resourceUris) { Assert.notEmpty(resourceUris, "resourceUri must not be empty"); Map registered = new HashMap<>(); for (String resourceUri : resourceUris) { Resource resource = this.resourceLoader.getResource(resourceUri); Properties properties = new Properties(); try (InputStream is = resource.getInputStream()) { properties.load(is); for (String key : properties.stringPropertyNames()) { try { URI uri = new URI(properties.getProperty(key)); boolean validUri = true; if (ObjectUtils.isEmpty(uri)) { logger.warn(String.format("Error when registering '%s': URI is required", key)); validUri = false; } if (validUri && !StringUtils.hasText(uri.getScheme())) { logger.warn(String.format("Error when registering '%s' with URI %s: URI scheme must be specified", key, uri)); validUri = false; } if (validUri && !StringUtils.hasText(uri.getSchemeSpecificPart())) { logger.warn(String.format("Error when registering '%s' with URI %s: URI scheme-specific part must be specified", key, uri)); validUri = false; } if (!overwrite) { try { if (registry.find(key) != null) { // already exists; move on continue; } } catch (IllegalArgumentException e) { // this key does not exist; will add } } if (validUri) { registry.register(key, uri); registered.put(key, uri); } } catch (URISyntaxException e) { throw new IllegalArgumentException(String.format("'%s' for '%s' is not a properly formed URI", properties.getProperty(key), key), e); } } } catch (IOException e) { throw new RuntimeException(e); } } return registered; } } ================================================ FILE: spring-cloud-deployer-resource-support/src/main/java/org/springframework/cloud/deployer/resource/support/DelegatingResourceLoader.java ================================================ /* * Copyright 2016-2018 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.cloud.deployer.resource.support; import java.net.URI; import java.util.Collections; import java.util.Map; import org.springframework.context.ResourceLoaderAware; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; /** * A {@link ResourceLoader} implementation that delegates to other {@link ResourceLoader} instances * that are stored in a Map with their associated URI schemes as the keys. If a scheme does not * exist within the Map, it will fallback to a {@link DefaultResourceLoader}. * The Map may be empty (or {@literal null}). * * @author Mark Fisher * @author Janne Valkealahti * @author Ilayaperumal Gopinathan */ public class DelegatingResourceLoader implements ResourceLoader, ResourceLoaderAware { private final ClassLoader classLoader = ClassUtils.getDefaultClassLoader(); private final Map loaders; private ResourceLoader defaultResourceLoader = new DefaultResourceLoader(); /** * Instantiates a new delegating resource loader. */ public DelegatingResourceLoader() { this(null); } /** * Instantiates a new delegating resource loader. * * @param loaders the loaders */ public DelegatingResourceLoader(Map loaders) { this.loaders = CollectionUtils.isEmpty(loaders) ? Collections.emptyMap() : Collections.unmodifiableMap(loaders); } @Override public void setResourceLoader(ResourceLoader contextResourceLoader) { if (contextResourceLoader != null && contextResourceLoader != this) { this.defaultResourceLoader = contextResourceLoader; } } @Override public Resource getResource(String location) { try { URI uri = new URI(location); String scheme = uri.getScheme(); Assert.notNull(scheme, "a scheme (prefix) is required"); ResourceLoader loader = this.loaders.get(scheme); if (loader == null) { if (scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https")) { loader = new DownloadingUrlResourceLoader(); } else { loader = this.defaultResourceLoader; } } return loader.getResource(location); } catch (Exception e) { throw new ResourceNotResolvedException(e.getMessage(), e); } } @Override public ClassLoader getClassLoader() { return this.classLoader; } /** * Gets a map of configured loaders. * * @return the loaders */ public Map getLoaders() { return loaders; } } ================================================ FILE: spring-cloud-deployer-resource-support/src/main/java/org/springframework/cloud/deployer/resource/support/DownloadingUrlResource.java ================================================ /* * Copyright 2017 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.cloud.deployer.resource.support; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.io.Resource; import org.springframework.core.io.UrlResource; import org.springframework.util.FileCopyUtils; /** * A {@link Resource} implementation that will download a {@link UrlResource} to a temp file when * the {@link DownloadingUrlResource#getFile()} is invoked. * * @author Ilayaperumal Gopinathan * @author Mark Pollack */ public class DownloadingUrlResource extends UrlResource { private static final Logger logger = LoggerFactory.getLogger(DownloadingUrlResource.class); private File file; /** * Create a new {@code DownloadingUrlResource} based on the given URI object. * @param uri a URI * @throws MalformedURLException if the given URL path is not valid */ public DownloadingUrlResource(String uri) throws MalformedURLException { super(uri); } /** * Downloads the file from the HTTP location to a temporary file. * The temporary file uses the directory prefix "spring-cloud-deployer" and the filename is * the SHA1 hash of the URL. The file will only be downloaded on the first invocation * of this method. * @return The downloaded file. * @throws IOException if there are errors downloading or writing the temporary file. */ @Override public synchronized File getFile() throws IOException { if (file == null) { // Create a well formatted filename, no dashes, slashes, etc from the URL String simpleName = null; try { Path path = Paths.get(getURL().toURI().getPath()); simpleName = path.getFileName().toString().replaceAll("[^\\p{IsAlphabetic}^\\p{IsDigit}]", ""); } catch (URISyntaxException e) { logger.info("Could not create simple name from last part of URL", e.getMessage()); } String fileName = ShaUtils.sha1(getURL().toString()); if (simpleName != null) { try { this.file = new File(Files.createTempDirectory("spring-cloud-deployer").toFile(), fileName + "-" + simpleName); } catch (IOException e) { logger.info("Could not create simple temp file name using last part of URL"); } } if (file == null) { this.file = new File(Files.createTempDirectory("spring-cloud-deployer").toFile(), fileName); } // Get the input stream for the URLResource logger.info("Downloading [" + getURL().toString() + "] to " + this.file.getAbsolutePath()); FileCopyUtils.copy(this.getInputStream(), new FileOutputStream(file)); } return file; } @Override public synchronized String getDescription() { StringBuffer sb = new StringBuffer(); sb.append("URL [" + getURL() + "]"); if (file != null) { sb.append(", file [" + this.file.getAbsolutePath() + "]"); } return sb.toString(); } } ================================================ FILE: spring-cloud-deployer-resource-support/src/main/java/org/springframework/cloud/deployer/resource/support/DownloadingUrlResourceLoader.java ================================================ /* * Copyright 2017 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.cloud.deployer.resource.support; import java.net.MalformedURLException; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.Resource; /** * Resource loader that will return a {@link DownloadingUrlResource} * * @author Ilayaperumal Gopinathan */ public class DownloadingUrlResourceLoader extends DefaultResourceLoader { @Override public Resource getResource(String location) { try { return new DownloadingUrlResource(location); } catch (MalformedURLException e) { throw new IllegalStateException(e); } } } ================================================ FILE: spring-cloud-deployer-resource-support/src/main/java/org/springframework/cloud/deployer/resource/support/ResourceNotResolvedException.java ================================================ /* * Copyright 2016 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.cloud.deployer.resource.support; /** * Thrown to indicate failure resolving a {@link org.springframework.core.io.Resource}. * * @author Mark Pollack */ @SuppressWarnings("serial") public class ResourceNotResolvedException extends RuntimeException { /** * Create a new {@link ResourceNotResolvedException} with the specified options. * @param message the exception message to display to the user * @param cause the underlying cause */ public ResourceNotResolvedException(String message, Throwable cause) { super(message, cause); } } ================================================ FILE: spring-cloud-deployer-resource-support/src/main/java/org/springframework/cloud/deployer/resource/support/ShaUtils.java ================================================ /* * Copyright 2016 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.cloud.deployer.resource.support; import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; /** * Simple sha utils. * * @author Janne Valkealahti */ public abstract class ShaUtils { private static final char[] CHARS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; /** * Creates a sha1 out from a given data. * * @param data the data * @return the sha1 for data */ protected static String sha1(String data) { try { return new String(encodeHex(MessageDigest.getInstance("SHA-1").digest(data.getBytes("UTF-8")))); } catch (NoSuchAlgorithmException e) { throw new IllegalStateException(e); } catch (UnsupportedEncodingException e) { throw new IllegalStateException(e); } } /** * Encode given data as lower case hex chars. * * @param data the data * @return the endoced chars */ private static char[] encodeHex(final byte[] data) { final int len = data.length; final char[] out = new char[len << 1]; for (int i = 0, j = 0; i < len; i++) { out[j++] = CHARS[(0xF0 & data[i]) >>> 4]; out[j++] = CHARS[0x0F & data[i]]; } return out; } } ================================================ FILE: spring-cloud-deployer-resource-support/src/test/java/org/springframework/cloud/deployer/resource/StubResourceLoader.java ================================================ /* * Copyright 2016 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.cloud.deployer.resource; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.util.ClassUtils; /** * Simple {@link ResourceLoader} that returns the {@link Resource} * provided in the constructor. It also keeps track of the locations * of requested resources. * * @author Patrick Peralta */ public class StubResourceLoader implements ResourceLoader { private final Resource resource; private final List requestedLocations = new ArrayList<>(); /** * Construct a {@code StubResourceLoader} that returns the provided * resource. * * @param resource resource to return via {@link #getResource(String)} */ public StubResourceLoader(Resource resource) { this.resource = resource; } @Override public Resource getResource(String location) { this.requestedLocations.add(location); return this.resource; } @Override public ClassLoader getClassLoader() { return ClassUtils.getDefaultClassLoader(); } public List getRequestedLocations() { return Collections.unmodifiableList(this.requestedLocations); } } ================================================ FILE: spring-cloud-deployer-resource-support/src/test/java/org/springframework/cloud/deployer/resource/registry/UriRegistryPopulatorTests.java ================================================ /* * Copyright 2016-2017 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.cloud.deployer.resource.registry; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.util.Map; import java.util.Properties; import org.springframework.cloud.deployer.resource.StubResourceLoader; import org.junit.jupiter.api.Test; import org.springframework.core.io.AbstractResource; import org.springframework.core.io.Resource; import static org.assertj.core.api.Assertions.assertThat; /** * @author Patrick Peralta * @author Ilayaperumal Gopinathan */ public class UriRegistryPopulatorTests { private final Properties uris; public UriRegistryPopulatorTests() { this.uris = new Properties(); this.uris.setProperty("foo.1", "maven://group1:foo:jar:classifier1:1.0.1"); this.uris.setProperty("foo-2", "maven://group2:foo:2.1.7"); this.uris.setProperty("bar", "file:///bar-1.2.3.jar"); } @Test public void populateRegistry() throws Exception { String localUri = "local://local"; UriRegistryPopulator populator = new UriRegistryPopulator(); StubResourceLoader resourceLoader = new StubResourceLoader(new PropertiesResource(uris)); populator.setResourceLoader(resourceLoader); UriRegistry registry = new InMemoryUriRegistry(); populator.populateRegistry(true, registry, localUri); assertThat(resourceLoader.getRequestedLocations().contains(localUri)).isTrue(); assertThat(resourceLoader.getRequestedLocations().size()).isEqualTo(1); assertThat(registry.findAll().size()).isEqualTo(this.uris.size()); for (String key : this.uris.stringPropertyNames()) { assertThat(registry.find(key).toString()).isEqualTo(this.uris.getProperty(key)); } boolean thrown = false; try { registry.find("not present"); } catch (IllegalArgumentException e) { thrown = true; } finally { assertThat(thrown).isTrue(); } } @Test public void populateRegistryWithOverwrites() throws Exception { String localUri = "local://local"; UriRegistryPopulator populator = new UriRegistryPopulator(); PropertiesResource propertiesResource = new PropertiesResource(uris); StubResourceLoader resourceLoader = new StubResourceLoader(propertiesResource); populator.setResourceLoader(resourceLoader); UriRegistry registry = new InMemoryUriRegistry(); Map registered = populator.populateRegistry(true, registry, localUri); assertThat(registered.size() == 3).isTrue(); // Perform overwrites on the existing keys Map registeredWithNoOverwrites = populator.populateRegistry(false, registry, localUri); assertThat(registeredWithNoOverwrites.size() == 0).isTrue(); propertiesResource.addNewProperty("another", "maven://somegroup:someartifact:jar:exec:1.0.0"); Map newlyRegisteredWithNoOverwrites = populator.populateRegistry(false, registry, localUri); assertThat(newlyRegisteredWithNoOverwrites.size() == 1).isTrue(); propertiesResource.addNewProperty("yet-another", "file:///tmp/yet-another.jar"); Map newlyRegisteredWithOverwrites = populator.populateRegistry(true, registry, localUri); assertThat(newlyRegisteredWithOverwrites.size() == 5).isTrue(); } @Test public void populateRegistryInvalidUri() throws Exception { String localUri = "local://local"; Properties props = new Properties(); props.setProperty("test", "file:///bar-1.2.3.jar"); UriRegistryPopulator populator = new UriRegistryPopulator(); StubResourceLoader resourceLoader = new StubResourceLoader(new PropertiesResource(props)); populator.setResourceLoader(resourceLoader); UriRegistry registry = new InMemoryUriRegistry(); populator.populateRegistry(true, registry, localUri); assertThat(resourceLoader.getRequestedLocations().contains(localUri)).isTrue(); assertThat(resourceLoader.getRequestedLocations().size()).isEqualTo(1); assertThat(registry.findAll().size()).isEqualTo(1); assertThat(registry.find("test").toString()).isEqualTo("file:///bar-1.2.3.jar"); populator.populateRegistry(true, registry, localUri); props.setProperty("test", "invalid"); populator.populateRegistry(true, registry, localUri); assertThat(registry.find("test").toString()).isEqualTo("file:///bar-1.2.3.jar"); } /** * {@link Resource} implementation that returns an {@link InputStream} * fed by a {@link Properties} object. */ static class PropertiesResource extends AbstractResource { private final Properties properties; public PropertiesResource(Properties properties) { this.properties = properties; } public Properties addNewProperty(String key, String value) { if (this.properties != null) { this.properties.put(key, value); } return this.properties; } @Override public String getDescription() { return null; } @Override public InputStream getInputStream() throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); this.properties.store(out, "URIs"); return new ByteArrayInputStream(out.toByteArray()); } } } ================================================ FILE: spring-cloud-deployer-resource-support/src/test/java/org/springframework/cloud/deployer/resource/support/DelegatingResourceLoaderIntegrationTests.java ================================================ /* * Copyright 2017 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.cloud.deployer.resource.support; import java.io.File; import org.springframework.core.io.Resource; import org.junit.jupiter.api.Test; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; /** * @author Mark Pollack */ public class DelegatingResourceLoaderIntegrationTests { @Test public void test() throws Exception { DelegatingResourceLoader delegatingResourceLoader = new DelegatingResourceLoader(); Resource resource = delegatingResourceLoader.getResource("https://repo1.maven.org/maven2/org/springframework/cloud/stream/app/file-sink-rabbit/3.2.1/file-sink-rabbit-3.2.1.jar"); File file1 = resource.getFile(); File file2 = resource.getFile(); assertThat(file1).isEqualTo(file2); } } ================================================ FILE: spring-cloud-deployer-resource-support/src/test/java/org/springframework/cloud/deployer/resource/support/DelegatingResourceLoaderTests.java ================================================ /* * Copyright 2016 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.cloud.deployer.resource.support; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.springframework.cloud.deployer.resource.StubResourceLoader; import org.springframework.core.io.AbstractResource; import org.springframework.core.io.ResourceLoader; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; /** * Tests for {@link DelegatingResourceLoader}. * * @author Patrick Peralta * @author Janne Valkealahti * @author Ilayaperumal Gopinathan */ public class DelegatingResourceLoaderTests { @TempDir public File folder; @Test public void test() { NullResource one = new NullResource("one"); NullResource two = new NullResource("two"); NullResource three = new NullResource("three"); assertThat(two).isNotEqualTo(one); assertThat(three).isNotEqualTo(two); Map map = new HashMap<>(); map.put("one", new StubResourceLoader(one)); map.put("two", new StubResourceLoader(two)); map.put("three", new StubResourceLoader(three)); DelegatingResourceLoader resourceLoader = new DelegatingResourceLoader(map); assertEquals(one, resourceLoader.getResource("one://one")); assertEquals(two, resourceLoader.getResource("two://two")); assertEquals(three, resourceLoader.getResource("three://three")); } static class NullResource extends AbstractResource { final String description; public NullResource(String description) { this.description = description; } @Override public String getDescription() { return description; } @Override public File getFile() throws IOException { return null; } @Override public InputStream getInputStream() throws IOException { return null; } } } ================================================ FILE: spring-cloud-deployer-resource-support/src/test/java/org/springframework/cloud/deployer/resource/support/DownloadingUrlResourceTests.java ================================================ /* * Copyright 2017 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.cloud.deployer.resource.support; import java.io.File; import org.junit.jupiter.api.Test; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; /** * @author Mark Pollack */ public class DownloadingUrlResourceTests { @Test public void test() throws Exception { DownloadingUrlResource httpResource = new DownloadingUrlResource("https://repo1.maven.org/maven2/org/springframework/cloud/stream/app/file-sink-rabbit/3.2.1/file-sink-rabbit-3.2.1.jar"); File file1 = httpResource.getFile(); File file2 = httpResource.getFile(); assertThat(file1).isEqualTo(file2); assertThat(file1.getName()).isEqualTo("81a23583726958052fdf75c399b81a3c4fcab6d1-filesinkrabbit321jar"); } } ================================================ FILE: spring-cloud-deployer-resource-support/src/test/java/org/springframework/cloud/deployer/resource/support/ShaUtilsTests.java ================================================ /* * Copyright 2016 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.cloud.deployer.resource.support; import java.security.SecureRandom; import java.util.HashSet; import java.util.Set; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; /** * Simple sha utils tests. * * @author Janne Valkealahti */ public class ShaUtilsTests { @Test public void testSimpleSmoke() { for (int j = 0; j < 100; j++) { Set nodups = new HashSet<>(); for (int i = 0; i < 1000; i++) { nodups.add(ShaUtils.sha1(randomString(20))); } assertEquals(nodups.size() == 1000, true); } } static final String CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!£$%^&*()-=+_"; static SecureRandom rnd = new SecureRandom(); String randomString(int len) { StringBuilder sb = new StringBuilder(len); for (int i = 0; i < len; i++) sb.append(CHARS.charAt(rnd.nextInt(CHARS.length()))); return sb.toString(); } } ================================================ FILE: spring-cloud-deployer-spi/pom.xml ================================================ 4.0.0 spring-cloud-deployer-spi jar Spring Cloud Deployer SPI org.springframework.cloud spring-cloud-deployer-parent 3.0.0-SNAPSHOT .. org.springframework spring-core org.springframework spring-web io.projectreactor reactor-core org.junit.jupiter junit-jupiter test org.assertj assertj-core test ================================================ FILE: spring-cloud-deployer-spi/src/main/java/org/springframework/cloud/deployer/spi/app/AbstractActuatorTemplate.java ================================================ /* * Copyright 2022 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.cloud.deployer.spi.app; import java.nio.charset.Charset; import java.util.Collections; import java.util.Optional; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.util.Assert; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; /** * Base class for ActuatorTemplate implementations. * * @author David Turanski */ public abstract class AbstractActuatorTemplate implements ActuatorOperations { protected final Log logger = LogFactory.getLog(getClass().getName()); protected final RestTemplate restTemplate; protected final AppDeployer appDeployer; private final AppAdmin appAdmin; private final Optional defaultAuthenticationHeaderValue; protected AbstractActuatorTemplate(RestTemplate restTemplate, AppDeployer appDeployer, AppAdmin appAdmin) { Assert.notNull(restTemplate, "'restTemplate' is required."); Assert.notNull(appDeployer, "'appDeployer' is required."); Assert.notNull(appAdmin, "'appAdmin' is required."); this.restTemplate = restTemplate; this.appDeployer = appDeployer; this.appAdmin = appAdmin; this.defaultAuthenticationHeaderValue = prepareDefaultAthentication(appAdmin); } @Override public T getFromActuator(String deploymentId, String guid, String endpoint, Class responseType, Optional optionalRequestHeaders) { AppInstanceStatus appInstanceStatus = getDeployedInstance(deploymentId, guid) .orElseThrow(() -> new IllegalStateException( String.format("App with deploymentId %s and guid %s not deployed", deploymentId, guid))); String actuatorUrl = getActuatorUrl(appInstanceStatus); HttpHeaders requestHeaders = requestHeaders(httpHeadersForInstance(appInstanceStatus), optionalRequestHeaders); ResponseEntity responseEntity = httpGet(UriComponentsBuilder .fromUriString(actuatorUrl).path(normalizePath(endpoint)).toUriString(), responseType, requestHeaders); if (responseEntity.getStatusCode().isError()) { logger.error(responseEntity.getStatusCode().toString()); } return responseEntity.getBody(); } @Override public R postToActuator(String deploymentId, String guid, String endpoint, T body, Class responseType, Optional optionalRequestHeaders) { AppInstanceStatus appInstanceStatus = getDeployedInstance(deploymentId, guid) .orElseThrow(() -> new IllegalStateException( String.format("App with deploymentId %s and guid %s not deployed", deploymentId, guid))); String actuatorUrl = getActuatorUrl(appInstanceStatus); HttpHeaders requestHeaders = requestHeaders(httpHeadersForInstance(appInstanceStatus), optionalRequestHeaders); ResponseEntity responseEntity = httpPost(UriComponentsBuilder .fromUriString(actuatorUrl).path(normalizePath(endpoint)).toUriString(), body, responseType, requestHeaders); if (responseEntity.getStatusCode().isError()) { logger.error(responseEntity.getStatusCode().toString()); } return responseEntity.getBody(); } protected final String getActuatorUrl(AppInstanceStatus appInstanceStatus) { try { return actuatorUrlForInstance(appInstanceStatus); } catch (Exception e) { throw new IllegalArgumentException(String.format( "Unable to determine actuator url for app with guid %s", appInstanceStatus.getAttributes().get("guid"))); } } protected abstract String actuatorUrlForInstance(AppInstanceStatus appInstanceStatus); /** * Hook to allow subclasses to add special headers derived {@link AppInstanceStatus} metadata if necessary. * * @param appInstanceStatus the AppInstanceStatus for the target instance. * @return HttpHeaders */ protected Optional httpHeadersForInstance(AppInstanceStatus appInstanceStatus) { return Optional.empty(); } private final HttpHeaders requestHeaders(Optional optionalAppInstanceHeaders, Optional optionalRequestHeaders) { HttpHeaders requestHeaders = optionalAppInstanceHeaders .orElse(new HttpHeaders()); optionalRequestHeaders.ifPresent(requestHeaders::addAll); //Any pass-thru auth overrides the default. if (!requestHeaders.containsKey(HttpHeaders.AUTHORIZATION)) { this.defaultAuthenticationHeaderValue.ifPresent(auth -> requestHeaders.setBasicAuth(auth)); } requestHeaders.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); requestHeaders.setContentType(MediaType.APPLICATION_JSON); return requestHeaders; } private final String normalizePath(String path) { return path.startsWith("/") ? path : "/" + path; } private final ResponseEntity httpGet(String url, Class responseType, HttpHeaders requestHeaders) { return restTemplate.exchange(url, HttpMethod.GET, new HttpEntity(requestHeaders), responseType); } private final ResponseEntity httpPost(String url, T requestBody, Class responseType, HttpHeaders requestHeaders) { return restTemplate.exchange(url, HttpMethod.POST, new HttpEntity(requestBody, requestHeaders), responseType); } private final Optional getDeployedInstance(String deploymentId, String guid) { AppStatus appStatus = appDeployer.status(deploymentId); long count = appStatus.getInstances().values().stream().filter( appInstance -> appInstance.getAttributes().get("guid").equals(guid)).count(); if (count == 0) { return Optional.empty(); } else if (count > 1) { throw new IllegalStateException(String.format( "guid %s is not unique for instances of deploymentId %s", guid, deploymentId)); } return appStatus.getInstances().values().stream() .filter(appInstance -> appInstance.getState() == DeploymentState.deployed && appInstance.getAttributes().get("guid").equals(guid)) .findFirst(); } private Optional prepareDefaultAthentication(AppAdmin appAdmin) { Optional encodeBasicAuth; encodeBasicAuth = appAdmin.hasCredentials() ? Optional.of(HttpHeaders.encodeBasicAuth(appAdmin.getUser(), appAdmin.getPassword(), Charset.defaultCharset())) : Optional.empty(); if (!encodeBasicAuth.isPresent()) { logger.warn("No app admin credentials have been configured for " + this.getClass().getName()); } return encodeBasicAuth; } } ================================================ FILE: spring-cloud-deployer-spi/src/main/java/org/springframework/cloud/deployer/spi/app/ActuatorOperations.java ================================================ /* * Copyright 2022 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.cloud.deployer.spi.app; import java.util.Optional; import org.springframework.http.HttpHeaders; /** * Deployers may implement this extension to invoke actuator endpoints of deployed app instances. * * @author David Turanski */ public interface ActuatorOperations { /** * Get a resource from an actuator path. * * @param deploymentId the deployment ID of the deployed app. * @param guid unique id for the app instance. * @param endpoint the endpoint path relative to the base actuator URL for the instance, with or without preceding '/'. * @param responseType the expected response type. * @param requestHeaders optional request headers. * @param type of the response. * @return the contents as the given type. */ T getFromActuator(String deploymentId, String guid, String endpoint, Class responseType, Optional requestHeaders); /** * Get a resource from an actuator path. * * @param deploymentId the deployment ID of the deployed app. * @param guid unique id for the app instance. * @param endpoint the endpoint path relative to the base actuator URL for the instance, with or without preceding '/'. * @param responseType the expected response type. * @param type of the response * @return the contents as the given type. */ default T getFromActuator(String deploymentId, String guid, String endpoint, Class responseType) { return getFromActuator(deploymentId, guid, endpoint, responseType, Optional.empty()); } /** * Get a resource from an actuator path. * * @param deploymentId the deployment ID of the deployed app. * @param guid unique id for the app instance. * @param endpoint the endpoint path relative to the base actuator URL for the instance, with or without preceding '/'. * @param requestHeaders optional request headers. * @return the contents as a {@code String}. */ default String getFromActuator( String deploymentId, String guid, String endpoint, Optional requestHeaders ) { return getFromActuator(deploymentId, guid, endpoint, String.class, requestHeaders); } /** * Get a resource from an actuator path. * * @param deploymentId the deployment ID of the deployed app. * @param guid unique id for the app instance. * @param endpoint the endpoint path relative to the base actuator URL for the instance, with or without preceding '/'. * @return the contents as a {@code String}. */ default String getFromActuator(String deploymentId, String guid, String endpoint) { return getFromActuator(deploymentId, guid, endpoint, String.class, Optional.empty()); } /** * Post to resource on actuator path. * * @param deploymentId the deployment ID of the deployed app. * @param guid unique id for the app instance. * @param endpoint the endpoint path relative to the base actuator URL for the instance, with or without preceding '/'. * @param body the request body. * @param responseType the expected response type. * @param requestHeaders optional request headers. * @param type of the body. * @param type of the response. * @return the result (response body). */ R postToActuator(String deploymentId, String guid, String endpoint, T body, Class responseType, Optional requestHeaders); /** * Post to resource on actuator path. * * @param deploymentId the deployment ID of the deployed app. * @param guid unique id for the app instance. * @param endpoint the endpoint path relative to the base actuator URL for the instance, with or without preceding '/'. * @param body the request body. * @param responseType the expected response type. * @param the type of the response. * @param the type of the body. * @return the result (response body). */ default R postToActuator(String deploymentId, String guid, String endpoint, T body, Class responseType) { return postToActuator(deploymentId, guid, endpoint, body, responseType, Optional.empty()); } } ================================================ FILE: spring-cloud-deployer-spi/src/main/java/org/springframework/cloud/deployer/spi/app/AppAdmin.java ================================================ /* * Copyright 2022 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.cloud.deployer.spi.app; import java.util.Map; import org.springframework.util.StringUtils; public class AppAdmin { public static final String ADMIN_USER_KEY = "SPRING_CLOUD_STREAMAPP_SECURITY_ADMIN-USER"; public static final String ADMIN_PASSWORD_KEY = "SPRING_CLOUD_STREAMAPP_SECURITY_ADMIN-PASSWORD"; public static final String ADMIN_USER_PROPERTY_KEY = "spring.cloud.streamapp.security.admin-user"; public static final String ADMIN_PASSWORD_PROPERTY_KEY = "spring.cloud.streamapp.security.admin-password"; /** * Username used to access protected application resources, e.g., POST to actuator. */ private String user; /** * Password used to access protected application resources, e.g., POST to actuator. */ private String password; public String getUser() { return user; } public void setUser(String user) { this.user = user; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public boolean hasCredentials() { return StringUtils.hasText(this.user) && StringUtils.hasText(this.password); } public void addCredentialsToAppEnvironment(Map environment) { /* * These are configured credentials which the app security can use configure an authorized user * to access protected resources, for example, ActuatorOperations.postToActuator() provides these credentials. * This is intended to work for any app autoconfigured with spring-cloud-stream-applications-common-security, * as are the pre-packaged stream apps. */ if (this.hasCredentials()) { environment.put(ADMIN_USER_KEY, this.user); environment.put(ADMIN_PASSWORD_KEY, this.password); } } public void addCredentialsToAppEnvironmentAsProperties(Map environment) { /* * These are configured credentials which the app security can use configure an authorized user * to access protected resources, for example, ActuatorOperations.postToActuator() provides these credentials. * This is intended to work for any app autoconfigured with spring-cloud-stream-applications-common-security, * as are the pre-packaged stream apps. */ if (this.hasCredentials()) { environment.put(ADMIN_USER_PROPERTY_KEY, this.user); environment.put(ADMIN_PASSWORD_PROPERTY_KEY, this.password); } } } ================================================ FILE: spring-cloud-deployer-spi/src/main/java/org/springframework/cloud/deployer/spi/app/AppDeployer.java ================================================ /* * Copyright 2016-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.cloud.deployer.spi.app; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; /** * SPI defining a runtime environment capable of deploying and managing the * lifecycle of apps that are intended to run indefinitely (until undeployed), * as opposed to {@link org.springframework.cloud.deployer.spi.task.TaskLauncher * tasks}. * * @author Mark Fisher * @author Patrick Peralta * @author Marius Bogoevici * @author Janne Valkealahti * @author Thomas Risberg * @author Ilayaperumal Gopinathan * @author David Turanski */ public interface AppDeployer { /** * Common prefix used for deployment properties. */ static final String PREFIX = "spring.cloud.deployer."; /** * The deployment property for the count (number of app instances). * If not provided, a deployer should assume 1 instance. */ static final String COUNT_PROPERTY_KEY = PREFIX + "count"; /** * The deployment property for the group to which an app belongs. * If not provided, a deployer should assume no group. */ static final String GROUP_PROPERTY_KEY = PREFIX + "group"; /** * The deployment property that indicates if each app instance should have an index value * within a sequence from 0 to N-1, where N is the value of the {@value #COUNT_PROPERTY_KEY} * property. If not provided, a deployer should assume app instance indexing is not necessary. */ static final String INDEXED_PROPERTY_KEY = PREFIX + "indexed"; /** * The property to be set at each instance level to specify the sequence number * amongst 0 to N-1, where N is the value of the {@value #COUNT_PROPERTY_KEY} property. * Specified as CAPITAL_WITH_UNDERSCORES as this is typically passed as an environment * variable, but when targeting a Spring app, other variations may apply. * * @see #INDEXED_PROPERTY_KEY */ static final String INSTANCE_INDEX_PROPERTY_KEY = "INSTANCE_INDEX"; /** * The deployment property for the memory setting for the container that will run the app. * The memory is specified in Mebibytes, * by default, with optional case-insensitive trailing unit 'm' and 'g' being supported, * for mebi- and giga- respectively. *

* 1 MiB = 2^20 bytes = 1024*1024 bytes vs. the decimal based 1MB = 10^6 bytes = 1000*1000 bytes, *

* Implementations are expected to translate this value to the target platform as faithfully as possible. * * @see org.springframework.cloud.deployer.spi.util.ByteSizeUtils */ static final String MEMORY_PROPERTY_KEY = PREFIX + "memory"; /** * The deployment property for the disk setting for the container that will run the app. * The memory is specified in Mebibytes, * by default, with optional case-insensitive trailing unit 'm' and 'g' being supported, * for mebi- and giga- respectively. *

* 1 MiB = 2^20 bytes = 1024*1024 bytes vs. the decimal based 1MB = 10^6 bytes = 1000*1000 bytes, *

* Implementations are expected to translate this value to the target platform as faithfully as possible. * * @see org.springframework.cloud.deployer.spi.util.ByteSizeUtils */ static final String DISK_PROPERTY_KEY = PREFIX + "disk"; /** * The deployment property for the cpu setting for the container that will run the app. * The cpu is specified as whole multiples or decimal fractions of virtual cores. Some platforms will not * support setting cpu and will ignore this setting. Other platforms may require whole numbers and might * round up. Exactly how this property affects the deployments will vary between implementations. */ static final String CPU_PROPERTY_KEY = PREFIX + "cpu"; /** * Deploy an app using an {@link AppDeploymentRequest}. The returned id is * later used with {@link #undeploy(String)} or {@link #status(String)} to * undeploy an app or check its status, respectively. * * Implementations may perform this operation asynchronously; therefore a * successful deployment may not be assumed upon return. To determine the * status of a deployment, invoke {@link #status(String)}. * * @param request the app deployment request * @return the deployment id for the app * @throws IllegalStateException if the app has already been deployed */ String deploy(AppDeploymentRequest request); /** * Un-deploy an app using its deployment id. Implementations may perform * this operation asynchronously; therefore a successful un-deployment may * not be assumed upon return. To determine the status of a deployment, * invoke {@link #status(String)}. * * @param id the app deployment id, as returned by {@link #deploy} * @throws IllegalStateException if the app has not been deployed */ void undeploy(String id); /** * Return the {@link AppStatus} for an app represented by a deployment id. * * @param id the app deployment id, as returned by {@link #deploy} * @return the app deployment status */ AppStatus status(String id); /** * Return the {@link AppStatus} for an app represented by a deployment id. * * @param id the app deployment id, as returned by {@link #deploy} * @return the app deployment status */ default Mono statusReactive(String id) { return Mono.defer(() -> Mono.just(status(id))); } /** * Return the {@link AppStatus}s for an app represented by a deployment ids. * * @param ids the app deployment ids, as returned by {@link #deploy} * @return the app deployment statuses */ default Flux statusesReactive(String... ids) { return Flux.fromArray(ids).flatMap(id -> statusReactive(id)); } /** * Return the environment info for this deployer. * * @return the runtime environment info */ RuntimeEnvironmentInfo environmentInfo(); /** * Return the log of the application identified by the deployment id. * @param id the id of the deployment. * @return the application log */ default String getLog(String id) { throw new UnsupportedOperationException("'getLog' is not implemented."); } /** * Scale an app according to given values. * * @param appScaleRequest an {@link AppScaleRequest}. */ default void scale(AppScaleRequest appScaleRequest) { throw new UnsupportedOperationException("'scale' is not implemented."); } } ================================================ FILE: spring-cloud-deployer-spi/src/main/java/org/springframework/cloud/deployer/spi/app/AppInstanceStatus.java ================================================ /* * Copyright 2015-2016 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.cloud.deployer.spi.app; import java.util.Map; /** * Status for an individual app instance deployment. * The underlying instance may be backed by an application context in * the JVM, or by a remote process managed by a distributed runtime. * * @author Patrick Peralta * @author Mark Fisher */ public interface AppInstanceStatus { /** * Return a unique identifier for the deployed app. * * @return identifier for the deployed app */ String getId(); /** * Return the state of the deployed app instance. * * @return state of the deployed app instance */ DeploymentState getState(); /** * Return a map of attributes for the deployed app instance. The specific * keys/values returned are dependent on the runtime executing the app. * This may include extra information such as deployment location * or specific error messages in the case of failure. * * @return map of attributes for the deployed app */ Map getAttributes(); } ================================================ FILE: spring-cloud-deployer-spi/src/main/java/org/springframework/cloud/deployer/spi/app/AppScaleRequest.java ================================================ /* * Copyright 2019 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.cloud.deployer.spi.app; import java.util.Map; import java.util.Optional; import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** * Representation of an app scale request. This includes the application's deployment Id, the number of desired * instances, and optionally, properties that may be applied during the scaling operation. * * @author David Turanski * @since 2.1.0 */ public class AppScaleRequest { private final int count; private final String deploymentId; private final Optional> properties; /** * * @param deploymentId the unique deployment ID of the application. * @param count the desired instance count. */ public AppScaleRequest(String deploymentId, int count) { this(deploymentId, count, null); } /** * * @param deploymentId the unique deployment ID of the application. * @param count the desired instance count. * @param properties optional properties that may be applied during the scale operation. */ public AppScaleRequest(String deploymentId, int count, @Nullable Map properties) { Assert.hasText(deploymentId,"'deploymentId', must not be empty or null"); Assert.state(count >= 0, "'count' must be >= 0"); this.deploymentId = deploymentId; this.count = count; this.properties = Optional.ofNullable(properties); } /** * * @return the desired instance count. */ public int getCount() { return count; } /** * * @return the deployment ID. */ public String getDeploymentId() { return deploymentId; } /** * * @return the {@link Optional} properties. */ public Optional> getProperties() { return properties; } @Override public String toString() { final StringBuffer sb = new StringBuffer("AppScaleRequest{"); sb.append("count=").append(count); sb.append(", deploymentId='").append(deploymentId).append('\''); sb.append(", properties=").append(properties); sb.append('}'); return sb.toString(); } } ================================================ FILE: spring-cloud-deployer-spi/src/main/java/org/springframework/cloud/deployer/spi/app/AppStatus.java ================================================ /* * Copyright 2016-2017 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.cloud.deployer.spi.app; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.springframework.util.Assert; /** * Status of an app which is initially constructed from an * {@link org.springframework.cloud.deployer.spi.core.AppDeploymentRequest} and * runtime deployment properties by a deployer during deployment. This status is * composed of an aggregate of all individual app instance deployments. *

* Consumers of the SPI obtain the app status via * {@link org.springframework.cloud.deployer.spi.app.AppDeployer#status(String)} * , whereas SPI implementations create instances of this class via * {@link AppStatus.Builder}. * * @author Patrick Peralta * @author Mark Fisher * @see AppInstanceStatus */ public class AppStatus { /** * The id of the app this status is for. */ private final String deploymentId; /** * Map of {@link AppInstanceStatus} keyed by a unique identifier * for each app deployment instance. */ private final Map instances = new HashMap(); private final DeploymentState generalState; /** * Construct a new {@code AppStatus}. * * @param deploymentId id of the app this status is for * @param generalState a value for general state of the app, or {@literal null} if this should be derived from instances */ protected AppStatus(String deploymentId, DeploymentState generalState) { this.deploymentId = deploymentId; this.generalState = generalState; } /** * Return the app deployment id. * * @return app deployment id */ public String getDeploymentId() { return deploymentId; } /** * Return the deployment state for the the app. If the descriptor * indicates multiple instances, this state represents an aggregate * of all individual app instances. * * @return deployment state for the app */ public DeploymentState getState() { if (generalState != null) { return generalState; } Set states = new HashSet<>(); for (Map.Entry entry : instances.entrySet()) { states.add(entry.getValue().getState()); } if (states.isEmpty()) { return DeploymentState.unknown; } if (states.size() == 1) { return states.iterator().next(); } if (states.contains(DeploymentState.error)) { return DeploymentState.error; } if (states.contains(DeploymentState.deploying)) { return DeploymentState.deploying; } if (states.contains(DeploymentState.deployed) || states.contains(DeploymentState.partial)) { return DeploymentState.partial; } if (states.contains(DeploymentState.failed)) { return DeploymentState.failed; } // reaching here is unlikely; it would require some // combination of unknown, undeployed, complete return DeploymentState.partial; } public String toString() { return this.getState().name(); } /** * Return a map of {@code AppInstanceStatus} keyed by a unique identifier * for each app instance. * @return map of {@code AppInstanceStatus} */ public Map getInstances() { return Collections.unmodifiableMap(this.instances); } private void addInstance(String id, AppInstanceStatus status) { this.instances.put(id, status); } /** * Return a {@code Builder} for {@code AppStatus}. * @param id of the app this status is for * @return {@code Builder} for {@code AppStatus} */ public static Builder of(String id) { return new Builder(id); } /** * Utility class constructing an instance of {@link AppStatus} * using a builder pattern. */ public static class Builder { private final String id; private DeploymentState generalState; private List statuses = new ArrayList<>(); /** * Instantiates a new builder. * * @param id the app deployment id */ private Builder(String id) { this.id = id; } /** * Add an instance of {@code AppInstanceStatus} to build the status for * the app. This will be invoked once per individual app instance. * * @param instance status of individual app deployment * @return this {@code Builder} */ public Builder with(AppInstanceStatus instance) { Assert.isNull(generalState, "Can't build an AppStatus from app instances if generalState has been set"); statuses.add(instance); return this; } /** * Set the state of the app as a direct value. This is to be used when no information about instances could * be determined (e.g. general error condition). * @param generalState the deployment state to set * @return this {@code Builder} */ public Builder generalState(DeploymentState generalState) { Assert.isTrue(statuses.isEmpty(), "Can't build an AppStatus from general state if some instances have been added"); this.generalState = generalState; return this; } /** * Return a new instance of {@code AppStatus} based on * the provided individual app instances via * {@link #with(AppInstanceStatus)}. * @return new instance of {@code AppStatus} */ public AppStatus build() { AppStatus status = new AppStatus(id, generalState); for (AppInstanceStatus instanceStatus : statuses) { status.addInstance(instanceStatus.getId(), instanceStatus); } return status; } } } ================================================ FILE: spring-cloud-deployer-spi/src/main/java/org/springframework/cloud/deployer/spi/app/DeploymentState.java ================================================ /* * Copyright 2015-2016 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.cloud.deployer.spi.app; /** * Deployment states for apps and groups. These may represent the * state of: *

    *
  • an entire group of apps
  • *
  • the global state of a deployed app as part of a group
  • *
  • the state of a particular instance of an app, in cases where * {@code app.count > 1}
  • *
* * @author Patrick Peralta * @author Eric Bottard * @author Mark Fisher */ public enum DeploymentState { /** * The app or group is being deployed. If there are multiple apps or * app instances, at least one of them is still being deployed. */ deploying, /** * All apps have been successfully deployed. */ deployed, /** * The app or group is known to the system, but is not currently deployed. */ undeployed, /** * In the case of multiple apps, some have successfully deployed, while * others have not. This state does not apply for individual app instances. */ partial, /** * All apps have failed deployment. */ failed, /** * A system error occurred trying to determine deployment status. */ error, /** * The app or group deployment is not known to the system. */ unknown; } ================================================ FILE: spring-cloud-deployer-spi/src/main/java/org/springframework/cloud/deployer/spi/app/MultiStateAppDeployer.java ================================================ /* * Copyright 2017-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.cloud.deployer.spi.app; import java.util.Map; import reactor.core.publisher.Mono; /** * Extension of the AppDeployer interface that adds an additional * method to return the DeploymentState for a collection of deployment ids. */ public interface MultiStateAppDeployer extends AppDeployer { /** * Return the {@link DeploymentState} for all the apps represented by * a collection of deployment ids. * * @param ids the collection of app deployment ids, as returned by {@link #deploy} * @return a Map of deployment id and DeploymentState */ Map states(String ... ids); /** * Return the {@link DeploymentState} for all the apps represented by * a collection of deployment ids. * * @param ids the collection of app deployment ids, as returned by {@link #deploy} * @return a Map of deployment id and DeploymentState */ default Mono> statesReactive(String ... ids) { return Mono.defer(() -> Mono.just(states(ids))); } } ================================================ FILE: spring-cloud-deployer-spi/src/main/java/org/springframework/cloud/deployer/spi/core/AppDefinition.java ================================================ /* * Copyright 2016 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.cloud.deployer.spi.core; import java.util.Collections; import java.util.HashMap; import java.util.Map; import org.springframework.core.style.ToStringCreator; import org.springframework.util.Assert; /** * Definition of an app, including its name and its properties. * * A deployer may not modify properties in this class as those are meant for an * actual app as is. The deployer's only responsibility is to pass those * properties into a runtime environment in a way that they are available in a * running application. For example, they could be passed as env vars. * * @author Mark Fisher * @author Janne Valkealahti */ public class AppDefinition { /** * Name of the app. */ private final String name; /** * Properties for this app. */ private final Map properties; /** * Construct an {@code AppDefinition}. * * @param name name of app * @param properties app properties; may be {@code null} */ public AppDefinition(String name, Map properties) { Assert.notNull(name, "name must not be null"); this.name = name; this.properties = properties == null ? Collections.emptyMap() : Collections.unmodifiableMap(new HashMap(properties)); } /** * Return the name of this app. * * @return the app name */ public String getName() { return name; } /** * Gets the app definition properties. These properties are passed into a running app. * * @return the unmodifiable map of app properties */ public Map getProperties() { return properties; } @Override public String toString() { return new ToStringCreator(this) .append("name", this.name) .append("properties", this.properties).toString(); } } ================================================ FILE: spring-cloud-deployer-spi/src/main/java/org/springframework/cloud/deployer/spi/core/AppDeploymentRequest.java ================================================ /* * Copyright 2016 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.cloud.deployer.spi.core; import java.util.Collections; import java.util.List; import java.util.Map; import org.springframework.core.io.Resource; import org.springframework.core.style.ToStringCreator; import org.springframework.util.Assert; /** * Representation of an app deployment request. This includes the * {@link AppDefinition}, the {@link Resource} representing its deployable * artifact, and any deployment properties. * * Deployment properties are related to a specific implementation of the SPI * and will never be passed into an app itself. For example, a runtime container * may allow the definition of various settings for a context where the actual * app is executed, such as allowed memory, cpu or simply a way to define * colocation like node labeling. * * For passing properties into the app itself, use {@link AppDefinition#getProperties()}. * Those could be passed as env vars, or whatever approach is best for the target * platform. Each deployer implementation should clearly document how it handles * these properties. * * For passing command line arguments into the app itself, use {@link #commandlineArguments}. * * @author Mark Fisher * @author Janne Valkealahti * @author Oleg Zhurakousky */ public class AppDeploymentRequest { /** * App definition. */ private final AppDefinition definition; /** * Resource representing the artifact of the underlying app. */ private final Resource resource; /** * Map of deployment properties to be used by the deployer. */ private final Map deploymentProperties; /** * List of command line arguments for the target runtime of the app. */ private final List commandlineArguments; /** * Construct an {@code AppDeploymentRequest}. * * @param definition app definition * @param resource resource for the underlying app's artifact * @param deploymentProperties map of deployment properties; may be {@code null} */ public AppDeploymentRequest(AppDefinition definition, Resource resource, Map deploymentProperties) { this(definition, resource, deploymentProperties, null); } /** * Construct an {@code AppDeploymentRequest}. * * @param definition app definition * @param resource resource for the underlying app's artifact * @param deploymentProperties map of deployment properties; may be {@code null} * @param commandlineArguments set of command line arguments; may be {@code null} */ public AppDeploymentRequest(AppDefinition definition, Resource resource, Map deploymentProperties, List commandlineArguments) { Assert.notNull(definition, "definition must not be null"); Assert.notNull(resource, "resource must not be null"); this.definition = definition; this.resource = resource; this.deploymentProperties = deploymentProperties == null ? Collections.emptyMap() : Collections.unmodifiableMap(deploymentProperties); this.commandlineArguments = commandlineArguments == null ? Collections.emptyList() : Collections.unmodifiableList(commandlineArguments); } /** * Construct an {@code AppDeploymentRequest} with no deployment properties. * * @param definition app definition * @param resource resource for the underlying app's artifact */ public AppDeploymentRequest(AppDefinition definition, Resource resource) { this(definition, resource, null); } /** * @see #definition * @return application definition */ public AppDefinition getDefinition() { return definition; } /** * @see #resource * @return the resource */ public Resource getResource() { return resource; } /** * @see #deploymentProperties * @return the deployment properties */ public Map getDeploymentProperties() { return deploymentProperties; } /** * @see #commandlineArguments * @return the commandline arguments */ public List getCommandlineArguments() { return commandlineArguments; } @Override public String toString(){ return new ToStringCreator(this) .append("commandlineArguments", this.commandlineArguments) .append("deploymentProperties", this.deploymentProperties) .append("definition", this.definition) .append("resource", this.resource) .toString(); } } ================================================ FILE: spring-cloud-deployer-spi/src/main/java/org/springframework/cloud/deployer/spi/core/RuntimeEnvironmentInfo.java ================================================ /* * Copyright 2017 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.cloud.deployer.spi.core; import java.util.HashMap; import java.util.Map; import org.springframework.cloud.deployer.spi.util.RuntimeVersionUtils; import org.springframework.core.SpringVersion; import org.springframework.util.Assert; /** * Class used to communicate the runtime environment info. * * @author Thomas Risberg */ public class RuntimeEnvironmentInfo { /** * The SPI version used by this implementation. */ private String spiVersion; /** * The name of this implementation (could be simple class name). */ private String implementationName; /** * The version of this implementation. */ private String implementationVersion; /** * The platform type for this implementation. */ private String platformType; /** * The platform API version for this implementation. */ private String platformApiVersion; /** * The client library version used by this implementation. */ private String platformClientVersion; /** * The version running on the host of the platform used by this implementation. */ private String platformHostVersion; /** * The Java version used by this implementation. */ private String javaVersion; /** * The Spring Framework version used by this implementation. */ private String springVersion; /** * The Spring Boot version used by this implementation. */ private String springBootVersion; /** * Platform specific properties */ private Map platformSpecificInfo = new HashMap<>(); private RuntimeEnvironmentInfo(Class spiClass, String implementationName, String implementationVersion, String platformType, String platformApiVersion, String platformClientVersion, String platformHostVersion, Map platformSpecificInfo) { Assert.notNull(spiClass, "spiClass is required"); Assert.notNull(implementationName, "implementationName is required"); Assert.notNull(implementationVersion, "implementationVersion is required"); Assert.notNull(platformType, "platformType is required"); Assert.notNull(platformApiVersion, "platformApiVersion is required"); Assert.notNull(platformClientVersion, "platformClientVersion is required"); Assert.notNull(platformHostVersion, "platformHostVersion is required"); this.spiVersion = RuntimeVersionUtils.getVersion(spiClass); this.implementationName = implementationName; this.implementationVersion = implementationVersion; this.platformType = platformType; this.platformApiVersion = platformApiVersion; this.platformClientVersion = platformClientVersion; this.platformHostVersion = platformHostVersion; this.javaVersion = System.getProperty("java.version"); this.springVersion = SpringVersion.getVersion(); this.springBootVersion = RuntimeVersionUtils.getSpringBootVersion(); this.platformSpecificInfo.putAll(platformSpecificInfo); } public String getSpiVersion() { return spiVersion; } public String getImplementationName() { return implementationName; } public String getImplementationVersion() { return implementationVersion; } public String getPlatformType() { return platformType; } public String getPlatformApiVersion() { return platformApiVersion; } public String getPlatformClientVersion() { return platformClientVersion; } public String getPlatformHostVersion() { return platformHostVersion; } public String getJavaVersion() { return javaVersion; } public String getSpringVersion() { return springVersion; } public String getSpringBootVersion() { return springBootVersion; } public Map getPlatformSpecificInfo() { return platformSpecificInfo; } public static class Builder { private Class spiClass; private String implementationName; private String implementationVersion; private String platformType; private String platformApiVersion; private String platformClientVersion; private String platformHostVersion; private Map platformSpecificInfo = new HashMap<>(); public Builder() { } public Builder spiClass(Class spiClass) { this.spiClass = spiClass; return this; } public Builder implementationName(String implementationName) { this.implementationName = implementationName; return this; } public Builder implementationVersion(String implementationVersion) { this.implementationVersion = implementationVersion; return this; } public Builder platformType(String platformType) { this.platformType = platformType; return this; } public Builder platformApiVersion(String platformApiVersion) { this.platformApiVersion = platformApiVersion; return this; } public Builder platformClientVersion(String platformClientVersion) { this.platformClientVersion = platformClientVersion; return this; } public Builder platformHostVersion(String platformHostVersion) { this.platformHostVersion = platformHostVersion; return this; } public Builder addPlatformSpecificInfo(String key, String value) { this.platformSpecificInfo.put(key, value); return this; } public RuntimeEnvironmentInfo build() { return new RuntimeEnvironmentInfo(spiClass, implementationName, implementationVersion, platformType, platformApiVersion, platformClientVersion, platformHostVersion, platformSpecificInfo); } } } ================================================ FILE: spring-cloud-deployer-spi/src/main/java/org/springframework/cloud/deployer/spi/scheduler/CreateScheduleException.java ================================================ /* * Copyright 2018-2019 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.cloud.deployer.spi.scheduler; /** * Thrown when a schedule fails to be created on the scheduler infrastructure. * * @author Glenn Renfro * @author Ilayaperumal Gopinathan */ public class CreateScheduleException extends SchedulerException{ public CreateScheduleException(String scheduleName, Throwable t) { super(String.format("Failed to create schedule %s",scheduleName), t); } } ================================================ FILE: spring-cloud-deployer-spi/src/main/java/org/springframework/cloud/deployer/spi/scheduler/ScheduleInfo.java ================================================ /* * Copyright 2018-2019 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.cloud.deployer.spi.scheduler; import java.util.Map; /** * A {@code Schedule} represents the association between the task definition and the * times it is to be executed. The application, in this case, is represented by * the taskDefinitionName. The schedulerProperties, contain the information to calculate * when the next time the application should run (cron expression, etc)". * * @author Glenn Renfro * @author Ilayaperumal Gopinathan */ public class ScheduleInfo { /** * The name to be associated with the Schedule instance. */ private String scheduleName; /** * The task definition name associated with Schedule instance. */ private String taskDefinitionName; /** * Schedule specific information returned from the Scheduler implementation */ private Map scheduleProperties; public String getScheduleName() { return scheduleName; } public void setScheduleName(String scheduleName) { this.scheduleName = scheduleName; } public String getTaskDefinitionName() { return taskDefinitionName; } public void setTaskDefinitionName(String taskDefinitionName) { this.taskDefinitionName = taskDefinitionName; } public Map getScheduleProperties() { return scheduleProperties; } public void setScheduleProperties(Map scheduleProperties) { this.scheduleProperties = scheduleProperties; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof ScheduleInfo)) return false; ScheduleInfo that = (ScheduleInfo) o; if (scheduleName != null ? !scheduleName.equals(that.scheduleName) : that.scheduleName != null) return false; return true; } @Override public String toString() { return "ScheduleInfo{" + "scheduleName='" + scheduleName + '\'' + ", taskDefinitionName='" + taskDefinitionName + '\'' + ", scheduleProperties=" + scheduleProperties + '}'; } } ================================================ FILE: spring-cloud-deployer-spi/src/main/java/org/springframework/cloud/deployer/spi/scheduler/ScheduleRequest.java ================================================ /* * Copyright 2018-2019 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.cloud.deployer.spi.scheduler; import java.util.Collections; import java.util.List; import java.util.Map; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.core.io.Resource; import org.springframework.core.style.ToStringCreator; /** * Representation of a schedule request. This includes the {@link AppDefinition} * and any deployment properties. * * Deployment properties are related to a specific implementation of the SPI * and will never be passed into an app itself. For example, a runtime container * may allow the definition of various settings for a context where the actual * app is executed, such as allowed memory, cpu or simply a way to define * collocation like node labeling. * * For passing properties into the app itself, use {@link AppDefinition#getProperties()}. * Those could be passed as env vars, or whatever approach is best for the target * platform. Each deployer implementation should clearly document how it handles * these properties. * * For passing command line arguments into the app itself, use {@link #commandlineArguments}. * * @author Glenn Renfro * @author Ilayaperumal Gopinathan */ public class ScheduleRequest extends AppDeploymentRequest{ /** * The name of the Schedule. */ private final String scheduleName; private Map schedulerProperties; /** * Construct an {@code AppDeploymentRequest}. * * @param definition app definition. * @param schedulerProperties properties that contain scheduler specific information. * @param deploymentProperties map of deployment properties; may be {@code null}. * @param scheduleName the name associated with the schedule. * @param resource the resource associated with the request. */ @Deprecated public ScheduleRequest(AppDefinition definition, Map schedulerProperties, Map deploymentProperties, String scheduleName, Resource resource) { this(definition, schedulerProperties, deploymentProperties, null, scheduleName, resource); } /** * Construct an {@code AppDeploymentRequest}. * * @param definition app definition * @param schedulerProperties properties that contain scheduler specific information. * @param deploymentProperties map of deployment properties; may be {@code null} * @param commandlineArguments set of command line arguments; may be {@code null} * @param scheduleName the name associated with the schedule. * @param resource the resource associated with the request. */ @Deprecated public ScheduleRequest(AppDefinition definition, Map schedulerProperties, Map deploymentProperties, List commandlineArguments, String scheduleName, Resource resource) { super(definition, resource, deploymentProperties, commandlineArguments); this.scheduleName = scheduleName; this.schedulerProperties = schedulerProperties == null ? Collections.emptyMap() : Collections.unmodifiableMap(schedulerProperties); } /** * Construct an {@code AppDeploymentRequest}. * * @param definition app definition. * @param deploymentProperties map of deployment properties; may be {@code null}. * @param scheduleName the name associated with the schedule. * @param resource the resource associated with the request. */ public ScheduleRequest(AppDefinition definition, Map deploymentProperties, String scheduleName, Resource resource) { this(definition, deploymentProperties, (List) null, scheduleName, resource); } /** * Construct an {@code AppDeploymentRequest}. * * @param definition app definition * @param deploymentProperties map of deployment properties; may be {@code null} * @param commandlineArguments set of command line arguments; may be {@code null} * @param scheduleName the name associated with the schedule. * @param resource the resource associated with the request. */ public ScheduleRequest(AppDefinition definition, Map deploymentProperties, List commandlineArguments, String scheduleName, Resource resource) { super(definition, resource, deploymentProperties, commandlineArguments); this.scheduleName = scheduleName; } /** * @see #scheduleName * @return the schedule name */ public String getScheduleName() { return scheduleName; } @Deprecated public Map getSchedulerProperties() { return schedulerProperties; } @Deprecated public void setSchedulerProperties(Map schedulerProperties) { this.schedulerProperties = schedulerProperties; } @Override public String toString(){ return new ToStringCreator(this) .append("scheduleName", this.scheduleName) .append("schedulerProperties", this.schedulerProperties) .toString(); } } ================================================ FILE: spring-cloud-deployer-spi/src/main/java/org/springframework/cloud/deployer/spi/scheduler/Scheduler.java ================================================ /* * Copyright 2018-2019 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.cloud.deployer.spi.scheduler; import java.util.List; import org.springframework.core.io.Resource; /** * A {@code Scheduler} is a component that provides a way to register the execution of a * {@link ScheduleRequest} with an underlying scheduler system (Quartz, etc). * * @author Glenn Renfro * @author Ilayaperumal Gopinathan * */ public interface Scheduler { /** * Registers the {@link ScheduleRequest} to be executed based on the * cron expression provided. If an error occurs during schedule creation * then a {@link CreateScheduleException} should be thrown. * * @param scheduleRequest A request representing a sched-uable * artifact({@link org.springframework.cloud.deployer.spi.core.AppDefinition}, * the {@link Resource}), schedule properties, and deployment properties. */ void schedule(ScheduleRequest scheduleRequest); /** * Deletes a schedule that has been created. If an error occurs during * un-scheduling then a {@link UnScheduleException} should be thrown. * * @param scheduleName the name of the schedule to be removed. */ void unschedule(String scheduleName); /** * List all of the Schedules associated with the provided AppDefinition. * If an error occurs during list generation then a {@link SchedulerException} * should be thrown. * * @param taskDefinitionName to retrieve {@link ScheduleInfo}s for a specified taskDefinitionName. * @return A List of {@link ScheduleInfo}s configured for the provided taskDefinitionName. */ List list(String taskDefinitionName) ; /** * List all of the {@link ScheduleInfo}s registered with the system. * If an error occurs during list generation then a {@link SchedulerException} * should be thrown. * * @return A List of {@link ScheduleInfo}s for the given system. */ List list(); } ================================================ FILE: spring-cloud-deployer-spi/src/main/java/org/springframework/cloud/deployer/spi/scheduler/SchedulerException.java ================================================ /* * Copyright 2018-2019 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.cloud.deployer.spi.scheduler; /** * Base Exception class for Spring Cloud Scheduler. * * @author Glenn Renfro * @author Ilayaperumal Gopinathan */ public class SchedulerException extends RuntimeException{ public SchedulerException(String exceptionMessage) { super(exceptionMessage); } public SchedulerException(String exceptionMessage, Throwable t) { super(exceptionMessage, t); } } ================================================ FILE: spring-cloud-deployer-spi/src/main/java/org/springframework/cloud/deployer/spi/scheduler/SchedulerPropertyKeys.java ================================================ /* * Copyright 2018-2019 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.cloud.deployer.spi.scheduler; /** * Spring Cloud Scheduler property keys. * * @author Glenn Renfro * @author Ilayaperumal Gopinathan */ public class SchedulerPropertyKeys { public static final String PREFIX = "spring.cloud.scheduler."; /** * Scheduler cron property key prefix. */ public static final String CRON_PREFIX = PREFIX + "cron."; /** * Scheduler cron expression property key. */ public static final String CRON_EXPRESSION = CRON_PREFIX + "expression"; } ================================================ FILE: spring-cloud-deployer-spi/src/main/java/org/springframework/cloud/deployer/spi/scheduler/UnScheduleException.java ================================================ /* * Copyright 2018-2019 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.cloud.deployer.spi.scheduler; /** * Thrown when a schedule fails to be unscheduled on the scheduler infrastructure. * * @author Glenn Renfro * @author Ilayaperumal Gopinathan */ public class UnScheduleException extends SchedulerException { public UnScheduleException(String scheduleName) { super(String.format("Failed to unschedule %s", scheduleName)); } public UnScheduleException(String scheduleName, Throwable t) { super(String.format("Failed to unschedule %s", scheduleName), t); } } ================================================ FILE: spring-cloud-deployer-spi/src/main/java/org/springframework/cloud/deployer/spi/task/LaunchState.java ================================================ /* * Copyright 2016 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.cloud.deployer.spi.task; /** * Launch states for a Task. * * @author Mark Fisher */ public enum LaunchState { /** * The task launch has been requested, but it is not yet known to be in a running state. */ launching, /** * The task has been successfully launched, but has not yet completed. */ running, /** * The task has been cancelled. */ cancelled, /** * The task completed execution. */ complete, /** * The task failed to launch. */ failed, /** * A system error occurred trying to determine launch status. */ error, /** * The task is not known to the system. */ unknown; } ================================================ FILE: spring-cloud-deployer-spi/src/main/java/org/springframework/cloud/deployer/spi/task/TaskLauncher.java ================================================ /* * Copyright 2016-2019 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.cloud.deployer.spi.task; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; /** * SPI defining a runtime environment capable of launching and managing the * lifecycle of tasks. The term 'task' in the context of a {@code TaskLauncher} * is merely a runtime representation of a container or application wherein the * task should be executed. * * The SPI itself doesn't expect the launcher to keep state of launched tasks, * meaning it doesn't need to reconstruct all existing {@link TaskStatus}es or * the IDs needed to resolve a {@link TaskStatus}. The responsibility for * keeping track of the state of all existing tasks lies to whomever is using * this SPI. It is unrealistic to expect the launcher to be able to store enough * information using the underlying infrastructure to reconstruct the full * history of task executions. * * @author Mark Fisher * @author Janne Valkealahti * @author David Turanski * @author Ilayaperumal Gopinathan */ public interface TaskLauncher { /** * Launch a task for the provided {@link AppDeploymentRequest}. The returned * id may later be used with {@link #cancel(String)} or * {@link #status(String)} to cancel a task or get its status, * respectively. * * Implementations may perform this operation asynchronously; therefore a * successful launch may not be assumed upon return. To determine the status * of a launch, invoke {@link #status(String)}. * * @param request the task launch request * @return the id for the launched task */ String launch(AppDeploymentRequest request); /** * Cancel the task corresponding to the provided id. * * Implementations may perform this operation asynchronously; therefore a * successful cancellation may not be assumed upon return. To determine the * status of a cancellation, invoke {@link #status(String)}. * * @param id the task id, as returned by {@link #launch(AppDeploymentRequest)} */ void cancel(String id); /** * Returns the {@link TaskStatus} for a task represented by the provided id. * * @param id the task id, as returned by {@link #launch(AppDeploymentRequest)} * @return the task status */ TaskStatus status(String id); /** * Attempt to clean up any app execution resources that are associated with a task launch represented by the * provided task execution id. * * Implementations can choose to ignore this request if the underlying platform does not have any resources * associated with the task launch. Implementations may perform this operation asynchronously; therefore a * successful clean up may not be assumed upon return. * * @param id the task id, as returned by {@link #launch(AppDeploymentRequest)} */ void cleanup(String id); /** * Attempt to clean up any app resources that are associated with a task app represented by the provided * appName. Any app execution resources from all task launches for this app should be cleaned up as well. * * Implementations can choose to ignore this request if the underlying platform does not have any resources * associated with the task app and if there are no task launch resources created for this app. Implementations * may perform this operation asynchronously; therefore a successful clean up may not be assumed upon return. * * @param appName the app name as specified in {@link org.springframework.cloud.deployer.spi.core.AppDefinition#name} * from the {@link #launch(AppDeploymentRequest)} */ void destroy(String appName); /** * Return the environment info for this launcher/deployer. * * @return the deployer environment info */ RuntimeEnvironmentInfo environmentInfo(); /** * Implementations may limit the number of concurrent task executions. * * @return the maximum concurrent task executions allowed. */ default int getMaximumConcurrentTasks() { throw new UnsupportedOperationException("'getMaximumConcurrentTasks' is not implemented."); }; /** * * @return the count of currently running task executions if known. */ default int getRunningTaskExecutionCount() { throw new UnsupportedOperationException("'getRunningTaskExecutionCount' is not implemented."); } /** * Return the log of the application identified by the task ID. * The ID can be specific to the platform where the task is launched. * @param id the id of the task. * @return the task application log */ default String getLog(String id) { throw new UnsupportedOperationException("'getLog' is not implemented."); } } ================================================ FILE: spring-cloud-deployer-spi/src/main/java/org/springframework/cloud/deployer/spi/task/TaskStatus.java ================================================ /* * Copyright 2016-2017 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.cloud.deployer.spi.task; import java.util.Collections; import java.util.HashMap; import java.util.Map; /** * Status of a task launch which is initially constructed from an * {@link org.springframework.cloud.deployer.spi.core.AppDeploymentRequest} and * runtime properties by a {@link TaskLauncher}. *

* Consumers of the SPI obtain task status via * {@link org.springframework.cloud.deployer.spi.task.TaskLauncher#status}, * whereas SPI implementations create instances of this class via * {@link #TaskStatus(String, LaunchState, Map)} * * @author Patrick Peralta * @author Mark Fisher */ public class TaskStatus { /** * The id of the task this status is for. */ private final String id; /** * The {@link LaunchState} of the task. */ private final LaunchState state; /** * A map of attributes for the task. */ private final Map attributes; /** * Construct a new {@code TaskStatus}. * @param id the id of the task launch this status is for * @param state the {@link LaunchState} of the task * @param attributes map of attributes for the task */ public TaskStatus(String id, LaunchState state, Map attributes) { this.id = id; this.state = state; this.attributes = attributes == null ? Collections. emptyMap() : Collections.unmodifiableMap(new HashMap<>(attributes)); } /** * Return the task launch id for the task. * @return task launch id */ public String getTaskLaunchId() { return id; } /** * Return the state for the the task. * * @return state for the task */ public LaunchState getState() { return this.state; } /** * Return a string representation of this status. * * @return string representation of this status */ public String toString() { return this.getState().name(); } /** * Return a map of attributes for the launched task. The specific keys and * values returned are dependent on the runtime where the task has been * launched. This may include extra information such as execution location * or specific error messages in the case of failure. * * @return map of attributes for the task */ public Map getAttributes() { return this.attributes; } } ================================================ FILE: spring-cloud-deployer-spi/src/main/java/org/springframework/cloud/deployer/spi/util/ByteSizeUtils.java ================================================ /* * Copyright 2016 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.cloud.deployer.spi.util; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Utility class for dealing with parseable byte sizes, such as memory and disk limits. * * @author Eric Bottard */ public class ByteSizeUtils { private ByteSizeUtils() { } private static final Pattern SIZE_PATTERN = Pattern.compile("(?\\d+)(?(m|g)?)", Pattern.CASE_INSENSITIVE); /** * Return the number of mebibytes (1024*1024) denoted by the given text, where an optional case-insensitive unit of * 'm' or 'g' can be used to mean mebi- or gebi- bytes, respectively. Lack of unit assumes mebibytes. * @param text The text to parse * @return mebibytes */ public static long parseToMebibytes(String text) { Matcher matcher = SIZE_PATTERN.matcher(text); if (!matcher.matches()) { throw new IllegalArgumentException(String.format("Could not parse '%s' as a byte size." + " Expected a number with optional 'm' or 'g' suffix", text)); } long size = Long.parseLong(matcher.group("amount")); if (matcher.group("unit").equalsIgnoreCase("g")) { size *= 1024L; } return size; } } ================================================ FILE: spring-cloud-deployer-spi/src/main/java/org/springframework/cloud/deployer/spi/util/CommandLineTokenizer.java ================================================ /* * Copyright 2016 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.cloud.deployer.spi.util; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * A general purpose tokenizer for "command line" arrays. Allows tokenizing a single String into an array of args, * splitting the array on space characters and trimming. Quoting using single and double quotes is supported, in which * case those quotes can be escaped using a backslash character. * * @author Eric Bottard */ public class CommandLineTokenizer { private final char[] buffer; private int pos; private static final char ESCAPE_CHAR = '\\'; private final List args = new ArrayList<>(); public CommandLineTokenizer(String value) { this.buffer = value.toCharArray(); tokenize(); } public List getArgs() { return Collections.unmodifiableList(args); } private void tokenize() { while (pos < buffer.length) { eatWhiteSpace(); if (pos < buffer.length) { eatArg(); } } } private void eatWhiteSpace() { while (pos < buffer.length && buffer[pos] == ' ') { pos++; } } private void eatArg() { char endDelimiter; if (buffer[pos] == '\'' || buffer[pos] == '"') { endDelimiter = buffer[pos++]; } else { endDelimiter = ' '; } StringBuilder sb = new StringBuilder(); while (pos < buffer.length && buffer[pos] != endDelimiter) { if (buffer[pos] == ESCAPE_CHAR) { sb.append(processCharacterEscapeCodes(endDelimiter)); } else { sb.append(buffer[pos++]); } } if (pos == buffer.length && endDelimiter != ' ') { throw new IllegalStateException(String.format("Ran out of input in [%s], expected closing [%s]", new String(buffer), endDelimiter)); } else if (endDelimiter != ' ' && buffer[pos] == endDelimiter) { pos++; } args.add(sb.toString()); } /** * When the escape character is encountered, consume and return the escaped sequence. Note that depending on which * end delimiter is currently in use, not all combinations need to be escaped * @param endDelimiter the current endDelimiter */ private char processCharacterEscapeCodes(char endDelimiter) { pos++; if (pos >= buffer.length) { throw new IllegalStateException("Ran out of input in escape sequence"); } if (buffer[pos] == ESCAPE_CHAR) { pos++; // consume the second escape char return ESCAPE_CHAR; } else if (buffer[pos] == endDelimiter) { pos++; return endDelimiter; } else { // Not an actual escape. Do not increment pos, // and return the \ we consumed at the very beginning return ESCAPE_CHAR; } } } ================================================ FILE: spring-cloud-deployer-spi/src/main/java/org/springframework/cloud/deployer/spi/util/RuntimeVersionUtils.java ================================================ /* * Copyright 2017 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.cloud.deployer.spi.util; import org.springframework.util.StringUtils; /** * Utility class to be used for generating version info for various libraries. * * @author Thomas Risberg */ public class RuntimeVersionUtils { public static String getSpringBootVersion() { Class springApp; try { springApp = Class.forName("org.springframework.boot.SpringApplication"); } catch (ClassNotFoundException e) { return "unknown"; } return getVersion(springApp); } public static String getVersion(final Class source) { if (source == null) { return "null"; } Package sourcePackage = source.getPackage(); if (sourcePackage == null) { return "unknown"; } String version = source.getPackage().getImplementationVersion(); if (!StringUtils.hasText(version)) { return "unknown"; } return version; } } ================================================ FILE: spring-cloud-deployer-spi/src/test/java/org/springframework/cloud/deployer/spi/app/AppDeployerTests.java ================================================ /* * Copyright 2019 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.cloud.deployer.spi.app; import org.junit.jupiter.api.Test; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; import static org.assertj.core.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.assertEquals; /** * Tests for {@link AppDeployer} */ public class AppDeployerTests { @Test public void testAppDeployerGetLogDefaultMethod() { AppDeployer customAppDeployer = new AppDeployer() { @Override public String deploy(AppDeploymentRequest request) { return "Deployment request received."; } @Override public void undeploy(String id) { } @Override public AppStatus status(String id) { return AppStatus.of("id").build(); } @Override public RuntimeEnvironmentInfo environmentInfo() { return new RuntimeEnvironmentInfo.Builder().build(); } @Override public void scale(AppScaleRequest appScaleRequest) { } }; try { customAppDeployer.getLog("test"); fail(""); } catch (UnsupportedOperationException e) { assertEquals(e.getMessage(), "'getLog' is not implemented."); } } } ================================================ FILE: spring-cloud-deployer-spi/src/test/java/org/springframework/cloud/deployer/spi/app/RuntimeEnvironmentInfoBuilderTests.java ================================================ /* * Copyright 2017 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.cloud.deployer.spi.app; import org.junit.jupiter.api.Test; import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; import org.springframework.cloud.deployer.spi.util.RuntimeVersionUtils; import org.springframework.core.SpringVersion; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; /** * Tests for constructing a {@link RuntimeEnvironmentInfo} */ public class RuntimeEnvironmentInfoBuilderTests { @Test public void testCreatingRuntimeEnvironmentInfo() { RuntimeEnvironmentInfo rei = new RuntimeEnvironmentInfo.Builder() .spiClass(AppDeployer.class) .implementationName("TestDeployer") .implementationVersion("1.0.0") .platformClientVersion("1.2.0") .platformHostVersion("1.1.0") .platformType("Test") .platformApiVersion("1") .addPlatformSpecificInfo("foo", "bar") .build(); assertThat(rei.getSpiVersion()).isEqualTo(RuntimeVersionUtils.getVersion(AppDeployer.class)); assertThat(rei.getImplementationName()).isEqualTo("TestDeployer"); assertThat(rei.getImplementationVersion()).isEqualTo("1.0.0"); assertThat(rei.getPlatformType()).isEqualTo("Test"); assertThat(rei.getPlatformApiVersion()).isEqualTo("1"); assertThat(rei.getPlatformClientVersion()).isEqualTo("1.2.0"); assertThat(rei.getPlatformHostVersion()).isEqualTo("1.1.0"); assertThat(rei.getJavaVersion()).isEqualTo(System.getProperty("java.version")); assertThat(rei.getSpringVersion()).isEqualTo(SpringVersion.getVersion()); assertThat(rei.getSpringBootVersion()).isEqualTo(RuntimeVersionUtils.getSpringBootVersion()); assertThat(rei.getPlatformSpecificInfo().get("foo")).isEqualTo("bar"); } } ================================================ FILE: spring-cloud-deployer-spi/src/test/java/org/springframework/cloud/deployer/spi/task/TaskLauncherTests.java ================================================ /* * Copyright 2019 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.cloud.deployer.spi.task; import org.junit.jupiter.api.Test; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; import static org.assertj.core.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.assertEquals; /** * Tests for {@link TaskLauncher} */ public class TaskLauncherTests { @Test public void testTaskLauncherDefaultMethods() { TaskLauncher taskLauncher = new TaskLauncher() { @Override public String launch(AppDeploymentRequest request) { return null; } @Override public void cancel(String id) { } @Override public TaskStatus status(String id) { return null; } @Override public void cleanup(String id) { } @Override public void destroy(String appName) { } @Override public RuntimeEnvironmentInfo environmentInfo() { return null; } }; try { taskLauncher.getLog("test"); fail(""); } catch (UnsupportedOperationException e) { assertEquals(e.getMessage(), "'getLog' is not implemented."); } try { taskLauncher.getRunningTaskExecutionCount(); fail(""); } catch (UnsupportedOperationException e) { assertEquals(e.getMessage(), "'getRunningTaskExecutionCount' is not implemented."); } try { taskLauncher.getMaximumConcurrentTasks(); fail(""); } catch (UnsupportedOperationException e) { assertEquals(e.getMessage(), "'getMaximumConcurrentTasks' is not implemented."); } } } ================================================ FILE: spring-cloud-deployer-spi/src/test/java/org/springframework/cloud/deployer/spi/util/ByteSizeUtilsTests.java ================================================ /* * Copyright 2016 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.cloud.deployer.spi.util; import org.junit.jupiter.api.Test; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; /** * Unit tests for {@link ByteSizeUtils}. * * @author Eric Bottard */ public class ByteSizeUtilsTests { @Test public void testParse() { assertThat(ByteSizeUtils.parseToMebibytes("1")).isEqualTo(1L); assertThat(ByteSizeUtils.parseToMebibytes("2m")).isEqualTo(2L); assertThat(ByteSizeUtils.parseToMebibytes("20M")).isEqualTo(20L); assertThat(ByteSizeUtils.parseToMebibytes("1000g")).isEqualTo(1024_000L); assertThat(ByteSizeUtils.parseToMebibytes("1G")).isEqualTo(1024L); } @Test public void testNotANumber() { assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> ByteSizeUtils.parseToMebibytes("wat?124")); } @Test public void testUnsupportedUnit() { assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> ByteSizeUtils.parseToMebibytes("1PB")); } } ================================================ FILE: spring-cloud-deployer-spi/src/test/java/org/springframework/cloud/deployer/spi/util/CommandLineTokenizerTests.java ================================================ /* * Copyright 2016 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.cloud.deployer.spi.util; import java.util.Arrays; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; /** * Unit tests for {@link CommandLineTokenizer}. * * @author Eric Bottard */ public class CommandLineTokenizerTests { @Test public void testSimple() { CommandLineTokenizer tokenizer = new CommandLineTokenizer("a b cdef"); Assertions.assertEquals(Arrays.asList("a", "b", "cdef"), tokenizer.getArgs()); tokenizer = new CommandLineTokenizer(" a b cdef "); Assertions.assertEquals(Arrays.asList("a", "b", "cdef"), tokenizer.getArgs()); } @Test public void testQuotes() { CommandLineTokenizer tokenizer = new CommandLineTokenizer(" 'a b' cdef gh \"i j\""); Assertions.assertEquals(Arrays.asList("a b", "cdef", "gh", "i j"), tokenizer.getArgs()); } @Test public void testEscapes() { CommandLineTokenizer tokenizer = new CommandLineTokenizer(" 'a \\' \\\" b' cdef gh \"i \\\"j\""); Assertions.assertEquals(Arrays.asList("a ' \\\" b", "cdef", "gh", "i \"j"), tokenizer.getArgs()); } @Test public void testUnbalancedQuotes() { assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> { CommandLineTokenizer tokenizer = new CommandLineTokenizer(" 'ab cd' 'ef gh"); }); } } ================================================ FILE: spring-cloud-deployer-spi-scheduler-test-app/Dockerfile ================================================ FROM java:8-alpine ARG JAR_FILE ADD target/${JAR_FILE} spring-cloud-deployer-spi-scheduler-test-app.jar ENTRYPOINT ["java", "-jar", "/spring-cloud-deployer-spi-scheduler-test-app.jar"] ================================================ FILE: spring-cloud-deployer-spi-scheduler-test-app/pom.xml ================================================ 4.0.0 org.springframework.cloud spring-cloud-deployer-parent 3.0.0-SNAPSHOT .. spring-cloud-deployer-spi-scheduler-test-app org.springframework.cloud.deployer.spi.scheduler.test.app.SchedulerIntegrationTestApplication 17 springcloud 1.3.6 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-configuration-processor true org.springframework.boot spring-boot-maven-plugin ${spring-boot.version} repackage exec com.spotify dockerfile-maven-plugin ${dockerfile-maven-plugin.version} ${docker.image.prefix}/${project.artifactId} ${project.version} ${project.build.finalName}-exec.jar ================================================ FILE: spring-cloud-deployer-spi-scheduler-test-app/src/main/java/org/springframework/cloud/deployer/spi/scheduler/test/app/SchedulerIntegrationTest.java ================================================ /* * Copyright 2018-2019 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.cloud.deployer.spi.scheduler.test.app; import jakarta.annotation.PostConstruct; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; import org.springframework.util.Assert; /** * An app that can misbehave, useful for integration testing of app deployers. * * @author Glenn Renfro * @author Ilayaperumal Gopinathan */ @EnableConfigurationProperties(SchedulerIntegrationTestProperties.class) @Configuration public class SchedulerIntegrationTest { protected final Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private SchedulerIntegrationTestProperties properties; @PostConstruct public void init() throws InterruptedException { String parameterThatMayNeedEscaping = properties.getParameterThatMayNeedEscaping(); if (parameterThatMayNeedEscaping != null && !SchedulerIntegrationTestProperties.FUNNY_CHARACTERS.equals(parameterThatMayNeedEscaping)) { throw new IllegalArgumentException(String.format("Expected 'parameterThatMayNeedEscaping' value to be equal to '%s', but was '%s'", SchedulerIntegrationTestProperties.FUNNY_CHARACTERS, parameterThatMayNeedEscaping)); } String commandLineArgValueThatMayNeedEscaping = properties.getCommandLineArgValueThatMayNeedEscaping(); if (commandLineArgValueThatMayNeedEscaping != null && !SchedulerIntegrationTestProperties.FUNNY_CHARACTERS.equals(commandLineArgValueThatMayNeedEscaping)) { throw new IllegalArgumentException(String.format("Expected 'commandLineArgValueThatMayNeedEscaping' value to be equal to '%s', but was '%s'", SchedulerIntegrationTestProperties.FUNNY_CHARACTERS, commandLineArgValueThatMayNeedEscaping)); } Assert.notNull(properties.getInstanceIndex(), "instanceIndex should have been set by deployer or runtime"); if (properties.getMatchInstances().isEmpty() || properties.getMatchInstances().contains(properties.getInstanceIndex())) { logger.info("Waiting for %dms before allowing further initialization and actuator startup...", properties.getInitDelay()); Thread.sleep(properties.getInitDelay()); logger.info("... done"); if (properties.getKillDelay() >= 0) { logger.info("Will kill this process in %dms%n", properties.getKillDelay()); new Thread() { @Override public void run() { try { Thread.sleep(properties.getKillDelay()); System.exit(properties.getExitCode()); } catch (InterruptedException e) { throw new RuntimeException(e); } } }.start(); } } } } ================================================ FILE: spring-cloud-deployer-spi-scheduler-test-app/src/main/java/org/springframework/cloud/deployer/spi/scheduler/test/app/SchedulerIntegrationTestApplication.java ================================================ /* * Copyright 2018-2019 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.cloud.deployer.spi.scheduler.test.app; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * Main application runner. Will keep the main method running, to simulate a long running process. * * @author Glenn Renfro * @author Ilayaperumal Gopinathan */ @SpringBootApplication public class SchedulerIntegrationTestApplication { public static void main(String[] args) { SpringApplication.run(SchedulerIntegrationTestApplication.class, args); } } ================================================ FILE: spring-cloud-deployer-spi-scheduler-test-app/src/main/java/org/springframework/cloud/deployer/spi/scheduler/test/app/SchedulerIntegrationTestProperties.java ================================================ /* * Copyright 2018-2019 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.cloud.deployer.spi.scheduler.test.app; import java.util.HashSet; import java.util.Set; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; /** * Configuration properties for the IntegrationTestProcessor app. * * @author Glenn Renfro * @author Ilayaperumal Gopinathan */ @ConfigurationProperties public class SchedulerIntegrationTestProperties { public static final String FUNNY_CHARACTERS = "&'\"|< é\\("; /** * The delay in milliseconds to stall the initialization of this app. * Useful for testing the 'deploying' state of a app. */ private int initDelay = 0; /** * The delay in milliseconds after which this app will kill itself. *

-1 means don't kill

*/ private int killDelay = -1; /** * The exit code used when this app will kill itself. *

Set this to 0 for normal exit

*/ private int exitCode = 1; /** * If not empty, only the app intances whose number(s) are contained in this set * will behave according to the other configuration parameters. */ private Set matchInstances = new HashSet<>(); /** * If not null, this property will be tested against {@link #FUNNY_CHARACTERS}. * This makes sure that a deployer knows how to properly propagate application properties, including * those that contain chars that often require some form of escaping. */ private String parameterThatMayNeedEscaping; /** * If not null, this property will be tested against {@link #FUNNY_CHARACTERS}. * This makes sure that a deployer knows how to properly propagate deployment properties, including * those that contain chars that often require some form of escaping. */ private String commandLineArgValueThatMayNeedEscaping; @Value("${INSTANCE_INDEX:${CF_INSTANCE_INDEX:0}}") private Integer instanceIndex; public int getInitDelay() { return initDelay; } public void setInitDelay(int initDelay) { this.initDelay = initDelay; } public int getKillDelay() { return killDelay; } public void setKillDelay(int killDelay) { this.killDelay = killDelay; } public int getExitCode() { return exitCode; } public void setExitCode(int exitCode) { this.exitCode = exitCode; } public Set getMatchInstances() { return matchInstances; } public void setMatchInstances(Set matchInstances) { this.matchInstances = matchInstances; } public String getParameterThatMayNeedEscaping() { return parameterThatMayNeedEscaping; } public void setParameterThatMayNeedEscaping(String parameterThatMayNeedEscaping) { this.parameterThatMayNeedEscaping = parameterThatMayNeedEscaping; } public Integer getInstanceIndex() { return instanceIndex; } public void setInstanceIndex(Integer instanceIndex) { this.instanceIndex = instanceIndex; } public String getCommandLineArgValueThatMayNeedEscaping() { return commandLineArgValueThatMayNeedEscaping; } public void setCommandLineArgValueThatMayNeedEscaping(String commandLineArgValueThatMayNeedEscaping) { this.commandLineArgValueThatMayNeedEscaping = commandLineArgValueThatMayNeedEscaping; } } ================================================ FILE: spring-cloud-deployer-spi-scheduler-test-app/src/main/resources/application.properties ================================================ management.endpoint.shutdown.enabled=true management.endpoints.web.exposure.include=* ================================================ FILE: spring-cloud-deployer-spi-test/pom.xml ================================================ 4.0.0 spring-cloud-deployer-spi-test jar spring-cloud-deployer-spi-test Base Tests for Spring Cloud Deployer SPI Implementations org.springframework.cloud spring-cloud-deployer-parent 3.0.0-SNAPSHOT .. ${project.version} src/main/resources true integration-test-app.properties org.springframework.cloud spring-cloud-deployer-spi org.springframework.cloud spring-cloud-deployer-resource-maven org.springframework.cloud spring-cloud-deployer-spi-test-app 3.0.0-SNAPSHOT org.springframework.boot spring-boot-starter-test compile org.awaitility awaitility org.assertj assertj-core ================================================ FILE: spring-cloud-deployer-spi-test/src/main/java/org/springframework/cloud/deployer/spi/scheduler/test/AbstractSchedulerIntegrationJUnit5Tests.java ================================================ /* * Copyright 2018-2021 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.cloud.deployer.spi.scheduler.test; import java.io.IOException; import java.lang.reflect.Method; import java.time.Duration; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Properties; import java.util.UUID; import org.assertj.core.api.AbstractAssert; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.extension.ExtendWith; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.cloud.deployer.resource.maven.MavenProperties; import org.springframework.cloud.deployer.resource.maven.MavenResource; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.scheduler.CreateScheduleException; import org.springframework.cloud.deployer.spi.scheduler.ScheduleInfo; import org.springframework.cloud.deployer.spi.scheduler.ScheduleRequest; import org.springframework.cloud.deployer.spi.scheduler.Scheduler; import org.springframework.cloud.deployer.spi.scheduler.SchedulerException; import org.springframework.cloud.deployer.spi.scheduler.SchedulerPropertyKeys; import org.springframework.cloud.deployer.spi.test.Timeout; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.awaitility.Awaitility.await; /** * Contains base set of tests that are required for each implementation of * Spring Cloud Scheduler to pass. * * @author Glenn Renfro * @author Ilayaperumal Gopinathan * @author Corneil du Plessis */ @ExtendWith(SpringExtension.class) @SpringBootTest(webEnvironment = WebEnvironment.NONE) @ContextConfiguration(classes = AbstractSchedulerIntegrationJUnit5Tests.Config.class) public abstract class AbstractSchedulerIntegrationJUnit5Tests { @Autowired protected MavenProperties mavenProperties; private SchedulerWrapper schedulerWrapper; /** * Return the timeout to use for repeatedly querying that a task has been scheduled. * Default value is one minute, being queried every 5 seconds. */ private Timeout scheduleTimeout = new Timeout(30, 5000); /** * Return the timeout to use for repeatedly querying whether a task has been unscheduled. * Default value is one minute, being queried every 5 seconds. */ private Timeout unScheduleTimeout = new Timeout(12, 5000); protected final Logger log = LoggerFactory.getLogger(this.getClass()); private String testName; @AfterEach public void tearDown() { List scheduleRequests = new ArrayList<>(schedulerWrapper.getScheduledTasks().values()); for (ScheduleRequest scheduleRequest : scheduleRequests) { unscheduleTestSchedule(scheduleRequest.getScheduleName()); } } /** * Subclasses should call this method to interact with the Scheduler under test. * Returns a wrapper around the scheduler returned by {@link #provideScheduler()}, that keeps * track of which tasks have been scheduled and unscheduled. */ protected Scheduler taskScheduler() { return this.schedulerWrapper; } /** * To be implemented by subclasses, which should return the instance of Scheduler that needs * to be tested. Can be used if subclasses decide to add additional implementation-specific tests. * @return the scheduler */ protected abstract Scheduler provideScheduler(); /** * To be implemented by subclasses, which should return the commandLineArgs that * will be used for the tests. * @return the command line arguments */ protected abstract List getCommandLineArgs(); /** * To be implemented by subclasses, which should return the schedulerProperties that * will be used for the tests. * @return the scheduler properties */ protected abstract Map getSchedulerProperties(); /** * To be implemented by subclasses, which should return the deploymentProperties that * will be used for the tests. * @return the deployment properties */ protected abstract Map getDeploymentProperties(); /** * To be implemented by subclasses, which should return the appProperties that * will be used for the tests. * @return the application properties */ protected abstract Map getAppProperties(); @BeforeEach public void wrapScheduler(TestInfo testInfo) { this.schedulerWrapper = new SchedulerWrapper(provideScheduler()); Optional testMethod = testInfo.getTestMethod(); if (testMethod.isPresent()) { this.testName = testMethod.get().getName(); } } @Test public void testSimpleSchedule() { createAndVerifySchedule(); } @Test public void testUnschedule() { int initialSize = taskScheduler().list().size(); ScheduleInfo scheduleInfo = createAndVerifySchedule(); unscheduleTestSchedule(scheduleInfo.getScheduleName()); assertThat(taskScheduler().list().size() - initialSize).isEqualTo(0); } @Test public void testDuplicateSchedule() { ScheduleRequest request = createScheduleRequest(); taskScheduler().schedule(request); ScheduleInfo scheduleInfo = new ScheduleInfo(); scheduleInfo.setScheduleName(request.getScheduleName()); verifySchedule(scheduleInfo); assertThatThrownBy(() -> { taskScheduler().schedule(request); }).isInstanceOf(CreateScheduleException.class).hasMessageContaining("Failed to create schedule %s", request.getScheduleName()); } @Test public void testUnScheduleNoEntry() { String definitionName = randomName(); String scheduleName = scheduleName() + definitionName; assertThatThrownBy(() -> { unscheduleTestSchedule(scheduleName); }).isInstanceOf(SchedulerException.class).hasMessage("Failed to unschedule %s", scheduleName); } @Test public void testInvalidCronExpression() { final String INVALID_EXPRESSION = "BAD"; String definitionName = randomName(); String scheduleName = scheduleName() + definitionName; Map properties = new HashMap<>(getDeploymentProperties()); properties.put(SchedulerPropertyKeys.CRON_EXPRESSION, INVALID_EXPRESSION); AppDefinition definition = new AppDefinition(definitionName, properties); ScheduleRequest request = new ScheduleRequest(definition, properties, getCommandLineArgs(), scheduleName, testApplication()); assertThatThrownBy(() -> { taskScheduler().schedule(request); }).isInstanceOf(CreateScheduleException.class); } @Test public void testMultipleSchedule() { String definitionName = randomName(); String scheduleName = scheduleName() + definitionName; for (int i = 0; i < 4; i++) { ScheduleRequest request = createScheduleRequest(scheduleName + i, definitionName + i); taskScheduler().schedule(request); } List scheduleInfos = taskScheduler().list(); for (ScheduleInfo scheduleInfo : scheduleInfos) { verifySchedule(scheduleInfo); } } @Test public void testListFilter() { String definitionName = randomName(); String scheduleName = scheduleName() + definitionName; for (int i = 0; i < 4; i++) { ScheduleRequest request = createScheduleRequest(scheduleName + i, definitionName + i%2); taskScheduler().schedule(request); } ScheduleInfo scheduleInfo = new ScheduleInfo(); scheduleInfo.setScheduleName(scheduleName + 0); scheduleInfo.setTaskDefinitionName(definitionName + 0); await().pollInterval(Duration.ofMillis(this.scheduleTimeout.pause)) .atMost(Duration.ofMillis(this.scheduleTimeout.totalTime)) .untilAsserted(() -> { ListScheduleInfoAssert.assertThat(taskScheduler().list(definitionName + 0)) .hasExpectedScheduleCount(definitionName + 0, 2); }); } public Timeout getScheduleTimeout() { return scheduleTimeout; } public void setScheduleTimeout(Timeout scheduleTimeout) { this.scheduleTimeout = scheduleTimeout; } public Timeout getUnScheduleTimeout() { return unScheduleTimeout; } public void setUnScheduleTimeout(Timeout unScheduleTimeout) { this.unScheduleTimeout = unScheduleTimeout; } private ScheduleInfo createAndVerifySchedule() { ScheduleRequest request = createScheduleRequest(); taskScheduler().schedule(request); ScheduleInfo scheduleInfo = new ScheduleInfo(); scheduleInfo.setScheduleName(request.getScheduleName()); verifySchedule(scheduleInfo); return scheduleInfo; } private ScheduleRequest createScheduleRequest() { String definitionName = randomName(); String scheduleName = scheduleName() + definitionName; return createScheduleRequest(scheduleName, definitionName); } private ScheduleRequest createScheduleRequest(String scheduleName, String definitionName) { AppDefinition definition = new AppDefinition(definitionName, getAppProperties()); return new ScheduleRequest(definition, getDeploymentProperties(), getCommandLineArgs(), scheduleName, testApplication()); } private void verifySchedule(ScheduleInfo scheduleInfo) { await().pollInterval(Duration.ofMillis(this.scheduleTimeout.pause)) .atMost(Duration.ofMillis(this.scheduleTimeout.totalTime)) .untilAsserted(() -> { ListScheduleInfoAssert.assertThat(taskScheduler().list()).hasSchedule(scheduleInfo.getScheduleName()); }); } private void unscheduleTestSchedule(String scheduleName) { log.info("unscheduling {}...", scheduleName); taskScheduler().unschedule(scheduleName); ScheduleInfo scheduleInfo = new ScheduleInfo(); scheduleInfo.setScheduleName(scheduleName); await().pollInterval(Duration.ofMillis(this.unScheduleTimeout.pause)) .atMost(Duration.ofMillis(this.unScheduleTimeout.totalTime)) .untilAsserted(() -> { ListScheduleInfoAssert.assertThat(taskScheduler().list()).hasNotSchedule(scheduleName); }); } protected String randomName() { return this.testName + "-" + UUID.randomUUID(); } protected String scheduleName() { return "ScheduleName_"; } protected static class ListScheduleInfoAssert extends AbstractAssert> { public ListScheduleInfoAssert(List scheduleInfos) { super(scheduleInfos, ListScheduleInfoAssert.class); } public static ListScheduleInfoAssert assertThat(List scheduleInfos) { return new ListScheduleInfoAssert(scheduleInfos); } public ListScheduleInfoAssert hasSchedule(String scheduleName) { isNotNull(); if (!actual.stream().map(si -> si.getScheduleName()).anyMatch(sc -> sc.equals(scheduleName))) { failWithMessage("unable to find specified scheduleName <%s> ", scheduleName); } return this; } public ListScheduleInfoAssert hasNotSchedule(String scheduleName) { isNotNull(); if (actual.stream().map(si -> si.getScheduleName()).anyMatch(sc -> sc.equals(scheduleName))) { failWithMessage("found specified scheduleName <%s> ", scheduleName); } return this; } public ListScheduleInfoAssert hasExpectedScheduleCount(String taskDefinitionName, int expectedScheduleCount) { this.isNotNull(); if (actual.size() != expectedScheduleCount) { failWithMessage("given schedule info list doesn't match expected count <%s>, was <%s>", expectedScheduleCount, actual.size()); } if (!actual.stream().map(si -> si.getTaskDefinitionName()).anyMatch(sc -> sc.equals(taskDefinitionName))) { failWithMessage("found specified scheduleName <%s> ", taskDefinitionName); } return this; } } /** * Return a resource corresponding to the spring-cloud-deployer-spi-scheduler-test-app app suitable for the target runtime. * * The default implementation returns an uber-jar fetched via Maven. Subclasses may override. * @return the resource of the test application. */ protected Resource testApplication() { Properties properties = new Properties(); try { properties.load(new ClassPathResource("integration-test-app.properties").getInputStream()); } catch (IOException e) { throw new RuntimeException("Failed to determine which version of spring-cloud-deployer-spi-scheduler-test-app to use", e); } return new MavenResource.Builder(mavenProperties) .groupId("org.springframework.cloud") .artifactId("spring-cloud-deployer-spi-scheduler-test-app") .classifier("exec") .version(properties.getProperty("version")) .extension("jar") .build(); } /** * A decorator for Scheduler that keeps track of scheduled/unscheduled tasks. * * @author Glenn Renfro */ protected static class SchedulerWrapper implements Scheduler { private final Scheduler wrapped; private final Map scheduledTasks = new HashMap<>(); public SchedulerWrapper(Scheduler wrapped) { this.wrapped = wrapped; } @Override public void schedule(ScheduleRequest scheduleRequest) { wrapped.schedule(scheduleRequest); scheduledTasks.put(scheduleRequest.getScheduleName(), scheduleRequest); } @Override public void unschedule(String scheduleName) { wrapped.unschedule(scheduleName); scheduledTasks.remove(scheduleName); } @Override public List list(String taskDefinitionName) { return wrapped.list(taskDefinitionName); } @Override public List list() { return wrapped.list(); } public Map getScheduledTasks() { return Collections.unmodifiableMap(scheduledTasks); } } @Configuration public static class Config { @Bean @ConfigurationProperties("maven") public MavenProperties mavenProperties() { return new MavenProperties(); } } } ================================================ FILE: spring-cloud-deployer-spi-test/src/main/java/org/springframework/cloud/deployer/spi/test/AbstractAppDeployerIntegrationJUnit5Tests.java ================================================ /* * Copyright 2016-2021 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.cloud.deployer.spi.test; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.cloud.deployer.spi.app.AppDeployer; import org.springframework.cloud.deployer.spi.app.AppInstanceStatus; import org.springframework.cloud.deployer.spi.app.AppScaleRequest; import org.springframework.cloud.deployer.spi.app.AppStatus; import org.springframework.cloud.deployer.spi.app.DeploymentState; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; import org.springframework.cloud.deployer.spi.test.app.DeployerIntegrationTestProperties; import org.springframework.core.io.Resource; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.awaitility.Awaitility.await; /** * Abstract base class for integration tests of * {@link org.springframework.cloud.deployer.spi.app.AppDeployer} * implementations. *

* Inheritors should setup an environment with a newly created * {@link org.springframework.cloud.deployer.spi.app.AppDeployer}. * Tests in this class are independent and leave the * deployer in a clean state after they successfully run. *

*

* As deploying an application is often quite time consuming, some tests assert * various aspects of deployment in a row, to avoid re-deploying apps over and * over again. *

* * @author Eric Bottard * @author Mark Fisher * @author Greg Turnquist * @author David Turanski * @author Corneil du Plessis */ public abstract class AbstractAppDeployerIntegrationJUnit5Tests extends AbstractIntegrationJUnit5Tests { final static int DESIRED_COUNT = 3; private AppDeployerWrapper deployerWrapper; /** * To be implemented by subclasses, which should return the instance of AppDeployer that needs * to be tested. If subclasses decide to add additional implementation-specific tests, they should * interact with the deployer through {@link #appDeployer()}, and not directly via a field or a call * to this method. * @return the app deployer */ protected abstract AppDeployer provideAppDeployer(); /** * Subclasses should call this method to interact with the AppDeployer under test. * Returns a wrapper around the deployer returned by {@link #provideAppDeployer()}, that keeps * track of which apps have been deployed and undeployed. * @return the app deployer */ protected AppDeployer appDeployer() { return deployerWrapper; } @BeforeEach public void wrapDeployer() { deployerWrapper = new AppDeployerWrapper(provideAppDeployer()); } @AfterEach public void cleanupLingeringApps() { for (String id : deployerWrapper.deployments) { try { log.warn("Test named {} left behind an app for deploymentId '{}', trying to cleanup", this.testName, id); deployerWrapper.wrapped.undeploy(id); } catch (Exception e) { log.warn("Exception caught while trying to cleanup '{}'. Moving on...", id); } } } @Test public void testUnknownDeployment() { String unknownId = randomName(); AppStatus status = appDeployer().status(unknownId); assertThat(status.getDeploymentId()).isEqualTo(unknownId); assertThat(status.getInstances()).as("The map was not empty: " + status.getInstances()).isEmpty(); assertThat(status.getState()).isEqualTo(DeploymentState.unknown); } /** * Tests a simple deploy-undeploy cycle. */ @Test public void testSimpleDeployment() { AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); log.info("Deploying {}...", request.getDefinition().getName()); AppDeployer appDeployer = appDeployer(); String deploymentId = appDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await() .pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer.status(deploymentId).getState()).isEqualTo(DeploymentState.deployed) ); log.info("Deploying {} again...", request.getDefinition().getName()); assertThatThrownBy(() -> appDeployer.deploy(request) ).isInstanceOf(IllegalStateException.class); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer.status(deploymentId).getState()).isEqualTo(DeploymentState.unknown) ); assertThatThrownBy(() -> appDeployer.undeploy(deploymentId) ).isInstanceOf(IllegalStateException.class); } /** * An app deployer should be able to re-deploy an application after it has been un-deployed. * This test makes sure the deployer does not leave things lying around for example. */ @Test public void testRedeploy() { AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); log.info("Deploying {}...", request.getDefinition().getName()); AppDeployer appDeployer = appDeployer(); String deploymentId = appDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await() .pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer.status(deploymentId).getState()).isEqualTo(DeploymentState.deployed) ); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer.undeploy(deploymentId); await() .pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer.status(deploymentId).getState()).isEqualTo(DeploymentState.unknown) ); // Optionally pause before re-using request try { Thread.sleep(redeploymentPause()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } log.info("Deploying {} again...", request.getDefinition().getName()); // Attempt re-deploy of SAME request String deploymentId2 = appDeployer.deploy(request); timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer.status(deploymentId2).getState()).isEqualTo(DeploymentState.deployed) ); log.info("Undeploying {}...", deploymentId2); timeout = undeploymentTimeout(); appDeployer.undeploy(deploymentId2); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer.status(deploymentId2).getState()).isEqualTo(DeploymentState.unknown) ); } /** * Tests that an app which takes a long time to deploy is correctly reported as deploying. * Test that such an app can be undeployed. */ @Test public void testDeployingStateCalculationAndCancel() { Map properties = new HashMap<>(); properties.put("initDelay", "" + 1000 * 60 * 60); // 1hr AppDefinition definition = new AppDefinition(randomName(), properties); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, properties); log.info("Deploying {}...", request.getDefinition().getName()); AppDeployer appDeployer = appDeployer(); String deploymentId = appDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer.status(deploymentId).getState()).isEqualTo(DeploymentState.deploying) ); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer.status(deploymentId).getState()).isEqualTo(DeploymentState.unknown) ); } @Test public void testFailedDeployment() { Map properties = new HashMap<>(); properties.put("killDelay", "0"); AppDefinition definition = new AppDefinition(randomName(), properties); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, properties); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = appDeployer().deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.failed) ); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer().undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown) ); } /** * Tests that properties (key-value mappings) can be passed to a deployed app, * including values that typically require special handling. */ @Test public void testApplicationPropertiesPassing() { Map properties = new HashMap<>(); properties.put("parameterThatMayNeedEscaping", DeployerIntegrationTestProperties.FUNNY_CHARACTERS); AppDefinition definition = new AppDefinition(randomName(), properties); Map deploymentProperties = new HashMap<>(); // This makes sure that deploymentProperties are not passed to the deployed app itself deploymentProperties.put("killDelay", "0"); AppDeploymentRequest request = new AppDeploymentRequest(definition, testApplication(), deploymentProperties); log.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = appDeployer().deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed) ); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer().undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown) ); // This second pass makes sure that properties are indeed passed properties.put("parameterThatMayNeedEscaping", "notWhatIsExpected"); definition = new AppDefinition(randomName(), properties); request = new AppDeploymentRequest(definition, testApplication(), deploymentProperties); log.info("Deploying {}, expecting it to fail...", request.getDefinition().getName()); String deploymentId2 = appDeployer().deploy(request); timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId2).getState()).isEqualTo(DeploymentState.failed) ); log.info("Undeploying {}...", deploymentId2); timeout = undeploymentTimeout(); appDeployer().undeploy(deploymentId2); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId2).getState()).isEqualTo(DeploymentState.unknown) ); } /** * Tests that command line arguments (ordered strings) can be passed to a deployed app, * including values that typically require special handling. */ @Test public void testCommandLineArgumentsPassing() { Map properties = new HashMap<>(); AppDefinition definition = new AppDefinition(randomName(), properties); Map deploymentProperties = new HashMap<>(); List cmdLineArgs = Collections.singletonList("--commandLineArgValueThatMayNeedEscaping=" + DeployerIntegrationTestProperties.FUNNY_CHARACTERS); AppDeploymentRequest request = new AppDeploymentRequest(definition, testApplication(), deploymentProperties, cmdLineArgs); log.info("Deploying {}...", request.getDefinition().getName()); AppDeployer appDeployer = appDeployer(); String deploymentId = appDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer.status(deploymentId).getState()).isEqualTo(DeploymentState.deployed) ); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer.status(deploymentId).getState()).isEqualTo(DeploymentState.unknown) ); // This second pass makes sure that commandLine args are indeed understood properties = new HashMap<>(); definition = new AppDefinition(randomName(), properties); deploymentProperties = new HashMap<>(); cmdLineArgs = Collections.singletonList("--commandLineArgValueThatMayNeedEscaping=notWhatIsExpected"); request = new AppDeploymentRequest(definition, testApplication(), deploymentProperties, cmdLineArgs); log.info("Deploying {}, expecting it to fail...", request.getDefinition().getName()); String deploymentId2 = appDeployer.deploy(request); timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer.status(deploymentId2).getState()).isEqualTo(DeploymentState.failed) ); log.info("Undeploying {}...", deploymentId2); timeout = undeploymentTimeout(); appDeployer.undeploy(deploymentId2); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer.status(deploymentId2).getState()).isEqualTo(DeploymentState.unknown) ); } /** * Tests support for instance count support and individual instance status report. */ @Test public void testMultipleInstancesDeploymentAndPartialState() { Map appProperties = new HashMap<>(); appProperties.put("matchInstances", "1"); // Only instance n°1 will kill itself appProperties.put("killDelay", "0"); AppDefinition definition = new AppDefinition(randomName(), appProperties); Resource resource = testApplication(); Map deploymentProperties = new HashMap<>(); deploymentProperties.put(AppDeployer.COUNT_PROPERTY_KEY, "3"); deploymentProperties.put(AppDeployer.INDEXED_PROPERTY_KEY, "true"); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, deploymentProperties); log.info("Deploying {}...", request.getDefinition().getName()); AppDeployer appDeployer = appDeployer(); String deploymentId = appDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer.status(deploymentId).getState()).isEqualTo(DeploymentState.partial) ); // Assert individual instance state // Note we can't rely on instances order, neither on their id indicating their ordinal number List individualStates = new ArrayList<>(); for (AppInstanceStatus status : appDeployer.status(deploymentId).getInstances().values()) { individualStates.add(status.getState()); } assertThat(individualStates).containsExactlyInAnyOrder(DeploymentState.deployed, DeploymentState.deployed, DeploymentState.failed); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer.status(deploymentId).getState()).isEqualTo(DeploymentState.unknown) ); } /** * Tests support for DeployerEnvironmentInfo is implemented. */ @Test public void testEnvironmentInfo() { RuntimeEnvironmentInfo info = appDeployer().environmentInfo(); assertThat(info.getImplementationVersion()).isNotNull(); assertThat(info.getPlatformType()).isNotNull(); assertThat(info.getPlatformClientVersion()).isNotNull(); assertThat(info.getPlatformHostVersion()).isNotNull(); } @Test @Disabled("Disabled pending the implementation of this feature.") public void testScale() { doTestScale(false); } @Test @Disabled("Disabled pending the implementation of this feature.") public void testScaleWithIndex() { doTestScale(true); } protected void doTestScale(Boolean indexed) { Map deploymentProperties = Collections.singletonMap(AppDeployer.INDEXED_PROPERTY_KEY, indexed.toString()); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, deploymentProperties); log.info("Deploying {} index={}...", request.getDefinition().getName(), indexed); AppDeployer appDeployer = appDeployer(); String deploymentId = appDeployer.deploy(request); Timeout timeout = deploymentTimeout(); log.info("DeploymentTimeout:{}", timeout); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer.status(deploymentId).getState()).isEqualTo(DeploymentState.deployed) ); assertThat(appDeployer.status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); log.info("Scaling {} to {} instances...", request.getDefinition().getName(), DESIRED_COUNT); appDeployer.scale(new AppScaleRequest(deploymentId, DESIRED_COUNT)); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { DeploymentState state = appDeployer.status(deploymentId).getState(); log.info("Awaiting deployed. State={}", state); assertThat(state).isEqualTo(DeploymentState.deployed); }); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { Map instances = appDeployer.status(deploymentId).getInstances(); log.info("Awaiting {} instances. Instances={}", DESIRED_COUNT, instances.size()); assertThat(instances).hasSize(DESIRED_COUNT); }); List individualStates = new ArrayList<>(); for (AppInstanceStatus status : appDeployer.status(deploymentId).getInstances().values()) { individualStates.add(status.getState()); } assertThat(individualStates).allMatch(is -> is == DeploymentState.deployed); log.info("Scaling {} from {} to 1 instance...", request.getDefinition().getName(), DESIRED_COUNT); appDeployer.scale(new AppScaleRequest(deploymentId, 1)); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { DeploymentState state = appDeployer.status(deploymentId).getState(); log.info("Awaiting deployed. State={}", state); assertThat(state).isEqualTo(DeploymentState.deployed); }); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer.status(deploymentId).getInstances()).hasSize(1) ); log.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { DeploymentState state = appDeployer.status(deploymentId).getState(); log.info("Awaiting unknown. State={}", state); assertThat(state).isEqualTo(DeploymentState.unknown); }); } /** * A decorator for AppDeployer that keeps track of deployed/undeployed apps. * * @author Eric Bottard */ protected static class AppDeployerWrapper implements AppDeployer { private final AppDeployer wrapped; private final Set deployments = new LinkedHashSet<>(); public AppDeployerWrapper(AppDeployer wrapped) { this.wrapped = wrapped; } @Override public String deploy(AppDeploymentRequest request) { String deploymentId = wrapped.deploy(request); deployments.add(deploymentId); return deploymentId; } @Override public void undeploy(String id) { wrapped.undeploy(id); deployments.remove(id); } @Override public AppStatus status(String id) { return wrapped.status(id); } @Override public RuntimeEnvironmentInfo environmentInfo() { return wrapped.environmentInfo(); } @Override public String getLog(String id) { return wrapped.getLog(id); } @Override public void scale(AppScaleRequest appScaleRequest) { wrapped.scale(appScaleRequest); } } } ================================================ FILE: spring-cloud-deployer-spi-test/src/main/java/org/springframework/cloud/deployer/spi/test/AbstractIntegrationJUnit5Tests.java ================================================ /* * Copyright 2016-2021 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.cloud.deployer.spi.test; import java.io.IOException; import java.lang.reflect.Method; import java.util.Optional; import java.util.Properties; import java.util.UUID; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.extension.ExtendWith; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.cloud.deployer.resource.maven.MavenProperties; import org.springframework.cloud.deployer.resource.maven.MavenResource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; /** * Abstract base class containing infrastructure for the TCK, common to both * {@link org.springframework.cloud.deployer.spi.app.AppDeployer} and * {@link org.springframework.cloud.deployer.spi.task.TaskLauncher} tests. * *

Subclasses should explicitly declare additional config that should be used via the use of * {@link ContextConfiguration}.

* * @author Eric Bottard */ @ExtendWith(SpringExtension.class) @SpringBootTest(webEnvironment= WebEnvironment.NONE) @ContextConfiguration(classes = AbstractIntegrationJUnit5Tests.Config.class) public abstract class AbstractIntegrationJUnit5Tests { protected final Logger log = LoggerFactory.getLogger(this.getClass()); protected String testName; @Autowired protected MavenProperties mavenProperties; @BeforeEach public void setup(TestInfo testInfo) { Optional testMethod = testInfo.getTestMethod(); testMethod.ifPresent(method -> this.testName = method.getName()); } protected String randomName() { return this.testName + "-" + UUID.randomUUID().toString(); } /** * Return the timeout to use for repeatedly querying app status while it is being deployed. * Default value is one minute, being queried every 5 seconds. * @return the timeout */ protected Timeout deploymentTimeout() { return new Timeout(30, 5000); } /** * Return the timeout to use for repeatedly querying app status while it is being un-deployed. * Default value is one minute, being queried every 5 seconds. * @return the timeout */ protected Timeout undeploymentTimeout() { return new Timeout(20, 5000); } /** * Return the time to wait between reusing deployment requests. This could be necessary to give * some platforms time to clean up after undeployment. * @return redeployment pause */ protected int redeploymentPause() { return 0; } /** * Return a resource corresponding to the spring-cloud-deployer-spi-test-app app suitable for the target runtime. * * The default implementation returns an uber-jar fetched via Maven. Subclasses may override. * @return the resource */ protected Resource testApplication() { Properties properties = new Properties(); try { properties.load(new ClassPathResource("integration-test-app.properties").getInputStream()); } catch (IOException e) { throw new RuntimeException("Failed to determine which version of spring-cloud-deployer-spi-test-app to use", e); } return new MavenResource.Builder(mavenProperties) .groupId("org.springframework.cloud") .artifactId("spring-cloud-deployer-spi-test-app") .classifier("exec") .version(properties.getProperty("version")) .extension("jar") .build(); } @Configuration public static class Config { @Bean @ConfigurationProperties("maven") public MavenProperties mavenProperties() { return new MavenProperties(); } } } ================================================ FILE: spring-cloud-deployer-spi-test/src/main/java/org/springframework/cloud/deployer/spi/test/AbstractTaskLauncherIntegrationJUnit5Tests.java ================================================ /* * Copyright 2016-2019 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.cloud.deployer.spi.test; import java.time.Duration; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import org.assertj.core.api.AbstractAssert; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; import org.springframework.cloud.deployer.spi.task.LaunchState; import org.springframework.cloud.deployer.spi.task.TaskLauncher; import org.springframework.cloud.deployer.spi.task.TaskStatus; import org.springframework.core.io.Resource; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; /** * Abstract base class for integration tests of * {@link org.springframework.cloud.deployer.spi.task.TaskLauncher} implementations. *

* Inheritors should setup an environment with a newly created * {@link org.springframework.cloud.deployer.spi.task.TaskLauncher}. * * Tests in this class are independent and leave the * launcher in a clean state after they successfully run. *

*

* As deploying a task is often quite time consuming, some tests assert * various aspects of deployment in a row, to avoid re-deploying apps over and * over again. *

* * @author Eric Bottard * @author Ilayaperumal Gopinathan */ public abstract class AbstractTaskLauncherIntegrationJUnit5Tests extends AbstractIntegrationJUnit5Tests { private TaskLauncherWrapper launcherWrapper; /** * To be implemented by subclasses, which should return the instance of TaskLauncher that needs * to be tested. If subclasses decide to add additional implementation-specific tests, they should * interact with the task launcher through {@link #taskLauncher()}, and not directly via a field or a call * to this method. * @return the task launcher */ protected abstract TaskLauncher provideTaskLauncher(); /** * Subclasses should call this method to interact with the AppDeployer under test. * Returns a wrapper around the deployer returned by {@link #provideTaskLauncher()}, that keeps * track of which apps have been deployed and undeployed. * @return the task launcher */ protected TaskLauncher taskLauncher() { return launcherWrapper; } @BeforeEach public void wrapDeployer() { launcherWrapper = new TaskLauncherWrapper(provideTaskLauncher()); } @AfterEach public void cleanupLingeringApps() { for (String id : launcherWrapper.launchedTasks) { try { launcherWrapper.wrapped.cleanup(id); } catch (Exception e) { log.warn("Exception caught while trying to cleanup '{}'. Moving on...", id); } } for (String appName : launcherWrapper.deployedApps) { try { log.warn("Test named '{}' left behind an app for ''. Trying to destroy.", this.testName, appName); launcherWrapper.wrapped.destroy(appName); } catch (Exception e) { log.warn("Exception caught while trying to destroy '{}'. Moving on...", appName); } } } @Test public void testNonExistentAppsStatus() { assertThat(taskLauncher().status(randomName()).getState()).isEqualTo(LaunchState.unknown); } @Test public void testSimpleLaunch() throws InterruptedException { Map appProperties = new HashMap<>(); appProperties.put("killDelay", "0"); appProperties.put("exitCode", "0"); AppDefinition definition = new AppDefinition(randomName(), appProperties); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); log.info("Launching {}...", request.getDefinition().getName()); String launchId = taskLauncher().launch(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(taskLauncher().status(launchId).getState()).isEqualTo(LaunchState.complete); }); taskLauncher().destroy(definition.getName()); } @Test public void testReLaunch() throws InterruptedException { Map appProperties = new HashMap<>(); appProperties.put("killDelay", "0"); appProperties.put("exitCode", "0"); AppDefinition definition = new AppDefinition(randomName(), appProperties); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); log.info("Launching {}...", request.getDefinition().getName()); String launchId = taskLauncher().launch(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(taskLauncher().status(launchId).getState()).isEqualTo(LaunchState.complete); }); log.info("Re-Launching {}...", request.getDefinition().getName()); String newLaunchId = taskLauncher().launch(request); assertThat(newLaunchId).isNotEqualTo(launchId); timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(taskLauncher().status(newLaunchId).getState()).isEqualTo(LaunchState.complete); }); taskLauncher().destroy(definition.getName()); } @Test public void testErrorExit() throws InterruptedException { Map appProperties = new HashMap<>(); appProperties.put("killDelay", "0"); appProperties.put("exitCode", "1"); AppDefinition definition = new AppDefinition(randomName(), appProperties); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); log.info("Launching {}...", request.getDefinition().getName()); String launchId = taskLauncher().launch(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(taskLauncher().status(launchId).getState()).isEqualTo(LaunchState.failed); }); taskLauncher().destroy(definition.getName()); } @Test public void testSimpleCancel() throws InterruptedException { Map appProperties = new HashMap<>(); appProperties.put("killDelay", "-1"); appProperties.put("exitCode", "0"); AppDefinition definition = new AppDefinition(randomName(), appProperties); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); log.info("Launching {}...", request.getDefinition().getName()); String launchId = taskLauncher().launch(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(taskLauncher().status(launchId).getState()).isEqualTo(LaunchState.running); }); log.info("Cancelling {}...", request.getDefinition().getName()); taskLauncher().cancel(launchId); timeout = undeploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(taskLauncher().status(launchId).getState()).isEqualTo(LaunchState.cancelled); }); taskLauncher().destroy(definition.getName()); } /** * Tests that command line args can be passed in. */ @Test public void testCommandLineArgs() { Map properties = new HashMap<>(); properties.put("killDelay", "1000"); AppDefinition definition = new AppDefinition(randomName(), properties); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, Collections.emptyMap(), Collections.singletonList("--exitCode=0")); log.info("Launching {}...", request.getDefinition().getName()); String deploymentId = taskLauncher().launch(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(taskLauncher().status(deploymentId).getState()).isEqualTo(LaunchState.complete); }); taskLauncher().destroy(definition.getName()); } /** * Tests support for DeployerEnvironmentInfo is implemented. */ @Test public void testEnvironmentInfo() { RuntimeEnvironmentInfo info = taskLauncher().environmentInfo(); assertThat(info.getImplementationVersion()).isNotNull(); assertThat(info.getPlatformType()).isNotNull(); assertThat(info.getPlatformClientVersion()).isNotNull(); assertThat(info.getPlatformHostVersion()).isNotNull(); } protected static class TaskLauncherAssert extends AbstractAssert { public TaskLauncherAssert(TaskLauncher launcher) { super(launcher, TaskLauncherAssert.class); } } /** * A decorator for TaskLauncher that keeps track of deployed/undeployed apps. * * @author Eric Bottard */ protected static class TaskLauncherWrapper implements TaskLauncher { private final TaskLauncher wrapped; private final Set deployedApps = new LinkedHashSet<>(); private final Set launchedTasks = new LinkedHashSet<>(); public TaskLauncherWrapper(TaskLauncher wrapped) { this.wrapped = wrapped; } @Override public String launch(AppDeploymentRequest request) { String launchId = wrapped.launch(request); deployedApps.add(request.getDefinition().getName()); launchedTasks.add(launchId); return launchId; } @Override public void cancel(String id) { wrapped.cancel(id); } @Override public TaskStatus status(String id) { return wrapped.status(id); } @Override public void cleanup(String id) { wrapped.cleanup(id); launchedTasks.remove(id); } @Override public void destroy(String appName) { wrapped.destroy(appName); deployedApps.remove(appName); } @Override public RuntimeEnvironmentInfo environmentInfo() { return wrapped.environmentInfo(); } @Override public int getMaximumConcurrentTasks() { return wrapped.getMaximumConcurrentTasks(); } @Override public int getRunningTaskExecutionCount() { return wrapped.getRunningTaskExecutionCount(); } @Override public String getLog(String id) { return wrapped.getLog(id); } } } ================================================ FILE: spring-cloud-deployer-spi-test/src/main/java/org/springframework/cloud/deployer/spi/test/Timeout.java ================================================ /* * Copyright 2016 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.cloud.deployer.spi.test; /** * Represents a timeout for querying status, with repetitive queries until a certain number have been made. * * @author Eric Bottard */ public class Timeout { public final int maxAttempts; public final int pause; public final long totalTime; public Timeout(int maxAttempts, int pause) { this.maxAttempts = maxAttempts; this.pause = pause; this.totalTime = (long) this.maxAttempts * (long) this.pause; } } ================================================ FILE: spring-cloud-deployer-spi-test/src/main/resources/integration-test-app.properties ================================================ version=@integration-test-app.version@ ================================================ FILE: spring-cloud-deployer-spi-test-app/Dockerfile ================================================ FROM java:8-alpine ARG JAR_FILE ADD target/${JAR_FILE} spring-cloud-deployer-spi-test-app.jar ENTRYPOINT ["java", "-jar", "/spring-cloud-deployer-spi-test-app.jar"] ================================================ FILE: spring-cloud-deployer-spi-test-app/pom.xml ================================================ 4.0.0 org.springframework.cloud spring-cloud-deployer-parent 3.0.0-SNAPSHOT .. spring-cloud-deployer-spi-test-app org.springframework.cloud.deployer.spi.test.app.DeployerIntegrationTestApplication 17 springcloud 1.3.6 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-configuration-processor true org.springframework.boot spring-boot-maven-plugin ${spring-boot.version} IF_NOT_PRESENT springcloud/spring-cloud-deployer-spi-test-app:latest ================================================ FILE: spring-cloud-deployer-spi-test-app/src/main/java/org/springframework/cloud/deployer/spi/test/app/DeployerIntegrationTest.java ================================================ /* * Copyright 2016 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.cloud.deployer.spi.test.app; import jakarta.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; import org.springframework.util.Assert; /** * An app that can misbehave, useful for integration testing of app deployers. * * @author Eric Bottard */ @EnableConfigurationProperties(DeployerIntegrationTestProperties.class) @Configuration public class DeployerIntegrationTest { @Autowired private DeployerIntegrationTestProperties properties; @PostConstruct public void init() throws InterruptedException { String parameterThatMayNeedEscaping = properties.getParameterThatMayNeedEscaping(); if (parameterThatMayNeedEscaping != null && !DeployerIntegrationTestProperties.FUNNY_CHARACTERS.equals(parameterThatMayNeedEscaping)) { throw new IllegalArgumentException( String.format("Expected 'parameterThatMayNeedEscaping' value to be equal to '%s', but was '%s'", DeployerIntegrationTestProperties.FUNNY_CHARACTERS, parameterThatMayNeedEscaping)); } String commandLineArgValueThatMayNeedEscaping = properties.getCommandLineArgValueThatMayNeedEscaping(); if (commandLineArgValueThatMayNeedEscaping != null && !DeployerIntegrationTestProperties.FUNNY_CHARACTERS.equals(commandLineArgValueThatMayNeedEscaping)) { throw new IllegalArgumentException(String.format( "Expected 'commandLineArgValueThatMayNeedEscaping' value to be equal to '%s', but was '%s'", DeployerIntegrationTestProperties.FUNNY_CHARACTERS, commandLineArgValueThatMayNeedEscaping)); } Assert.notNull(properties.getInstanceIndex(), "instanceIndex should have been set by deployer or runtime"); if (properties.getMatchInstances().isEmpty() || properties.getMatchInstances().contains(properties.getInstanceIndex())) { System.out.format("Waiting for %dms before allowing further initialization and actuator startup...", properties.getInitDelay()); Thread.sleep(properties.getInitDelay()); System.out.println("... done"); if (properties.getKillDelay() >= 0) { System.out.format("Will kill this process in %dms%n", properties.getKillDelay()); new Thread() { @Override public void run() { try { Thread.sleep(properties.getKillDelay()); System.exit(properties.getExitCode()); } catch (InterruptedException e) { throw new RuntimeException(e); } } }.start(); } } } } ================================================ FILE: spring-cloud-deployer-spi-test-app/src/main/java/org/springframework/cloud/deployer/spi/test/app/DeployerIntegrationTestApplication.java ================================================ /* * Copyright 2016 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.cloud.deployer.spi.test.app; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * Main application runner. Will keep the main method running, to simulate a long running process. * * @author Eric Bottard */ @SpringBootApplication public class DeployerIntegrationTestApplication { public static void main(String[] args) { SpringApplication.run(DeployerIntegrationTestApplication.class, args); } } ================================================ FILE: spring-cloud-deployer-spi-test-app/src/main/java/org/springframework/cloud/deployer/spi/test/app/DeployerIntegrationTestProperties.java ================================================ /* * Copyright 2016 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.cloud.deployer.spi.test.app; import java.util.HashSet; import java.util.Set; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; /** * Configuration properties for the IntegrationTestProcessor app. * * @author Eric Bottard */ @ConfigurationProperties public class DeployerIntegrationTestProperties { public static final String FUNNY_CHARACTERS = "&'\"|< é\\("; /** * The delay in milliseconds to stall the initialization of this app. * Useful for testing the 'deploying' state of a app. */ private int initDelay = 0; /** * The delay in milliseconds after which this app will kill itself. *

-1 means don't kill

*/ private int killDelay = -1; /** * The exit code used when this app will kill itself. *

Set this to 0 for normal exit

*/ private int exitCode = 1; /** * If not empty, only the app intances whose number(s) are contained in this set * will behave according to the other configuration parameters. */ private Set matchInstances = new HashSet<>(); /** * If not null, this property will be tested against {@link #FUNNY_CHARACTERS}. * This makes sure that a deployer knows how to properly propagate application properties, including * those that contain chars that often require some form of escaping. */ private String parameterThatMayNeedEscaping; /** * If not null, this property will be tested against {@link #FUNNY_CHARACTERS}. * This makes sure that a deployer knows how to properly propagate deployment properties, including * those that contain chars that often require some form of escaping. */ private String commandLineArgValueThatMayNeedEscaping; @Value("${INSTANCE_INDEX:${instance.index:${CF_INSTANCE_INDEX:0}}}") private Integer instanceIndex; public int getInitDelay() { return initDelay; } public void setInitDelay(int initDelay) { this.initDelay = initDelay; } public int getKillDelay() { return killDelay; } public void setKillDelay(int killDelay) { this.killDelay = killDelay; } public int getExitCode() { return exitCode; } public void setExitCode(int exitCode) { this.exitCode = exitCode; } public Set getMatchInstances() { return matchInstances; } public void setMatchInstances(Set matchInstances) { this.matchInstances = matchInstances; } public String getParameterThatMayNeedEscaping() { return parameterThatMayNeedEscaping; } public void setParameterThatMayNeedEscaping(String parameterThatMayNeedEscaping) { this.parameterThatMayNeedEscaping = parameterThatMayNeedEscaping; } public Integer getInstanceIndex() { return instanceIndex; } public void setInstanceIndex(Integer instanceIndex) { this.instanceIndex = instanceIndex; } public String getCommandLineArgValueThatMayNeedEscaping() { return commandLineArgValueThatMayNeedEscaping; } public void setCommandLineArgValueThatMayNeedEscaping(String commandLineArgValueThatMayNeedEscaping) { this.commandLineArgValueThatMayNeedEscaping = commandLineArgValueThatMayNeedEscaping; } } ================================================ FILE: spring-cloud-deployer-spi-test-app/src/main/resources/application.properties ================================================ management.endpoint.shutdown.enabled=true management.endpoints.web.exposure.include=* ================================================ FILE: src/main/java/org/springframework/cloud/deployer/spi/kubernetes/DefaultContainerFactory.java ================================================ /* * Copyright 2015-2022 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.cloud.deployer.spi.kubernetes; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import io.fabric8.kubernetes.api.model.Container; import io.fabric8.kubernetes.api.model.ContainerBuilder; import io.fabric8.kubernetes.api.model.EnvFromSource; import io.fabric8.kubernetes.api.model.EnvVar; import io.fabric8.kubernetes.api.model.EnvVarSource; import io.fabric8.kubernetes.api.model.ObjectFieldSelector; import io.fabric8.kubernetes.api.model.Probe; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.cloud.deployer.spi.app.AppDeployer; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.scheduler.ScheduleRequest; import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** * Create a Kubernetes {@link Container} that will be started as part of a * Kubernetes Pod by launching the specified Docker image. * * @author Florian Rosenberg * @author Thomas Risberg * @author Donovan Muller * @author David Turanski * @author Chris Schaefer * @author Ilayaperumal Gopinathan * @author Glenn Renfro * @author Corneil du Plessis */ public class DefaultContainerFactory implements ContainerFactory { private static Log logger = LogFactory.getLog(DefaultContainerFactory.class); private static final String SPRING_APPLICATION_JSON = "SPRING_APPLICATION_JSON"; private static final String SPRING_CLOUD_APPLICATION_GUID = "SPRING_CLOUD_APPLICATION_GUID"; private final KubernetesDeployerProperties properties; public DefaultContainerFactory(KubernetesDeployerProperties properties) { this.properties = properties; } @Override public Container create(ContainerConfiguration containerConfiguration) { AppDeploymentRequest request = containerConfiguration.getAppDeploymentRequest(); Map deploymentProperties = getDeploymentProperties(request); DeploymentPropertiesResolver deploymentPropertiesResolver = getDeploymentPropertiesResolver(request); String image; try { image = request.getResource().getURI().getSchemeSpecificPart(); } catch (IOException e) { throw new IllegalArgumentException("Unable to get URI for " + request.getResource(), e); } logger.info("Using Docker image: " + image); EntryPointStyle entryPointStyle = deploymentPropertiesResolver.determineEntryPointStyle(deploymentProperties); logger.info("Using Docker entry point style: " + entryPointStyle); Map envVarsMap = new HashMap<>(); for (String envVar : this.properties.getEnvironmentVariables()) { String[] strings = envVar.split("=", 2); Assert.isTrue(strings.length == 2, "Invalid environment variable declared: " + envVar); envVarsMap.put(strings[0], strings[1]); } //Create EnvVar entries for additional variables set at the app level //For instance, this may be used to set JAVA_OPTS independently for each app if the base container //image supports it. envVarsMap.putAll(deploymentPropertiesResolver.getAppEnvironmentVariables(deploymentProperties)); List appArgs = new ArrayList<>(); Map appAdminCredentials = new HashMap<>(); properties.getAppAdmin().addCredentialsToAppEnvironmentAsProperties(appAdminCredentials); switch (entryPointStyle) { case exec: appArgs = createCommandArgs(request); List finalAppArgs = appArgs; appAdminCredentials.forEach((k, v) -> finalAppArgs.add(String.format("--%s=%s", k, v))); break; case boot: if (envVarsMap.containsKey(SPRING_APPLICATION_JSON)) { throw new IllegalStateException( "You can't use boot entry point style and also set SPRING_APPLICATION_JSON for the app"); } try { envVarsMap.put(SPRING_APPLICATION_JSON, new ObjectMapper().writeValueAsString(request.getDefinition().getProperties())); } catch (JsonProcessingException e) { throw new IllegalStateException("Unable to create SPRING_APPLICATION_JSON", e); } appArgs = request.getCommandlineArguments(); break; case shell: for (String key : request.getDefinition().getProperties().keySet()) { String envVar = key.replace('.', '_').toUpperCase(Locale.ROOT); envVarsMap.put(envVar, request.getDefinition().getProperties().get(key)); envVarsMap.putAll(appAdminCredentials); } // Push all the command line arguments as environment properties // The task app name(in case of Composed Task), platform_name and executionId are expected to be updated. // This will also override any of the existing app properties that match the provided cmdline args. for (String cmdLineArg : request.getCommandlineArguments()) { String cmdLineArgKey; if (cmdLineArg.startsWith("--")) { cmdLineArgKey = cmdLineArg.substring(2, cmdLineArg.indexOf("=")); } else { cmdLineArgKey = cmdLineArg.substring(0, cmdLineArg.indexOf("=")); } String cmdLineArgValue = cmdLineArg.substring(cmdLineArg.indexOf("=") + 1); envVarsMap.put(cmdLineArgKey.replace('.', '_').toUpperCase(Locale.ROOT), cmdLineArgValue); } break; } List envVars = new ArrayList<>(); for (Map.Entry e : envVarsMap.entrySet()) { envVars.add(new EnvVar(e.getKey(), e.getValue(), null)); } envVars.addAll(deploymentPropertiesResolver.getSecretKeyRefs(deploymentProperties)); envVars.addAll(deploymentPropertiesResolver.getConfigMapKeyRefs(deploymentProperties)); envVars.add(getGUIDEnvVar()); if (request.getDeploymentProperties().get(AppDeployer.GROUP_PROPERTY_KEY) != null) { envVars.add(new EnvVar("SPRING_CLOUD_APPLICATION_GROUP", request.getDeploymentProperties().get(AppDeployer.GROUP_PROPERTY_KEY), null)); } List envFromSources = new ArrayList<>(); envFromSources.addAll(deploymentPropertiesResolver.getConfigMapRefs(deploymentProperties)); envFromSources.addAll(deploymentPropertiesResolver.getSecretRefs(deploymentProperties)); ContainerBuilder container = new ContainerBuilder(); container.withName(containerConfiguration.getAppId()).withImage(image).withEnv(envVars).withEnvFrom(envFromSources) .withArgs(appArgs).withVolumeMounts(deploymentPropertiesResolver.getVolumeMounts(deploymentProperties)); Set ports = new HashSet<>(); Integer defaultPort = containerConfiguration.getExternalPort(); if (defaultPort != null) { ports.add(defaultPort); } ports.addAll(deploymentPropertiesResolver.getContainerPorts(deploymentProperties)); configureStartupProbe(containerConfiguration, container, ports); configureReadinessProbe(containerConfiguration, container, ports); configureLivenessProbe(containerConfiguration, container, ports); if (!ports.isEmpty()) { for (Integer containerPort : ports) { if (containerConfiguration.isHostNetwork()) { container.addNewPort().withContainerPort(containerPort).withHostPort(containerPort).endPort(); } else { container.addNewPort().withContainerPort(containerPort).endPort(); } } } //Override the containers default entry point with one specified during the app deployment List containerCommand = deploymentPropertiesResolver.getContainerCommand(deploymentProperties); if (!containerCommand.isEmpty()) { container.withCommand(containerCommand); } return container.build(); } private EnvVar getGUIDEnvVar() { ObjectFieldSelector objectFieldSelector = new ObjectFieldSelector(); objectFieldSelector.setFieldPath("metadata.uid"); EnvVarSource envVarSource = new EnvVarSource(); envVarSource.setFieldRef(objectFieldSelector); EnvVar guidEnvVar = new EnvVar(); guidEnvVar.setValueFrom(envVarSource); guidEnvVar.setName(SPRING_CLOUD_APPLICATION_GUID); return guidEnvVar; } private void configureReadinessProbe(ContainerConfiguration containerConfiguration, ContainerBuilder containerBuilder, Set ports) { Probe readinessProbe = ProbeCreatorFactory.createReadinessProbe(containerConfiguration, properties, getProbeType(containerConfiguration)); Integer probePort = null; if (readinessProbe.getHttpGet() != null) { probePort = readinessProbe.getHttpGet().getPort().getIntVal(); } if (readinessProbe.getTcpSocket() != null) { probePort = readinessProbe.getTcpSocket().getPort().getIntVal(); } if (probePort != null || (containerConfiguration.getExternalPort() != null && readinessProbe.getExec() != null)) { containerBuilder.withReadinessProbe(readinessProbe); } if (probePort != null) { ports.add(probePort); } } private void configureStartupProbe(ContainerConfiguration containerConfiguration, ContainerBuilder containerBuilder, Set ports) { Probe startupProbe = ProbeCreatorFactory.createStartupProbe(containerConfiguration, properties, getProbeType(containerConfiguration)); Integer probePort = null; if (startupProbe.getHttpGet() != null) { probePort = startupProbe.getHttpGet().getPort().getIntVal(); } if (startupProbe.getTcpSocket() != null) { probePort = startupProbe.getTcpSocket().getPort().getIntVal(); } if (probePort != null || (containerConfiguration.getExternalPort() != null && startupProbe.getExec() != null)) { containerBuilder.withStartupProbe(startupProbe); } if (probePort != null) { ports.add(probePort); } } private void configureLivenessProbe(ContainerConfiguration containerConfiguration, ContainerBuilder containerBuilder, Set ports) { Probe livenessProbe = ProbeCreatorFactory.createLivenessProbe(containerConfiguration, properties, getProbeType(containerConfiguration)); Integer probePort = null; if (livenessProbe.getHttpGet() != null) { probePort = livenessProbe.getHttpGet().getPort().getIntVal(); } if (livenessProbe.getTcpSocket() != null) { probePort = livenessProbe.getTcpSocket().getPort().getIntVal(); } if (probePort != null || (containerConfiguration.getExternalPort() != null && livenessProbe.getExec() != null)) { containerBuilder.withLivenessProbe(livenessProbe); } if (probePort != null) { ports.add(probePort); } } private ProbeType getProbeType(ContainerConfiguration containerConfiguration) { AppDeploymentRequest appDeploymentRequest = containerConfiguration.getAppDeploymentRequest(); Map deploymentProperties = getDeploymentProperties(appDeploymentRequest); DeploymentPropertiesResolver deploymentPropertiesResolver = getDeploymentPropertiesResolver(appDeploymentRequest); return deploymentPropertiesResolver.determineProbeType(deploymentProperties); } /** * Create command arguments * * @param request the {@link AppDeploymentRequest} * @return the command line arguments to use */ List createCommandArgs(AppDeploymentRequest request) { List cmdArgs = new LinkedList<>(); List commandArgOptions = request.getCommandlineArguments().stream() .map(this::getArgOption) .collect(Collectors.toList()); // add properties from deployment request Map args = request.getDefinition().getProperties(); for (Map.Entry entry : args.entrySet()) { if (!StringUtils.hasText(entry.getValue())) { logger.warn( "Excluding request property with missing value from command args: " + entry.getKey()); } else if (commandArgOptions.contains(entry.getKey())) { logger.warn( String.format( "Excluding request property [--%s=%s] as a command arg. Existing command line argument takes precedence." , entry.getKey(), entry.getValue())); } else { cmdArgs.add(String.format("--%s=%s", entry.getKey(), entry.getValue())); } } // add provided command line args cmdArgs.addAll(request.getCommandlineArguments()); logger.debug("Using command args: " + cmdArgs); return cmdArgs; } private String getArgOption(String arg) { int indexOfAssignment = arg.indexOf("="); String argOption = (indexOfAssignment < 0) ? arg : arg.substring(0, indexOfAssignment); return argOption.trim().replaceAll("^--", ""); } private DeploymentPropertiesResolver getDeploymentPropertiesResolver(AppDeploymentRequest request) { String propertiesPrefix = (request instanceof ScheduleRequest && ((ScheduleRequest) request).getSchedulerProperties() != null && ((ScheduleRequest) request).getSchedulerProperties().size() > 0) ? KubernetesSchedulerProperties.KUBERNETES_SCHEDULER_PROPERTIES_PREFIX : KubernetesDeployerProperties.KUBERNETES_DEPLOYER_PROPERTIES_PREFIX; return new DeploymentPropertiesResolver(propertiesPrefix, this.properties); } private Map getDeploymentProperties(AppDeploymentRequest request) { return request.getDeploymentProperties(); } } ================================================ FILE: src/main/java/org/springframework/cloud/deployer/spi/kubernetes/HttpProbeCreator.java ================================================ /* * Copyright 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.cloud.deployer.spi.kubernetes; import java.util.ArrayList; import java.util.List; import io.fabric8.kubernetes.api.model.HTTPGetActionBuilder; import io.fabric8.kubernetes.api.model.HTTPHeader; import io.fabric8.kubernetes.api.model.Probe; import io.fabric8.kubernetes.api.model.ProbeBuilder; import io.fabric8.kubernetes.api.model.Secret; import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** * Base class for HTTP based probe creators * * @author Chris Schaefer * @author Corneil du Plessis * @since 2.5 */ public abstract class HttpProbeCreator extends ProbeCreator { private static final int BOOT_1_MAJOR_VERSION = 1; static final String AUTHORIZATION_HEADER_NAME = "Authorization"; static final String PROBE_CREDENTIALS_SECRET_KEY_NAME = "credentials"; static final String BOOT_1_READINESS_PROBE_PATH = "/info"; static final String BOOT_1_LIVENESS_PROBE_PATH = "/health"; static final String BOOT_2_READINESS_PROBE_PATH = "/actuator" + BOOT_1_READINESS_PROBE_PATH; static final String BOOT_2_LIVENESS_PROBE_PATH = "/actuator" + BOOT_1_LIVENESS_PROBE_PATH; static final String DEFAULT_PROBE_SCHEME = "HTTP"; HttpProbeCreator(KubernetesDeployerProperties kubernetesDeployerProperties, ContainerConfiguration containerConfiguration) { super(kubernetesDeployerProperties, containerConfiguration); } protected abstract String getProbePath(); protected abstract Integer getPort(); protected abstract String getScheme(); protected abstract int getTimeout(); protected Probe create() { HTTPGetActionBuilder httpGetActionBuilder = new HTTPGetActionBuilder() .withPath(getProbePath()) .withNewPort(getPort()) .withScheme(getScheme()); List httpHeaders = getHttpHeaders(); if (!httpHeaders.isEmpty()) { httpGetActionBuilder.withHttpHeaders(httpHeaders); } return new ProbeBuilder() .withHttpGet(httpGetActionBuilder.build()) .withTimeoutSeconds(getTimeout()) .withInitialDelaySeconds(getInitialDelay()) .withPeriodSeconds(getPeriod()) .withFailureThreshold(getFailure()) .withSuccessThreshold(getSuccess()) .build(); } private List getHttpHeaders() { List httpHeaders = new ArrayList<>(); HTTPHeader authenticationHeader = getAuthorizationHeader(); if (authenticationHeader != null) { httpHeaders.add(authenticationHeader); } return httpHeaders; } private HTTPHeader getAuthorizationHeader() { HTTPHeader httpHeader = null; Secret probeCredentialsSecret = getContainerConfiguration().getProbeCredentialsSecret(); if (probeCredentialsSecret != null) { Assert.isTrue(probeCredentialsSecret.getData().containsKey(PROBE_CREDENTIALS_SECRET_KEY_NAME), "Secret does not contain a key by the name of " + PROBE_CREDENTIALS_SECRET_KEY_NAME); httpHeader = new HTTPHeader(); httpHeader.setName(AUTHORIZATION_HEADER_NAME); // only Basic auth is supported currently httpHeader.setValue(ProbeAuthenticationType.Basic.name() + " " + probeCredentialsSecret.getData().get(PROBE_CREDENTIALS_SECRET_KEY_NAME)); } return httpHeader; } Integer getDefaultPort() { return getContainerConfiguration().getExternalPort(); } boolean useBoot1ProbePath() { String bootMajorVersionProperty = KUBERNETES_DEPLOYER_PREFIX + ".bootMajorVersion"; String bootMajorVersionValue = getDeploymentPropertyValue(bootMajorVersionProperty); if (StringUtils.hasText(bootMajorVersionValue)) { return Integer.parseInt(bootMajorVersionValue) == BOOT_1_MAJOR_VERSION; } return false; } } ================================================ FILE: src/main/java/org/springframework/cloud/deployer/spi/kubernetes/KubernetesAppDeployer.java ================================================ /* * Copyright 2015-2023 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.cloud.deployer.spi.kubernetes; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import io.fabric8.kubernetes.api.model.Container; import io.fabric8.kubernetes.api.model.ContainerBuilder; import io.fabric8.kubernetes.api.model.PersistentVolumeClaim; import io.fabric8.kubernetes.api.model.PersistentVolumeClaimBuilder; import io.fabric8.kubernetes.api.model.PersistentVolumeClaimList; import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.api.model.PodList; import io.fabric8.kubernetes.api.model.PodSpec; import io.fabric8.kubernetes.api.model.Quantity; import io.fabric8.kubernetes.api.model.SecurityContext; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.ServiceBuilder; import io.fabric8.kubernetes.api.model.ServiceList; import io.fabric8.kubernetes.api.model.ServicePort; import io.fabric8.kubernetes.api.model.ServiceSpecBuilder; import io.fabric8.kubernetes.api.model.VolumeBuilder; import io.fabric8.kubernetes.api.model.VolumeMountBuilder; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; import io.fabric8.kubernetes.api.model.apps.DeploymentList; import io.fabric8.kubernetes.api.model.apps.StatefulSet; import io.fabric8.kubernetes.api.model.apps.StatefulSetBuilder; import io.fabric8.kubernetes.api.model.apps.StatefulSetList; import io.fabric8.kubernetes.api.model.apps.StatefulSetSpec; import io.fabric8.kubernetes.api.model.apps.StatefulSetSpecBuilder; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable; import io.fabric8.kubernetes.client.dsl.ScalableResource; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.deployer.spi.app.AppDeployer; import org.springframework.cloud.deployer.spi.app.AppInstanceStatus; import org.springframework.cloud.deployer.spi.app.AppScaleRequest; import org.springframework.cloud.deployer.spi.app.AppStatus; import org.springframework.cloud.deployer.spi.app.DeploymentState; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; import org.springframework.cloud.deployer.spi.kubernetes.support.ArgumentSanitizer; import org.springframework.cloud.deployer.spi.kubernetes.support.PropertyParserUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; /** * A deployer that targets Kubernetes. * * @author Florian Rosenberg * @author Thomas Risberg * @author Mark Fisher * @author Donovan Muller * @author David Turanski * @author Ilayaperumal Gopinathan * @author Chris Schaefer * @author Christian Tzolov * @author Omar Gonzalez * @author Chris Bono */ public class KubernetesAppDeployer extends AbstractKubernetesDeployer implements AppDeployer { protected final Log logger = LogFactory.getLog(getClass().getName()); @Autowired public KubernetesAppDeployer(KubernetesDeployerProperties properties, KubernetesClient client) { this(properties, client, new DefaultContainerFactory(properties)); } @Autowired public KubernetesAppDeployer(KubernetesDeployerProperties properties, KubernetesClient client, ContainerFactory containerFactory) { this.properties = properties; this.client = client; this.containerFactory = containerFactory; this.deploymentPropertiesResolver = new DeploymentPropertiesResolver( KubernetesDeployerProperties.KUBERNETES_DEPLOYER_PROPERTIES_PREFIX, properties); } @Override public String deploy(AppDeploymentRequest request) { String appId = createDeploymentId(request); if (logger.isDebugEnabled()) { ArgumentSanitizer sanitizer = new ArgumentSanitizer(); Map sanitized = sanitizer.sanitizeProperties(request.getDeploymentProperties()); List sanitizedCommandlineArguments = sanitizer.sanitizeArguments(request.getCommandlineArguments()); logger.debug(String.format("Deploying app: %s, request: commandlineArguments=%s, deploymentProperties=%s, definition=%s, resource=%s", appId, sanitizedCommandlineArguments, sanitized, request.getDefinition(), request.getResource())); } try { AppStatus status = status(appId); if (!status.getState().equals(DeploymentState.unknown)) { throw new IllegalStateException(String.format("App '%s' is already deployed", appId)); } String indexedProperty = request.getDeploymentProperties().get(INDEXED_PROPERTY_KEY); boolean indexed = (indexedProperty != null) ? Boolean.valueOf(indexedProperty) : false; logPossibleDownloadResourceMessage(request.getResource()); createService(request); if (indexed) { createStatefulSet(request); } else { createDeployment(request); } return appId; } catch (RuntimeException e) { logger.error(e.getMessage(), e); throw e; } } @Override public void undeploy(String appId) { if (logger.isDebugEnabled()) { logger.debug(String.format("Undeploying app: %s", appId)); } AppStatus status = status(appId); if (status.getState().equals(DeploymentState.unknown)) { // ensure objects for this appId are deleted in the event a previous deployment failed. // allows for log inspection prior to making an undeploy request. deleteAllObjects(appId); throw new IllegalStateException(String.format("App '%s' is not deployed", appId)); } try { deleteAllObjects(appId); } catch (RuntimeException e) { logger.error(e.getMessage(), e); throw e; } } @Override public AppStatus status(String appId) { Map selector = new HashMap<>(); ServiceList services = client.services().withLabel(SPRING_APP_KEY, appId).list(); selector.put(SPRING_APP_KEY, appId); PodList podList = client.pods().withLabels(selector).list(); if (logger.isDebugEnabled()) { logger.debug(String.format("Building AppStatus for app: %s", appId)); if (podList != null && podList.getItems() != null) { logger.debug(String.format("Pods for appId %s: %d", appId, podList.getItems().size())); for (Pod pod : podList.getItems()) { logger.debug(String.format("Pod: %s", pod.getMetadata().getName())); } } } AppStatus status = buildAppStatus(appId, podList, services); if (logger.isDebugEnabled()) { logger.debug(String.format("status:%s = %s", appId, status.getDeploymentId())); for (AppInstanceStatus instanceStatus : status.getInstances().values()) { logger.debug(String.format("status:%s:%s:%s", instanceStatus.getId(), instanceStatus.getState(), instanceStatus.getAttributes())); } } return status; } @Override public String getLog(String appId) { Map selector = new HashMap<>(); selector.put(SPRING_APP_KEY, appId); PodList podList = client.pods().withLabels(selector).list(); StringBuilder logAppender = new StringBuilder(); for (Pod pod : podList.getItems()) { if (pod.getSpec().getContainers().size() > 1) { for (Container container : pod.getSpec().getContainers()) { if (container.getEnv().stream().anyMatch(envVar -> "SPRING_CLOUD_APPLICATION_GUID".equals(envVar.getName()))) { //find log for this container logAppender.append(this.client.pods() .withName(pod.getMetadata().getName()) .inContainer(container.getName()) .tailingLines(500).getLog()); break; } } } else { logAppender.append(this.client.pods().withName(pod.getMetadata().getName()).tailingLines(500).getLog()); } } return logAppender.toString(); } @Override public void scale(AppScaleRequest appScaleRequest) { String deploymentId = appScaleRequest.getDeploymentId(); if (logger.isDebugEnabled()) { logger.debug(String.format("Scale app: %s to: %s", deploymentId, appScaleRequest.getCount())); } ScalableResource scalableResource = this.client.apps().deployments().withName(deploymentId); if (scalableResource.get() == null) { scalableResource = this.client.apps().statefulSets().withName(deploymentId); } if (scalableResource.get() == null) { throw new IllegalStateException(String.format("App '%s' is not deployed", deploymentId)); } scalableResource.scale(appScaleRequest.getCount(), true); } @Override public RuntimeEnvironmentInfo environmentInfo() { return super.createRuntimeEnvironmentInfo(AppDeployer.class, this.getClass()); } private Deployment createDeployment(AppDeploymentRequest request) { String appId = createDeploymentId(request); if (logger.isDebugEnabled()) { logger.debug(String.format("Creating Deployment: %s", appId)); } int replicas = getCountFromRequest(request); Map idMap = createIdMap(appId, request); Map kubernetesDeployerProperties = request.getDeploymentProperties(); Map annotations = this.deploymentPropertiesResolver.getPodAnnotations(kubernetesDeployerProperties); Map deploymentLabels = this.deploymentPropertiesResolver.getDeploymentLabels(kubernetesDeployerProperties); PodSpec podSpec = createPodSpec(request); Deployment d = new DeploymentBuilder().withNewMetadata().withName(appId).withLabels(idMap) .addToLabels(SPRING_MARKER_KEY, SPRING_MARKER_VALUE).addToLabels(deploymentLabels).endMetadata() .withNewSpec().withNewSelector().addToMatchLabels(idMap).endSelector().withReplicas(replicas) .withNewTemplate().withNewMetadata().withLabels(idMap).addToLabels(SPRING_MARKER_KEY, SPRING_MARKER_VALUE) .addToLabels(deploymentLabels).withAnnotations(annotations).endMetadata().withSpec(podSpec).endTemplate() .endSpec().build(); d = client.apps().deployments().create(d); if (logger.isDebugEnabled()) { logger.debug("created:" + d.getFullResourceName() + ":" + d.getStatus()); } return d; } private int getCountFromRequest(AppDeploymentRequest request) { String countProperty = request.getDeploymentProperties().get(COUNT_PROPERTY_KEY); return (countProperty != null) ? Integer.parseInt(countProperty) : 1; } /** * Create a StatefulSet * * @param request the {@link AppDeploymentRequest} */ protected void createStatefulSet(AppDeploymentRequest request) { String appId = createDeploymentId(request); int externalPort = getExternalPort(request); Map idMap = createIdMap(appId, request); int replicas = getCountFromRequest(request); Map kubernetesDeployerProperties = request.getDeploymentProperties(); if (logger.isDebugEnabled()) { logger.debug(String.format("Creating StatefulSet: %s on %d with %d replicas", appId, externalPort, replicas)); } Map storageResource = Collections.singletonMap("storage", new Quantity(this.deploymentPropertiesResolver.getStatefulSetStorage(kubernetesDeployerProperties))); String storageClassName = this.deploymentPropertiesResolver.getStatefulSetStorageClassName(kubernetesDeployerProperties); String volumeClaimTemplateName = this.deploymentPropertiesResolver.getStatefulSetVolumeClaimTemplateName(kubernetesDeployerProperties); volumeClaimTemplateName = StringUtils.hasText(volumeClaimTemplateName) ? volumeClaimTemplateName : appId; PersistentVolumeClaimBuilder persistentVolumeClaimBuilder = new PersistentVolumeClaimBuilder().withNewSpec(). withStorageClassName(storageClassName).withAccessModes(Collections.singletonList("ReadWriteOnce")) .withNewResources().addToLimits(storageResource).addToRequests(storageResource).endResources() .endSpec().withNewMetadata().withName(volumeClaimTemplateName).withLabels(idMap) .addToLabels(SPRING_MARKER_KEY, SPRING_MARKER_VALUE).endMetadata(); PodSpec podSpec = createPodSpec(request); podSpec.getVolumes().add(new VolumeBuilder().withName("config").withNewEmptyDir().endEmptyDir().build()); podSpec.getContainers().get(0).getVolumeMounts() .add(new VolumeMountBuilder().withName("config").withMountPath("/config").build()); String statefulSetInitContainerImageName = this.deploymentPropertiesResolver.getStatefulSetInitContainerImageName(kubernetesDeployerProperties); podSpec.getInitContainers().add(createStatefulSetInitContainer(podSpec, statefulSetInitContainerImageName)); Map deploymentLabels = this.deploymentPropertiesResolver.getDeploymentLabels(request.getDeploymentProperties()); Map annotations = this.deploymentPropertiesResolver.getPodAnnotations(kubernetesDeployerProperties); StatefulSetSpec spec = new StatefulSetSpecBuilder().withNewSelector().addToMatchLabels(idMap) .addToMatchLabels(SPRING_MARKER_KEY, SPRING_MARKER_VALUE).endSelector() .withVolumeClaimTemplates(persistentVolumeClaimBuilder.build()).withServiceName(appId) .withPodManagementPolicy("Parallel").withReplicas(replicas).withNewTemplate().withNewMetadata() .withLabels(idMap).addToLabels(SPRING_MARKER_KEY, SPRING_MARKER_VALUE).addToLabels(deploymentLabels) .addToAnnotations(annotations).endMetadata().withSpec(podSpec).endTemplate().build(); StatefulSet statefulSet = new StatefulSetBuilder().withNewMetadata().withName(appId).withLabels(idMap) .addToLabels(SPRING_MARKER_KEY, SPRING_MARKER_VALUE).addToLabels(deploymentLabels).endMetadata().withSpec(spec).build(); statefulSet = client.apps().statefulSets().create(statefulSet); if (logger.isDebugEnabled()) { logger.debug("created:" + statefulSet.getFullResourceName() + ":" + statefulSet.getStatus()); } } protected void createService(AppDeploymentRequest request) { String appId = createDeploymentId(request); int externalPort = getExternalPort(request); if (logger.isDebugEnabled()) { ArgumentSanitizer sanitizer = new ArgumentSanitizer(); Map sanitized = sanitizer.sanitizeProperties(request.getDeploymentProperties()); logger.debug(String.format("Creating Service: %s on %d using %s", appId, externalPort, sanitized)); } Map idMap = createIdMap(appId, request); ServiceSpecBuilder spec = new ServiceSpecBuilder(); boolean isCreateLoadBalancer = false; String createLoadBalancer = PropertyParserUtils.getDeploymentPropertyValue(request.getDeploymentProperties(), "spring.cloud.deployer.kubernetes.createLoadBalancer"); String createNodePort = PropertyParserUtils.getDeploymentPropertyValue(request.getDeploymentProperties(), "spring.cloud.deployer.kubernetes.createNodePort"); String additionalServicePorts = PropertyParserUtils.getDeploymentPropertyValue(request.getDeploymentProperties(), "spring.cloud.deployer.kubernetes.servicePorts"); if (createLoadBalancer != null && createNodePort != null) { throw new IllegalArgumentException("Cannot create NodePort and LoadBalancer at the same time."); } if (createLoadBalancer == null) { isCreateLoadBalancer = properties.isCreateLoadBalancer(); } else { if ("true".equals(createLoadBalancer.toLowerCase(Locale.ROOT))) { isCreateLoadBalancer = true; } } if (isCreateLoadBalancer) { spec.withType("LoadBalancer"); } ServicePort servicePort = new ServicePort(); servicePort.setPort(externalPort); servicePort.setName("port-" + externalPort); if (createNodePort != null) { spec.withType("NodePort"); if (!"true".equals(createNodePort.toLowerCase(Locale.ROOT))) { try { Integer nodePort = Integer.valueOf(createNodePort); servicePort.setNodePort(nodePort); } catch (NumberFormatException e) { throw new IllegalArgumentException( String.format("Invalid value: %s: provided port is not valid.", createNodePort)); } } } Set servicePorts = new HashSet<>(); servicePorts.add(servicePort); if (StringUtils.hasText(additionalServicePorts)) { servicePorts.addAll(addAdditionalServicePorts(additionalServicePorts)); } spec.addAllToPorts(servicePorts); Map annotations = this.deploymentPropertiesResolver.getServiceAnnotations(request.getDeploymentProperties()); String serviceName = getServiceName(request, appId); // if called from skipper, use unique selectors to maintain connectivity // between service and pods that are being brought up/down if (request.getDeploymentProperties().containsKey(APP_NAME_PROPERTY_KEY)) { spec.withSelector(Collections.singletonMap(APP_NAME_KEY, request.getDeploymentProperties().get(APP_NAME_PROPERTY_KEY))); String groupId = request.getDeploymentProperties().get(AppDeployer.GROUP_PROPERTY_KEY); if (groupId != null) { spec.addToSelector(SPRING_GROUP_KEY, groupId); } } else { spec.withSelector(idMap); } Service service = client.services().createOrReplace( new ServiceBuilder().withNewMetadata().withName(serviceName) .withLabels(idMap).withAnnotations(annotations).addToLabels(SPRING_MARKER_KEY, SPRING_MARKER_VALUE) .endMetadata().withSpec(spec.build()).build() ); if (logger.isDebugEnabled()) { logger.debug("created:" + service.getFullResourceName() + ":" + service.getStatus()); } } // logic to support using un-versioned service names when called from skipper private String getServiceName(AppDeploymentRequest request, String appId) { String appName = request.getDeploymentProperties().get(APP_NAME_PROPERTY_KEY); // if we have an un-versioned app name from skipper if (StringUtils.hasText(appName)) { String serviceName = formatServiceName(request, appName); // need to check if a versioned service exists to maintain backwards compat.. // version number itself isn't checked on as it could be different if create or upgrade // which we don't know at runtime.... List services = client.services().withLabel(SPRING_DEPLOYMENT_KEY).list().getItems(); for (Service service : services) { String serviceMetadataName = service.getMetadata().getName(); if (serviceMetadataName.startsWith(serviceName + "-v")) { return appId; } } return serviceName; } // no un-versioned app name provided, maybe not called from skipper, return whatever is in appId return appId; } private String formatServiceName(AppDeploymentRequest request, String appName) { String groupId = request.getDeploymentProperties().get(AppDeployer.GROUP_PROPERTY_KEY); String serviceName = groupId == null ? String.format("%s", appName) : String.format("%s-%s", groupId, appName); return serviceName.replace('.', '-').toLowerCase(Locale.ROOT); } private Set addAdditionalServicePorts(String additionalServicePorts) { Set ports = new HashSet<>(); String[] servicePorts = additionalServicePorts.split(","); for (String servicePort : servicePorts) { Integer port = Integer.parseInt(servicePort.trim()); ServicePort extraServicePort = new ServicePort(); extraServicePort.setPort(port); extraServicePort.setName("port-" + port); ports.add(extraServicePort); } return ports; } /** * For StatefulSets, create an init container to parse ${HOSTNAME} to get the `instance.index` and write it to * config/application.properties on a shared volume so the main container has it. Using the legacy annotation * configuration since the current client version does not directly support InitContainers. *

* Since 1.8 the annotation method has been removed, and the initContainer API is supported since 1.6 * * @param podSpec the current pod spec the container is being added to * @param imageName the image name to use in the init container * @return a container definition with the aforementioned configuration */ private Container createStatefulSetInitContainer(PodSpec podSpec, String imageName) { List command = new LinkedList<>(); String commandString = String .format("%s && %s", setIndexProperty("INSTANCE_INDEX"), setIndexProperty("spring.cloud.stream.instanceIndex")); command.add("sh"); command.add("-c"); command.add(commandString); ContainerBuilder containerBuilder = new ContainerBuilder().withName("index-provider") .withImage(imageName) .withImagePullPolicy("IfNotPresent") .withCommand(command) .withVolumeMounts(new VolumeMountBuilder().withName("config").withMountPath("/config").build()); if (!CollectionUtils.isEmpty(podSpec.getContainers())) { SecurityContext securityContext = podSpec.getContainers().get(0).getSecurityContext(); if (securityContext != null) { containerBuilder.withSecurityContext(securityContext); } } return containerBuilder.build(); } private String setIndexProperty(String name) { return String .format("echo %s=\"$(expr $HOSTNAME | grep -o \"[[:digit:]]*$\")\" >> "+ "/config/application.properties", name); } private void deleteAllObjects(String appIdToDelete) { Map labels = Collections.singletonMap(SPRING_APP_KEY, appIdToDelete); if (logger.isDebugEnabled()) { logger.debug(String.format("deleteAllObjects:%s:%s", appIdToDelete, labels)); } // waitForLoadBalancerReady(labels); // Not negative effect in not waiting for loadbalancer. deleteService(labels); deleteDeployment(labels); deleteStatefulSet(labels); deletePod(labels); deletePvc(labels); } private void deleteService(Map labels) { FilterWatchListDeletable servicesToDelete = client.services().withLabels(labels); if (servicesToDelete != null && servicesToDelete.list().getItems() != null) { boolean servicesDeleted = servicesToDelete.delete(); if (logger.isDebugEnabled()) { logger.debug(String.format("Service deleted for: %s - %b", labels, servicesDeleted)); } } } private void deleteDeployment(Map labels) { FilterWatchListDeletable deploymentsToDelete = client.apps().deployments().withLabels(labels); if (deploymentsToDelete != null && deploymentsToDelete.list().getItems() != null) { boolean deploymentsDeleted = deploymentsToDelete.delete(); if (logger.isDebugEnabled()) { logger.debug(String.format("Deployment deleted for: %s - %b", labels, deploymentsDeleted)); } } } private void deleteStatefulSet(Map labels) { FilterWatchListDeletable ssToDelete = client.apps().statefulSets().withLabels(labels); if (ssToDelete != null && ssToDelete.list().getItems() != null) { boolean ssDeleted = ssToDelete.delete(); if (logger.isDebugEnabled()) { logger.debug(String.format("StatefulSet deleted for: %s - %b", labels, ssDeleted)); } } } private void deletePod(Map labels) { FilterWatchListDeletable podsToDelete = client.pods() .withLabels(labels); if (podsToDelete != null && podsToDelete.list().getItems() != null) { boolean podsDeleted = podsToDelete.delete(); if (logger.isDebugEnabled()) { logger.debug(String.format("Pod deleted for: %s - %b", labels, podsDeleted)); } } } private void deletePvc(Map labels) { FilterWatchListDeletable pvcsToDelete = client.persistentVolumeClaims() .withLabels(labels); if (pvcsToDelete != null && pvcsToDelete.list().getItems() != null) { boolean pvcsDeleted = pvcsToDelete.delete(); if (logger.isDebugEnabled()) { logger.debug(String.format("PVC deleted for: %s - %b", labels, pvcsDeleted)); } } } private void waitForLoadBalancerReady(Map labels) { List services = client.services().withLabels(labels).list().getItems(); if (!services.isEmpty()) { Service svc = services.get(0); if (svc != null && "LoadBalancer".equals(svc.getSpec().getType())) { int tries = 0; int maxWait = properties.getMinutesToWaitForLoadBalancer() * 6; // we check 6 times per minute while (tries++ < maxWait) { if (svc.getStatus() != null && svc.getStatus().getLoadBalancer() != null && svc.getStatus().getLoadBalancer().getIngress() != null && svc.getStatus() .getLoadBalancer().getIngress().isEmpty()) { if (tries % 6 == 0) { logger.warn("Waiting for LoadBalancer to complete before deleting it ..."); } if (logger.isDebugEnabled()) { logger.debug(String.format("Waiting for LoadBalancer, try %d", tries)); } try { Thread.sleep(10000L); } catch (InterruptedException e) { } svc = client.services().withLabels(labels).list().getItems().get(0); } else { break; } } if (logger.isDebugEnabled()) { logger.debug(String.format("LoadBalancer Ingress: %s", svc.getStatus().getLoadBalancer().getIngress().toString())); } } } } } ================================================ FILE: src/main/java/org/springframework/cloud/deployer/spi/kubernetes/KubernetesAppInstanceStatus.java ================================================ /* * Copyright 2015-2022 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.cloud.deployer.spi.kubernetes; import java.nio.file.Paths; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import io.fabric8.kubernetes.api.model.Container; import io.fabric8.kubernetes.api.model.ContainerStatus; import io.fabric8.kubernetes.api.model.IntOrString; import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.ServicePort; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.cloud.deployer.spi.app.AppInstanceStatus; import org.springframework.cloud.deployer.spi.app.DeploymentState; /** * Represents the status of a module. * * @author Florian Rosenberg * @author Thomas Risberg * @author David Turanski * @author Chris Schaefer */ public class KubernetesAppInstanceStatus implements AppInstanceStatus { private static Log logger = LogFactory.getLog(KubernetesAppInstanceStatus.class); private final Pod pod; private final Service service; private final KubernetesDeployerProperties properties; private ContainerStatus containerStatus; private RunningPhaseDeploymentStateResolver runningPhaseDeploymentStateResolver; @Deprecated public KubernetesAppInstanceStatus(Pod pod, Service service, KubernetesDeployerProperties properties) { this.pod = pod; this.service = service; this.properties = properties; // we assume one container per pod if (pod != null && pod.getStatus().getContainerStatuses().size() == 1) { this.containerStatus = pod.getStatus().getContainerStatuses().get(0); } else { this.containerStatus = null; } this.runningPhaseDeploymentStateResolver = new DefaultRunningPhaseDeploymentStateResolver(properties); } public KubernetesAppInstanceStatus(Pod pod, Service service, KubernetesDeployerProperties properties, ContainerStatus containerStatus) { this.pod = pod; this.service = service; this.properties = properties; this.containerStatus = containerStatus; this.runningPhaseDeploymentStateResolver = new DefaultRunningPhaseDeploymentStateResolver(properties); } /** * Override the default {@link RunningPhaseDeploymentStateResolver} implementation. * * @param runningPhaseDeploymentStateResolver the * {@link RunningPhaseDeploymentStateResolver} to use */ public void setRunningPhaseDeploymentStateResolver( RunningPhaseDeploymentStateResolver runningPhaseDeploymentStateResolver) { this.runningPhaseDeploymentStateResolver = runningPhaseDeploymentStateResolver; } @Override public String getId() { return pod == null ? "N/A" : pod.getMetadata().getName(); } @Override public DeploymentState getState() { return pod != null && containerStatus != null ? mapState() : DeploymentState.unknown; } /** * Maps Kubernetes phases/states onto Spring Cloud Deployer states */ private DeploymentState mapState() { logger.debug(String.format("%s - Phase [ %s ]", pod.getMetadata().getName(), pod.getStatus().getPhase())); logger.debug(String.format("%s - ContainerStatus [ %s ]", pod.getMetadata().getName(), containerStatus)); switch (pod.getStatus().getPhase()) { case "Pending": return DeploymentState.deploying; // We only report a module as running if the container is also ready to service requests. // We also implement the Readiness check as part of the container to ensure ready means // that the module is up and running and not only that the JVM has been created and the // Spring module is still starting up case "Running": // we assume we only have one container return runningPhaseDeploymentStateResolver.resolve(containerStatus); case "Failed": return DeploymentState.failed; case "Unknown": return DeploymentState.unknown; default: return DeploymentState.unknown; } } @Override public Map getAttributes() { ConcurrentHashMap result = new ConcurrentHashMap<>(); if (pod != null) { result.put("pod.name", pod.getMetadata().getName()); result.put("pod.startTime", nullSafe(pod.getStatus().getStartTime())); result.put("pod.ip", nullSafe(pod.getStatus().getPodIP())); result.put("actuator.path", determineActuatorPathFromLivenessProbe(pod)); result.put("actuator.port", determineActuatorPortFromLivenessProbe(pod, result.get("actuator.path"))); result.put("host.ip", nullSafe(pod.getStatus().getHostIP())); result.put("phase", nullSafe(pod.getStatus().getPhase())); result.put(AbstractKubernetesDeployer.SPRING_APP_KEY.replace('-', '.'), pod.getMetadata().getLabels().get(AbstractKubernetesDeployer.SPRING_APP_KEY)); result.put(AbstractKubernetesDeployer.SPRING_DEPLOYMENT_KEY.replace('-', '.'), pod.getMetadata().getLabels().get(AbstractKubernetesDeployer.SPRING_DEPLOYMENT_KEY)); result.put("guid", pod.getMetadata().getUid()); } else { logger.debug("getAttributes:no pod"); } if (service != null) { result.put("service.name", service.getMetadata().getName()); if ("LoadBalancer".equals(service.getSpec().getType())) { if (service.getStatus() != null && service.getStatus().getLoadBalancer() != null && service.getStatus().getLoadBalancer().getIngress() != null && !service.getStatus() .getLoadBalancer().getIngress().isEmpty()) { String externalIp = service.getStatus().getLoadBalancer().getIngress().get(0).getIp(); if (externalIp == null) { externalIp = service.getStatus().getLoadBalancer().getIngress().get(0).getHostname(); } result.put("service.external.ip", externalIp); List ports = service.getSpec().getPorts(); int port = 0; if (ports != null && ports.size() > 0) { port = ports.get(0).getPort(); result.put("service.external.port", String.valueOf(port)); } if (externalIp != null) { result.put("url", "http://" + externalIp + (port > 0 && port != 80 ? ":" + port : "")); } } } } else { logger.debug("getAttributes:no service"); } if (containerStatus != null) { result.put("container.restartCount", "" + containerStatus.getRestartCount()); if (containerStatus.getLastState() != null && containerStatus.getLastState().getTerminated() != null) { result.put("container.lastState.terminated.exitCode", "" + containerStatus.getLastState().getTerminated().getExitCode()); result.put("container.lastState.terminated.reason", containerStatus.getLastState().getTerminated().getReason()); } if (containerStatus.getState() != null && containerStatus.getState().getTerminated() != null) { result.put("container.state.terminated.exitCode", "" + containerStatus.getState().getTerminated().getExitCode()); result.put("container.state.terminated.reason", containerStatus.getState().getTerminated().getReason()); } } else { logger.debug("getAttributes:no containerStatus"); } if (logger.isDebugEnabled()) { logger.debug("getAttributes:" + result); } return result; } private String nullSafe(String value) { return value == null ? "" : value; } private String determineActuatorPathFromLivenessProbe(Pod pod) { return pod.getSpec().getContainers().stream() .filter((Container container) -> container.getLivenessProbe() != null && container.getLivenessProbe().getHttpGet() != null) .findFirst() .map(container -> Paths.get(container.getLivenessProbe().getHttpGet().getPath()).getParent().toString()) .orElse("/actuator"); } private String determineActuatorPortFromLivenessProbe(Pod pod, String path) { IntOrString intOrString = pod.getSpec().getContainers().stream() .filter(container -> container.getLivenessProbe() != null && container.getLivenessProbe().getHttpGet() != null && container.getLivenessProbe().getHttpGet().getPath().equals(path)) .findFirst() .map(container -> container.getLivenessProbe().getHttpGet().getPort()) .orElse(new IntOrString(8080)); return intOrString.getIntVal() != null ? String.valueOf(intOrString.getIntVal()) : intOrString.getStrVal(); } } ================================================ FILE: src/main/java/org/springframework/cloud/deployer/spi/kubernetes/KubernetesDeployerProperties.java ================================================ /* * Copyright 2015-2023 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.cloud.deployer.spi.kubernetes; import java.util.ArrayList; import java.util.List; import io.fabric8.kubernetes.api.model.NodeAffinity; import io.fabric8.kubernetes.api.model.PodAffinity; import io.fabric8.kubernetes.api.model.PodAntiAffinity; import io.fabric8.kubernetes.api.model.Volume; import io.fabric8.kubernetes.api.model.VolumeMount; import io.fabric8.kubernetes.client.Config; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.NestedConfigurationProperty; import org.springframework.cloud.deployer.spi.app.AppAdmin; /** * @author Florian Rosenberg * @author Thomas Risberg * @author Donovan Muller * @author Ilayaperumal Gopinathan * @author Leonardo Diniz * @author Chris Schaefer * @author David Turanski * @author Enrique Medina Montenegro * @author Chris Bono * @author Corneil du Plessis */ @ConfigurationProperties(prefix = KubernetesDeployerProperties.KUBERNETES_DEPLOYER_PROPERTIES_PREFIX) public class KubernetesDeployerProperties { static final String KUBERNETES_DEPLOYER_PROPERTIES_PREFIX = "spring.cloud.deployer.kubernetes"; /** * Constants for app deployment properties that don't have a deployer level default * property. */ static final String KUBERNETES_DEPLOYMENT_NODE_SELECTOR = "spring.cloud.deployer.kubernetes.deployment.nodeSelector"; /** * The maximum concurrent tasks allowed for this platform instance. */ private int maximumConcurrentTasks = 20; @NestedConfigurationProperty private Config fabric8 = Config.autoConfigure(null); public Config getFabric8() { return this.fabric8; } public void setFabric8(Config fabric8) { this.fabric8 = fabric8; } /** * Encapsulates resources for Kubernetes Container resource limits */ public static class LimitsResources { /** * Container resource cpu limit. */ private String cpu; /** * Container resource memory limit. */ private String memory; /** * Container resource ephemeral storage size limit. */ private String ephemeralStorage; /** * Container resource hugepages-2Mi limit. */ private String hugepages2Mi; /** * Container resource hugepages-1Gi limit. */ private String hugepages1Gi; /** * Container GPU vendor name for limit * If gpuVendor=nvidia.com/gpu and gpuCount=2 then the following will be used * {@code * limits: * nvidia.com/gpu: 2 * } */ private String gpuVendor; /** * Container GPU count for limit. */ private String gpuCount; public LimitsResources() { } /** * 'All' args constructor * * @param cpu Container resource cpu limit * @param memory Container resource memory limit * @deprecated This method should no longer be used to set all fields at construct time. *

* Use the default constructor and set() methods instead. */ @Deprecated public LimitsResources(String cpu, String memory) { this.cpu = cpu; this.memory = memory; } @Deprecated public LimitsResources(String cpu, String memory, String ephemeralStorage, String hugepages2Mi, String hugepages1Gi, String gpuVendor, String gpuCount) { this.cpu = cpu; this.memory = memory; this.ephemeralStorage = ephemeralStorage; this.hugepages2Mi = hugepages2Mi; this.hugepages1Gi = hugepages1Gi; this.gpuVendor = gpuVendor; this.gpuCount = gpuCount; } public String getCpu() { return cpu; } public void setCpu(String cpu) { this.cpu = cpu; } public String getMemory() { return memory; } public void setMemory(String memory) { this.memory = memory; } public String getEphemeralStorage() { return ephemeralStorage; } public void setEphemeralStorage(String ephemeralStorage) { this.ephemeralStorage = ephemeralStorage; } public String getHugepages2Mi() { return hugepages2Mi; } public void setHugepages2Mi(String hugepages2Mi) { this.hugepages2Mi = hugepages2Mi; } public String getHugepages1Gi() { return hugepages1Gi; } public void setHugepages1Gi(String hugepages1Gi) { this.hugepages1Gi = hugepages1Gi; } public String getGpuVendor() { return gpuVendor; } public void setGpuVendor(String gpuVendor) { this.gpuVendor = gpuVendor; } public String getGpuCount() { return gpuCount; } public void setGpuCount(String gpuCount) { this.gpuCount = gpuCount; } } /** * Encapsulates resources for Kubernetes Container resource requests */ public static class RequestsResources { /** * Container request limit. */ private String cpu; /** * Container memory limit. */ private String memory; /** * Container resource ephemeral storage size request. */ private String ephemeralStorage; /** * Container resource hugepages-2Mi request. */ private String hugepages2Mi; /** * Container resource hugepages-1Gi request. */ private String hugepages1Gi; public RequestsResources() { } public RequestsResources(String cpu, String memory) { this.cpu = cpu; this.memory = memory; } public RequestsResources(String cpu, String memory, String ephemeralStorage, String hugepages2Mi, String hugepages1Gi) { this.cpu = cpu; this.memory = memory; this.ephemeralStorage = ephemeralStorage; this.hugepages2Mi = hugepages2Mi; this.hugepages1Gi = hugepages1Gi; } public String getCpu() { return cpu; } public void setCpu(String cpu) { this.cpu = cpu; } public String getMemory() { return memory; } public void setMemory(String memory) { this.memory = memory; } public String getEphemeralStorage() { return ephemeralStorage; } public void setEphemeralStorage(String ephemeralStorage) { this.ephemeralStorage = ephemeralStorage; } public String getHugepages2Mi() { return hugepages2Mi; } public void setHugepages2Mi(String hugepages2Mi) { this.hugepages2Mi = hugepages2Mi; } public String getHugepages1Gi() { return hugepages1Gi; } public void setHugepages1Gi(String hugepages1Gi) { this.hugepages1Gi = hugepages1Gi; } } public static class StatefulSet { private VolumeClaimTemplate volumeClaimTemplate = new VolumeClaimTemplate(); public VolumeClaimTemplate getVolumeClaimTemplate() { return volumeClaimTemplate; } public void setVolumeClaimTemplate(VolumeClaimTemplate volumeClaimTemplate) { this.volumeClaimTemplate = volumeClaimTemplate; } public static class VolumeClaimTemplate { /** * VolumeClaimTemplate name */ private String name; /** * VolumeClaimTemplate storage. */ private String storage = "10m"; /** * VolumeClaimTemplate storage class name. */ private String storageClassName; public String getName() { return this.name; } public void setName(String name) { this.name = name; } public String getStorage() { return storage; } public void setStorage(String storage) { this.storage = storage; } public String getStorageClassName() { return storageClassName; } public void setStorageClassName(String storageClassName) { this.storageClassName = storageClassName; } } } public static class Toleration { private String effect; private String key; private String operator; private Long tolerationSeconds; private String value; public String getEffect() { return effect; } public void setEffect(String effect) { this.effect = effect; } public String getKey() { return key; } public void setKey(String key) { this.key = key; } public String getOperator() { return operator; } public void setOperator(String operator) { this.operator = operator; } public Long getTolerationSeconds() { return tolerationSeconds; } public void setTolerationSeconds(Long tolerationSeconds) { this.tolerationSeconds = tolerationSeconds; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } } static class KeyRef { private String envVarName; private String dataKey; public void setEnvVarName(String envVarName) { this.envVarName = envVarName; } public String getEnvVarName() { return envVarName; } public void setDataKey(String dataKey) { this.dataKey = dataKey; } public String getDataKey() { return dataKey; } } public static class SecretKeyRef extends KeyRef { private String secretName; public void setSecretName(String secretName) { this.secretName = secretName; } public String getSecretName() { return secretName; } } public static class ConfigMapKeyRef extends KeyRef { private String configMapName; public void setConfigMapName(String configMapName) { this.configMapName = configMapName; } public String getConfigMapName() { return configMapName; } } public static class PodSecurityContext { /** * The numeric user ID to run pod container processes under */ private Long runAsUser; /** * The numeric group id to run the entrypoint of the container process */ private Long runAsGroup; /** * Indicates that the container must run as a non-root user */ private Boolean runAsNonRoot; /** * The numeric group ID for the volumes of the pod */ private Long fsGroup; /** * Defines behavior of changing ownership and permission of the volume before being * exposed inside pod (only applies to volume types which support fsGroup based * ownership and permissions) - possible values are "OnRootMismatch", "Always" */ private String fsGroupChangePolicy; /** * The numeric group IDs applied to the pod container processes, in addition to the container's primary group ID */ private Long[] supplementalGroups; /** * The seccomp options to use for the pod containers */ private SeccompProfile seccompProfile; /** * The SELinux context to be applied to the pod containers */ private SELinuxOptions seLinuxOptions; /** * List of namespaced sysctls used for the pod (not used when spec.os.name is windows). */ private List sysctls; /** * The Windows specific settings applied to all containers. */ private WindowsSecurityContextOptions windowsOptions; public void setRunAsUser(Long runAsUser) { this.runAsUser = runAsUser; } public Long getRunAsUser() { return this.runAsUser; } public Long getRunAsGroup() { return runAsGroup; } public void setRunAsGroup(Long runAsGroup) { this.runAsGroup = runAsGroup; } public Boolean getRunAsNonRoot() { return runAsNonRoot; } public void setRunAsNonRoot(Boolean runAsNonRoot) { this.runAsNonRoot = runAsNonRoot; } public void setFsGroup(Long fsGroup) { this.fsGroup = fsGroup; } public Long getFsGroup() { return fsGroup; } public String getFsGroupChangePolicy() { return fsGroupChangePolicy; } public void setFsGroupChangePolicy(String fsGroupChangePolicy) { this.fsGroupChangePolicy = fsGroupChangePolicy; } public void setSupplementalGroups(Long[] supplementalGroups) { this.supplementalGroups = supplementalGroups; } public Long[] getSupplementalGroups() { return supplementalGroups; } public SeccompProfile getSeccompProfile() { return seccompProfile; } public void setSeccompProfile(SeccompProfile seccompProfile) { this.seccompProfile = seccompProfile; } public SELinuxOptions getSeLinuxOptions() { return seLinuxOptions; } public void setSeLinuxOptions(SELinuxOptions seLinuxOptions) { this.seLinuxOptions = seLinuxOptions; } public List getSysctls() { return sysctls; } public void setSysctls(List sysctls) { this.sysctls = sysctls; } public WindowsSecurityContextOptions getWindowsOptions() { return windowsOptions; } public void setWindowsOptions(WindowsSecurityContextOptions windowsOptions) { this.windowsOptions = windowsOptions; } } public static class ContainerSecurityContext { /** * Whether a process can gain more privileges than its parent process */ private Boolean allowPrivilegeEscalation; /** * The capabilities to add/drop when running the container (cannot be set when spec.os.name is windows) */ private Capabilities capabilities; /** * Run container in privileged mode. */ private Boolean privileged; /** * The type of proc mount to use for the container (cannot be set when spec.os.name is windows) */ private String procMount; /** * Mounts the container's root filesystem as read-only */ private Boolean readOnlyRootFilesystem; /** * The numeric user ID to run pod container processes under */ private Long runAsUser; /** * The numeric group id to run the entrypoint of the container process */ private Long runAsGroup; /** * Indicates that the container must run as a non-root user */ private Boolean runAsNonRoot; /** * The seccomp options to use for the container */ private SeccompProfile seccompProfile; /** * The SELinux context to be applied to the container. */ private SELinuxOptions seLinuxOptions; /** * The Windows specific settings applied to the container. */ private WindowsSecurityContextOptions windowsOptions; public Boolean getAllowPrivilegeEscalation() { return allowPrivilegeEscalation; } public void setAllowPrivilegeEscalation(Boolean allowPrivilegeEscalation) { this.allowPrivilegeEscalation = allowPrivilegeEscalation; } public Capabilities getCapabilities() { return capabilities; } public void setCapabilities(Capabilities capabilities) { this.capabilities = capabilities; } public Boolean getPrivileged() { return privileged; } public void setPrivileged(Boolean privileged) { this.privileged = privileged; } public String getProcMount() { return procMount; } public void setProcMount(String procMount) { this.procMount = procMount; } public Boolean getReadOnlyRootFilesystem() { return readOnlyRootFilesystem; } public void setReadOnlyRootFilesystem(Boolean readOnlyRootFilesystem) { this.readOnlyRootFilesystem = readOnlyRootFilesystem; } public Long getRunAsUser() { return runAsUser; } public void setRunAsUser(Long runAsUser) { this.runAsUser = runAsUser; } public Long getRunAsGroup() { return runAsGroup; } public void setRunAsGroup(Long runAsGroup) { this.runAsGroup = runAsGroup; } public Boolean getRunAsNonRoot() { return runAsNonRoot; } public void setRunAsNonRoot(Boolean runAsNonRoot) { this.runAsNonRoot = runAsNonRoot; } public SeccompProfile getSeccompProfile() { return seccompProfile; } public void setSeccompProfile(SeccompProfile seccompProfile) { this.seccompProfile = seccompProfile; } public SELinuxOptions getSeLinuxOptions() { return seLinuxOptions; } public void setSeLinuxOptions(SELinuxOptions seLinuxOptions) { this.seLinuxOptions = seLinuxOptions; } public WindowsSecurityContextOptions getWindowsOptions() { return windowsOptions; } public void setWindowsOptions(WindowsSecurityContextOptions windowsOptions) { this.windowsOptions = windowsOptions; } } /** * Adds and removes POSIX capabilities from running containers. */ public static class Capabilities { /** * Added capabilities. */ private List add; /** * Removed capabilities. */ private List drop; public List getAdd() { return add; } public void setAdd(List add) { this.add = add; } public List getDrop() { return drop; } public void setDrop(List drop) { this.drop = drop; } } /** * Defines a pod seccomp profile settings. */ public static class SeccompProfile { /** * Type of seccomp profile. */ private String type; /** * Path of the pre-configured profile on the node, relative to the kubelet's configured Seccomp profile location, only valid when type is "Localhost". */ private String localhostProfile; public String getType() { return type; } public void setType(String type) { this.type = type; } public String getLocalhostProfile() { return localhostProfile; } public void setLocalhostProfile(String localhostProfile) { this.localhostProfile = localhostProfile; } } /** * The labels to be applied to the container. */ public static class SELinuxOptions { /** * Level label applied to the container */ private String level; /** * Role Level label applied to the container */ private String role; /** * Type label applied to the container */ private String type; /** * User label applied to the container */ private String user; public String getLevel() { return level; } public void setLevel(String level) { this.level = level; } public String getRole() { return role; } public void setRole(String role) { this.role = role; } public String getType() { return type; } public void setType(String type) { this.type = type; } public String getUser() { return user; } public void setUser(String user) { this.user = user; } } /** * Sysctl defines a kernel parameter to be set on the pod. */ public static class SysctlInfo { /** * Name of the property */ private String name; /** * Value of the property */ private String value; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } } /** * Contains Windows-specific options and credentials. */ public static class WindowsSecurityContextOptions { /** * Where the GMSA admission webhook inlines the contents of the GMSA credential spec * named by the GMSACredentialSpecName field. */ private String gmsaCredentialSpec; /** * The name of the GMSA credential spec to use. */ private String gmsaCredentialSpecName; /** * Whether a container should be run as a 'Host Process' container. */ private Boolean hostProcess; /** * The username in Windows to run the entrypoint of the container process. */ private String runAsUserName; public String getGmsaCredentialSpec() { return gmsaCredentialSpec; } public void setGmsaCredentialSpec(String gmsaCredentialSpec) { this.gmsaCredentialSpec = gmsaCredentialSpec; } public String getGmsaCredentialSpecName() { return gmsaCredentialSpecName; } public void setGmsaCredentialSpecName(String gmsaCredentialSpecName) { this.gmsaCredentialSpecName = gmsaCredentialSpecName; } public Boolean getHostProcess() { return hostProcess; } public void setHostProcess(Boolean hostProcess) { this.hostProcess = hostProcess; } public String getRunAsUserName() { return runAsUserName; } public void setRunAsUserName(String runAsUserName) { this.runAsUserName = runAsUserName; } } public static class Lifecycle { private Hook postStart; private Hook preStop; Hook getPreStop() { return preStop; } Hook getPostStart() { return postStart; } void setPostStart(Hook postStart) { this.postStart = postStart; } void setPreStop(Hook preStop) { this.preStop = preStop; } public static class Hook { private Exec exec; Exec getExec() { return exec; } void setExec(Exec exec) { this.exec = exec; } } public static class Exec { private List command; List getCommand() { return command; } void setCommand(List command) { this.command = command; } } } public static class InitContainer extends ContainerProperties { } static class Container extends io.fabric8.kubernetes.api.model.Container { } public static class ContainerProperties { private String imageName; private String containerName; private List commands; private List volumeMounts; /** * Environment variables to set for any deployed init container. */ private String[] environmentVariables = new String[]{}; public String getImageName() { return imageName; } public void setImageName(String imageName) { this.imageName = imageName; } public String getContainerName() { return containerName; } public void setContainerName(String containerName) { this.containerName = containerName; } public List getCommands() { return commands; } public void setCommands(List commands) { this.commands = commands; } public List getVolumeMounts() { return volumeMounts; } public void setVolumeMounts(List volumeMounts) { this.volumeMounts = volumeMounts; } public String[] getEnvironmentVariables() { return environmentVariables; } public void setEnvironmentVariables(String[] environmentVariables) { this.environmentVariables = environmentVariables; } } /** * The {@link RestartPolicy} to use. Defaults to {@link RestartPolicy#Always}. */ private RestartPolicy restartPolicy = RestartPolicy.Always; /** * The default service account name to use for tasks. */ protected static final String DEFAULT_TASK_SERVICE_ACCOUNT_NAME = "default"; /** * Service account name to use for tasks, defaults to: * {@link #DEFAULT_TASK_SERVICE_ACCOUNT_NAME} */ private String taskServiceAccountName = DEFAULT_TASK_SERVICE_ACCOUNT_NAME; /** * Obtains the {@link RestartPolicy} to use. Defaults to * {@link #restartPolicy}. * * @return the {@link RestartPolicy} to use */ public RestartPolicy getRestartPolicy() { return restartPolicy; } /** * Sets the {@link RestartPolicy} to use. * * @param restartPolicy the {@link RestartPolicy} to use */ public void setRestartPolicy(RestartPolicy restartPolicy) { this.restartPolicy = restartPolicy; } /** * Obtains the service account name to use for tasks. * * @return the service account name */ public String getTaskServiceAccountName() { return taskServiceAccountName; } /** * Sets the service account name to use for tasks. * * @param taskServiceAccountName the service account name */ public void setTaskServiceAccountName(String taskServiceAccountName) { this.taskServiceAccountName = taskServiceAccountName; } /** * Name of the environment variable that can define the Kubernetes namespace to use. */ public static final String ENV_KEY_KUBERNETES_NAMESPACE = "KUBERNETES_NAMESPACE"; private static String KUBERNETES_NAMESPACE = System.getenv("KUBERNETES_NAMESPACE"); /** * Namespace to use. */ private String namespace = KUBERNETES_NAMESPACE; /** * Secrets for a access a private registry to pull images. */ private String imagePullSecret; /** * List of Secrets for a access a private registry to pull images. */ private List imagePullSecrets; /** * Delay in seconds when the Kubernetes liveness check of the app container should start * checking its health status. */ private int livenessHttpProbeDelay = 1; /** * When the probe fails more times than the failure value the pod is restarted. */ private int livenessHttpProbeFailure = 3; /** * When the probe passes success times it is considered live. */ private int livenessHttpProbeSuccess = 1; /** * Period in seconds for performing the Kubernetes liveness check of the app container. */ private int livenessHttpProbePeriod = 60; /** * Timeout in seconds for the check to wait for a response after which it is considered a failed check. */ private int livenessHttpProbeTimeout = 5; /** * Timeout in seconds for the liveness check to wait for a response before the check is considered a failure. */ private int livenessTcpProbeTimeout = 5; /** * Path that app container has to respond to for liveness check. */ private String livenessHttpProbePath; /** * Port that app container has to respond on for liveness check. */ private Integer livenessHttpProbePort = null; /** * Schema that app container has to respond on for liveness check. */ private String livenessHttpProbeScheme = "HTTP"; /** * Schema that app container has to respond to for readiness check. */ private String readinessHttpProbeScheme = "HTTP"; /** * Schema that app container has to respond to for startup check. */ private String startupProbeScheme = "HTTP"; /** * If present will assign to spec.shareProcessNamespace of the Pod. */ private Boolean shareProcessNamespace; /** * Delay in seconds when the readiness check of the app container should start checking if * the module is fully up and running. */ private int readinessHttpProbeDelay = 1; private int readinessHttpProbeSuccess = 1; private int readinessHttpProbeFailure = 3; private int readinessTcpProbeTimeout = 3; private int readinessTcpProbeFailure = 3; private int readinessTcpProbeSuccess = 1; /** * Delay in seconds when the startup check of the app container should start checking if * the module is fully up and running. */ private int startupHttpProbeDelay = 30; /** * The number of time the http startup probe will be allowed to fail before restarting the pod. */ private int startupHttpProbeFailure = 20; /** * The number of time the http startup probe will be required to pass before considering the application started. */ private int startupHttpProbeSuccess = 1; /** * The number of time the tcp startup probe will be allowed to fail before restarting the pod. */ private int startupTcpProbeFailure = 20; /** * The number of time the tcp startup probe will be required to pass before considering the application started. */ private int startupTcpProbeSuccess = 1; /** * The number of time the command startup probe will be allowed to fail before restarting the pod. */ private int startupCommandProbeFailure = 10; /** * The number of time the command startup probe will be required to pass before considering the application started. */ private int startupCommandProbeSuccess = 1; /** * Period in seconds to perform the readiness check of the app container. */ private int readinessHttpProbePeriod = 10; /** * Period in seconds to perform the startup check of the app container. */ private int startupHttpProbePeriod = 3; /** * Timeout in seconds that the app container has to respond to its status during * the startup check. */ private int startupHttpProbeTimeout = 5; /** * Timeout in seconds that the app container has to respond to its health status during * the readiness check. */ private int readinessHttpProbeTimeout = 5; private int readinessCommandProbeFailure = 3; private int readinessCommandProbeSuccess = 1; /** * Path that app container has to respond to for readiness check. */ private String readinessHttpProbePath; /** * Path that app container has to respond to for startup check. */ private String startupHttpProbePath; /** * Port that app container has to respond on for readiness check. */ private Integer readinessHttpProbePort = null; /** * Port that app container has to respond on for startup check. */ private Integer startupHttpProbePort = null; /** * Delay in seconds when the liveness TCP check should start checking */ private int livenessTcpProbeDelay = 10; private int livenessTcpProbeSuccess = 1; private int livenessTcpProbeFailure = 3; /** * Period in seconds to perform the liveness TCP check */ private int livenessTcpProbePeriod = 60; /** * The TCP port the liveness probe should check */ private Integer livenessTcpProbePort = null; /** * Delay in seconds when the readiness TCP check should start checking */ private int readinessTcpProbeDelay = 1; /** * Period in seconds to perform the readiness TCP check */ private int readinessTcpProbePeriod = 10; /** * The TCP port the readiness probe should check */ private Integer readinessTcpProbePort = null; /** * Delay in seconds when the readiness command check should start checking */ private int readinessCommandProbeDelay = 1; /** * Period in seconds to perform the readiness command check */ private int readinessCommandProbePeriod = 10; /** * The command the readiness probe should use to check */ private String readinessCommandProbeCommand = null; /** * Delay in seconds when the readiness TCP check should start checking */ private int startupTcpProbeDelay = 30; private int startupTcpProbeTimeout = 5; /** * Period in seconds to perform the readiness TCP check */ private int startupTcpProbePeriod = 3; /** * The TCP port the readiness probe should check */ private Integer startupTcpProbePort = null; /** * Delay in seconds when the readiness command check should start checking */ private int startupCommandProbeDelay = 30; /** * Period in seconds to perform the readiness command check */ private int startupCommandProbePeriod = 10; /** * The command the readiness probe should use to check */ private String startupCommandProbeCommand = null; /** * Delay in seconds when the liveness command check should start checking */ private int livenessCommandProbeDelay = 10; private int livenessCommandProbeFailure = 3; private int livenessCommandProbeSuccess = 1; /** * Period in seconds to perform the liveness command check */ private int livenessCommandProbePeriod = 10; /** * The command the liveness probe should use to check */ private String livenessCommandProbeCommand = null; /** * The secret name containing the credentials to use when accessing secured probe * endpoints. */ private String probeCredentialsSecret; /** * The probe type to use when doing health checks. Defaults to HTTP. */ private ProbeType probeType = ProbeType.HTTP; /** * Memory and CPU limits (i.e. maximum needed values) to allocate for a Pod. */ private LimitsResources limits = new LimitsResources(); /** * Memory and CPU requests (i.e. guaranteed needed values) to allocate for a Pod. */ private RequestsResources requests = new RequestsResources(); /** * Tolerations to allocate for a Pod. */ private List tolerations = new ArrayList<>(); /** * Secret key references to be added to the Pod environment. */ private List secretKeyRefs = new ArrayList<>(); /** * ConfigMap key references to be added to the Pod environment. */ private List configMapKeyRefs = new ArrayList<>(); /** * ConfigMap references to be added to the Pod environment. */ private List configMapRefs = new ArrayList<>(); /** * Secret references to be added to the Pod environment. */ private List secretRefs = new ArrayList<>(); /** * Resources to assign for VolumeClaimTemplates (identified by metadata name) inside * StatefulSet. */ private StatefulSet statefulSet = new StatefulSet(); /** * Environment variables to set for any deployed app container. To be used for service * binding. */ private String[] environmentVariables = new String[]{}; /** * Entry point style used for the Docker image. To be used to determine how to pass in * properties. */ private EntryPointStyle entryPointStyle = EntryPointStyle.exec; /** * Create a "LoadBalancer" for the service created for each app. This facilitates * assignment of external IP to app. */ private boolean createLoadBalancer = false; /** * Service annotations to set for the service created for each app. */ private String serviceAnnotations = null; /** * Pod annotations to set for the pod created for each deployment. */ private String podAnnotations; /** * Job annotations to set for the pod or job created for a job. */ private String jobAnnotations; /** * Time to wait for load balancer to be available before attempting delete of service (in * minutes). */ private int minutesToWaitForLoadBalancer = 5; /** * Maximum allowed restarts for app that fails due to an error or excessive resource use. */ private int maxTerminatedErrorRestarts = 2; /** * Maximum allowed restarts for app that is in a CrashLoopBackOff. */ private int maxCrashLoopBackOffRestarts = 4; /** * The image pull policy to use for Pod deployments in Kubernetes. */ private ImagePullPolicy imagePullPolicy = ImagePullPolicy.IfNotPresent; /** * Volume mounts that a container is requesting. This can be specified as a deployer * property or as an app deployment property. Deployment properties will override deployer * properties. */ private List volumeMounts = new ArrayList<>(); /** * The volumes that a Kubernetes instance supports. See * https://kubernetes.io/docs/user-guide/volumes/#types-of-volumes This can be specified * as a deployer property or as an app deployment property. Deployment properties will * override deployer properties. */ private List volumes = new ArrayList<>(); /** * The hostNetwork setting for the deployments. See * https://kubernetes.io/docs/api-reference/v1/definitions/#_v1_podspec This can be * specified as a deployer property or as an app deployment property. Deployment * properties will override deployer properties. */ private boolean hostNetwork = false; /** * Create a "Job" instead of just a "Pod" when launching tasks. See * https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/ */ private boolean createJob = false; /** * The node selector to use in key:value format, comma separated */ private String nodeSelector; /** * Service account name to use for app deployments */ private String deploymentServiceAccountName; /** * The security context to apply to created pod's. */ private PodSecurityContext podSecurityContext; /** * The security context to apply to created pod's main container. */ private ContainerSecurityContext containerSecurityContext; /** * The node affinity rules to apply. */ private NodeAffinity nodeAffinity; /** * The pod affinity rules to apply */ private PodAffinity podAffinity; /** * The pod anti-affinity rules to apply */ private PodAntiAffinity podAntiAffinity; /** * A custom init container image name to use when creating a StatefulSet */ private String statefulSetInitContainerImageName; /** * A custom init container to apply. */ private InitContainer initContainer; /** * Lifecycle spec to apply. */ private Lifecycle lifecycle = new Lifecycle(); /** * The additional containers one can add to the main application container. */ private List additionalContainers; /** * Deployment label to be applied to Deployment, StatefulSet, JobSpec etc., */ private String deploymentLabels; /** * Pod priorityClassName. The user should create a PriorityClass before setting this property. */ private String priorityClassName; private AppAdmin appAdmin = new AppAdmin(); public String getNamespace() { return namespace; } public void setNamespace(String namespace) { this.namespace = namespace; } public String getImagePullSecret() { return imagePullSecret; } public void setImagePullSecret(String imagePullSecret) { this.imagePullSecret = imagePullSecret; } public List getImagePullSecrets() { return imagePullSecrets; } public void setImagePullSecrets(List imagePullSecrets) { this.imagePullSecrets = imagePullSecrets; } public static class CronConfig { private String concurrencyPolicy; private Integer ttlSecondsAfterFinished; public String getConcurrencyPolicy() { return concurrencyPolicy; } public void setConcurrencyPolicy(String concurrencyPolicy) { this.concurrencyPolicy = concurrencyPolicy; } public Integer getTtlSecondsAfterFinished() { return ttlSecondsAfterFinished; } public void setTtlSecondsAfterFinished(Integer ttlSecondsAfterFinished) { this.ttlSecondsAfterFinished = ttlSecondsAfterFinished; } } public Boolean getShareProcessNamespace() { return shareProcessNamespace; } public void setShareProcessNamespace(Boolean shareProcessNamespace) { this.shareProcessNamespace = shareProcessNamespace; } public String getPriorityClassName() { return priorityClassName; } public void setPriorityClassName(String priorityClassName) { this.priorityClassName = priorityClassName; } /** * @deprecated @{see {@link #getLivenessHttpProbeDelay()}} */ @Deprecated public int getLivenessProbeDelay() { return livenessHttpProbeDelay; } /** * @deprecated @{see {@link #setLivenessHttpProbeDelay(int)}} */ @Deprecated public void setLivenessProbeDelay(int livenessProbeDelay) { this.livenessHttpProbeDelay = livenessProbeDelay; } /** * @deprecated @{see {@link #getLivenessHttpProbePeriod()}} */ @Deprecated public int getLivenessProbePeriod() { return livenessHttpProbePeriod; } /** * @deprecated @{see {@link #setLivenessHttpProbePeriod(int)}} */ @Deprecated public void setLivenessProbePeriod(int livenessProbePeriod) { this.livenessHttpProbePeriod = livenessProbePeriod; } /** * @deprecated @{see {@link #getLivenessHttpProbeTimeout()}} */ @Deprecated public int getLivenessProbeTimeout() { return livenessHttpProbeTimeout; } /** * @deprecated @{see {@link #setLivenessHttpProbeTimeout(int)}} */ @Deprecated public void setLivenessProbeTimeout(int livenessProbeTimeout) { this.livenessHttpProbeTimeout = livenessProbeTimeout; } /** * @deprecated @{see {@link #getLivenessHttpProbePath()}} */ @Deprecated public String getLivenessProbePath() { return livenessHttpProbePath; } /** * @deprecated @{see {@link #setLivenessHttpProbePath(String)}} */ @Deprecated public void setLivenessProbePath(String livenessProbePath) { this.livenessHttpProbePath = livenessProbePath; } /** * @deprecated @{see {@link #getLivenessHttpProbePort()}} */ @Deprecated public Integer getLivenessProbePort() { return livenessHttpProbePort; } /** * @deprecated @{see {@link #setLivenessHttpProbePort(Integer)}} */ @Deprecated public void setLivenessProbePort(Integer livenessProbePort) { this.livenessHttpProbePort = livenessProbePort; } public int getLivenessTcpProbeSuccess() { return livenessTcpProbeSuccess; } public void setLivenessTcpProbeSuccess(int livenessTcpProbeSuccess) { this.livenessTcpProbeSuccess = livenessTcpProbeSuccess; } public int getLivenessTcpProbeFailure() { return livenessTcpProbeFailure; } public void setLivenessTcpProbeFailure(int livenessTcpProbeFailure) { this.livenessTcpProbeFailure = livenessTcpProbeFailure; } public int getLivenessHttpProbeDelay() { return livenessHttpProbeDelay; } public void setLivenessHttpProbeDelay(int livenessHttpProbeDelay) { this.livenessHttpProbeDelay = livenessHttpProbeDelay; } public int getLivenessTcpProbeTimeout() { return livenessTcpProbeTimeout; } public void setLivenessTcpProbeTimeout(int livenessTcpProbeTimeout) { this.livenessTcpProbeTimeout = livenessTcpProbeTimeout; } public int getLivenessHttpProbePeriod() { return livenessHttpProbePeriod; } public void setLivenessHttpProbePeriod(int livenessHttpProbePeriod) { this.livenessHttpProbePeriod = livenessHttpProbePeriod; } public int getLivenessHttpProbeTimeout() { return livenessHttpProbeTimeout; } public void setLivenessHttpProbeTimeout(int livenessHttpProbeTimeout) { this.livenessHttpProbeTimeout = livenessHttpProbeTimeout; } public String getLivenessHttpProbePath() { return livenessHttpProbePath; } public void setLivenessHttpProbePath(String livenessHttpProbePath) { this.livenessHttpProbePath = livenessHttpProbePath; } public Integer getLivenessHttpProbePort() { return livenessHttpProbePort; } public void setLivenessHttpProbePort(Integer livenessHttpProbePort) { this.livenessHttpProbePort = livenessHttpProbePort; } public int getStartupHttpProbeTimeout() { return startupHttpProbeTimeout; } public int getStartupHttpProbeFailure() { return startupHttpProbeFailure; } public void setStartupHttpProbeFailure(int startupHttpProbeFailure) { this.startupHttpProbeFailure = startupHttpProbeFailure; } public void setStartupProbeFailure(int startupHttpProbeFailure) { this.startupHttpProbeFailure = startupHttpProbeFailure; } public int getLivenessHttpProbeFailure() { return livenessHttpProbeFailure; } public void setLivenessHttpProbeFailure(int livenessHttpProbeFailure) { this.livenessHttpProbeFailure = livenessHttpProbeFailure; } public void setLivenessProbeFailure(int livenessHttpProbeFailure) { this.livenessHttpProbeFailure = livenessHttpProbeFailure; } public int getLivenessHttpProbeSuccess() { return livenessHttpProbeSuccess; } public void setLivenessHttpProbeSuccess(int livenessHttpProbeSuccess) { this.livenessHttpProbeSuccess = livenessHttpProbeSuccess; } public void setLivenessProbeSuccess(int livenessHttpProbeSuccess) { this.livenessHttpProbeSuccess = livenessHttpProbeSuccess; } public int getStartupHttpProbeSuccess() { return startupHttpProbeSuccess; } public void setStartupHttpProbeSuccess(int startupHttpProbeSuccess) { this.startupHttpProbeSuccess = startupHttpProbeSuccess; } public void setStartupProbeSuccess(int startupHttpProbeSuccess) { this.startupHttpProbeSuccess = startupHttpProbeSuccess; } public int getStartupTcpProbeFailure() { return startupTcpProbeFailure; } public void setStartupTcpProbeFailure(int startupTcpProbeFailure) { this.startupTcpProbeFailure = startupTcpProbeFailure; } public int getStartupTcpProbeSuccess() { return startupTcpProbeSuccess; } public void setStartupTcpProbeSuccess(int startupTcpProbeSuccess) { this.startupTcpProbeSuccess = startupTcpProbeSuccess; } public int getStartupCommandProbeFailure() { return startupCommandProbeFailure; } public void setStartupCommandProbeFailure(int startupCommandProbeFailure) { this.startupCommandProbeFailure = startupCommandProbeFailure; } public int getStartupCommandProbeSuccess() { return startupCommandProbeSuccess; } public void setStartupCommandProbeSuccess(int startupCommandProbeSuccess) { this.startupCommandProbeSuccess = startupCommandProbeSuccess; } public int getLivenessCommandProbeFailure() { return livenessCommandProbeFailure; } public void setLivenessCommandProbeFailure(int livenessCommandProbeFailure) { this.livenessCommandProbeFailure = livenessCommandProbeFailure; } public int getLivenessCommandProbeSuccess() { return livenessCommandProbeSuccess; } public void setLivenessCommandProbeSuccess(int livenessCommandProbeSuccess) { this.livenessCommandProbeSuccess = livenessCommandProbeSuccess; } public int getStartupHttpProbeDelay() { return startupHttpProbeDelay; } public void setStartupHttpProbeTimeout(int startupHttpProbeTimeout) { this.startupHttpProbeTimeout = startupHttpProbeTimeout; } public void setStartupProbeTimeout(int startupHttpProbeTimeout) { this.startupHttpProbeTimeout = startupHttpProbeTimeout; } public void setStartupHttpProbeDelay(int startupHttpProbeDelay) { this.startupHttpProbeDelay = startupHttpProbeDelay; } public void setStartupProbeDelay(int startupHttpProbeDelay) { this.startupHttpProbeDelay = startupHttpProbeDelay; } public String getStartupHttpProbePath() { return startupHttpProbePath; } public void setStartupHttpProbePath(String startupHttpProbePath) { this.startupHttpProbePath = startupHttpProbePath; } public Integer getStartupHttpProbePort() { return startupHttpProbePort; } public void setStartupHttpProbePort(Integer startupHttpProbePort) { this.startupHttpProbePort = startupHttpProbePort; } public void setStartupProbePort(Integer startupHttpProbePort) { this.startupHttpProbePort = startupHttpProbePort; } public String getStartupProbeScheme() { return startupProbeScheme; } public void setStartupProbeScheme(String startupProbeScheme) { this.startupProbeScheme = startupProbeScheme; } public int getStartupHttpProbePeriod() { return startupHttpProbePeriod; } public void setStartupHttpProbePeriod(int startupHttpProbePeriod) { this.startupHttpProbePeriod = startupHttpProbePeriod; } public int getStartupProbePeriod() { return startupHttpProbePeriod; } public void setStartupProbePeriod(int startupHttpProbePeriod) { this.startupHttpProbePeriod = startupHttpProbePeriod; } public int getStartupTcpProbeDelay() { return startupTcpProbeDelay; } public void setStartupTcpProbeDelay(int startupTcpProbeDelay) { this.startupTcpProbeDelay = startupTcpProbeDelay; } public int getStartupTcpProbeTimeout() { return startupTcpProbeTimeout; } public void setStartupTcpProbeTimeout(int startupTcpProbeTimeout) { this.startupTcpProbeTimeout = startupTcpProbeTimeout; } /** * Cron configuration for job scheduling */ private CronConfig cron = new CronConfig(); public int getStartupTcpProbePeriod() { return startupTcpProbePeriod; } public void setStartupTcpProbePeriod(int startupTcpProbePeriod) { this.startupTcpProbePeriod = startupTcpProbePeriod; } public Integer getStartupTcpProbePort() { return startupTcpProbePort; } public void setStartupTcpProbePort(Integer startupTcpProbePort) { this.startupTcpProbePort = startupTcpProbePort; } public int getStartupCommandProbeDelay() { return startupCommandProbeDelay; } public void setStartupCommandProbeDelay(int startupCommandProbeDelay) { this.startupCommandProbeDelay = startupCommandProbeDelay; } public int getStartupCommandProbePeriod() { return startupCommandProbePeriod; } public void setStartupCommandProbePeriod(int startupCommandProbePeriod) { this.startupCommandProbePeriod = startupCommandProbePeriod; } public String getStartupCommandProbeCommand() { return startupCommandProbeCommand; } public void setStartupCommandProbeCommand(String startupCommandProbeCommand) { this.startupCommandProbeCommand = startupCommandProbeCommand; } /** * @deprecated @{see {@link #getReadinessHttpProbeDelay()}} */ @Deprecated public int getReadinessProbeDelay() { return readinessHttpProbeDelay; } /** * @deprecated @{see {@link #setReadinessHttpProbeDelay(int)}} */ @Deprecated public void setReadinessProbeDelay(int readinessProbeDelay) { this.readinessHttpProbeDelay = readinessProbeDelay; } /** * @deprecated @{see {@link #getReadinessHttpProbePeriod()}} */ @Deprecated public int getReadinessProbePeriod() { return readinessHttpProbePeriod; } public int getReadinessTcpProbeFailure() { return readinessTcpProbeFailure; } public void setReadinessTcpProbeFailure(int readinessTcpProbeFailure) { this.readinessTcpProbeFailure = readinessTcpProbeFailure; } public int getReadinessTcpProbeSuccess() { return readinessTcpProbeSuccess; } public void setReadinessTcpProbeSuccess(int readinessTcpProbeSuccess) { this.readinessTcpProbeSuccess = readinessTcpProbeSuccess; } public int getReadinessHttpProbeSuccess() { return readinessHttpProbeSuccess; } public void setReadinessHttpProbeSuccess(int readinessHttpProbeSuccess) { this.readinessHttpProbeSuccess = readinessHttpProbeSuccess; } public int getReadinessHttpProbeFailure() { return readinessHttpProbeFailure; } public void setReadinessHttpProbeFailure(int readinessHttpProbeFailure) { this.readinessHttpProbeFailure = readinessHttpProbeFailure; } public void setReadinessProbeFailure(int readinessHttpProbeFailure) { this.readinessHttpProbeFailure = readinessHttpProbeFailure; } public int getReadinessCommandProbeFailure() { return readinessCommandProbeFailure; } public void setReadinessCommandProbeFailure(int readinessCommandProbeFailure) { this.readinessCommandProbeFailure = readinessCommandProbeFailure; } public int getReadinessCommandProbeSuccess() { return readinessCommandProbeSuccess; } public void setReadinessCommandProbeSuccess(int readinessCommandProbeSuccess) { this.readinessCommandProbeSuccess = readinessCommandProbeSuccess; } /** * @deprecated @{see {@link #setReadinessHttpProbePeriod(int)}} */ @Deprecated public void setReadinessProbePeriod(int readinessProbePeriod) { this.readinessHttpProbePeriod = readinessProbePeriod; } /** * @deprecated @{see {@link #getReadinessHttpProbeTimeout()}} */ @Deprecated public int getReadinessProbeTimeout() { return readinessHttpProbeTimeout; } /** * @deprecated @{see {@link #setReadinessHttpProbeTimeout(int)}} */ @Deprecated public void setReadinessProbeTimeout(int readinessProbeTimeout) { this.readinessHttpProbeTimeout = readinessProbeTimeout; } /** * @deprecated @{see {@link #getReadinessHttpProbePath()}} */ @Deprecated public String getReadinessProbePath() { return readinessHttpProbePath; } /** * @deprecated @{see {@link #setReadinessHttpProbePath(String)}} */ @Deprecated public void setReadinessProbePath(String readinessProbePath) { this.readinessHttpProbePath = readinessProbePath; } /** * @deprecated @{see {@link #getReadinessHttpProbePort()}} */ @Deprecated public Integer getReadinessProbePort() { return readinessHttpProbePort; } /** * @deprecated @{see {@link #setReadinessHttpProbePort(Integer)}} */ @Deprecated public void setReadinessProbePort(Integer readinessProbePort) { this.readinessHttpProbePort = readinessProbePort; } public int getReadinessHttpProbeDelay() { return readinessHttpProbeDelay; } public void setReadinessHttpProbeDelay(int readinessHttpProbeDelay) { this.readinessHttpProbeDelay = readinessHttpProbeDelay; } public int getReadinessHttpProbePeriod() { return readinessHttpProbePeriod; } public void setReadinessHttpProbePeriod(int readinessHttpProbePeriod) { this.readinessHttpProbePeriod = readinessHttpProbePeriod; } public int getReadinessTcpProbeTimeout() { return readinessTcpProbeTimeout; } public void setReadinessTcpProbeTimeout(int readinessTcpProbeTimeout) { this.readinessTcpProbeTimeout = readinessTcpProbeTimeout; } public int getReadinessHttpProbeTimeout() { return readinessHttpProbeTimeout; } public void setReadinessHttpProbeTimeout(int readinessHttpProbeTimeout) { this.readinessHttpProbeTimeout = readinessHttpProbeTimeout; } public String getReadinessHttpProbePath() { return readinessHttpProbePath; } public void setReadinessHttpProbePath(String readinessHttpProbePath) { this.readinessHttpProbePath = readinessHttpProbePath; } public Integer getReadinessHttpProbePort() { return readinessHttpProbePort; } public void setReadinessHttpProbePort(Integer readinessHttpProbePort) { this.readinessHttpProbePort = readinessHttpProbePort; } public int getLivenessTcpProbeDelay() { return livenessTcpProbeDelay; } public void setLivenessTcpProbeDelay(int livenessTcpProbeDelay) { this.livenessTcpProbeDelay = livenessTcpProbeDelay; } public int getLivenessTcpProbePeriod() { return livenessTcpProbePeriod; } public void setLivenessTcpProbePeriod(int livenessTcpProbePeriod) { this.livenessTcpProbePeriod = livenessTcpProbePeriod; } public Integer getLivenessTcpProbePort() { return livenessTcpProbePort; } public void setLivenessTcpProbePort(Integer livenessTcpProbePort) { this.livenessTcpProbePort = livenessTcpProbePort; } public int getReadinessTcpProbeDelay() { return readinessTcpProbeDelay; } public void setReadinessTcpProbeDelay(int readinessTcpProbeDelay) { this.readinessTcpProbeDelay = readinessTcpProbeDelay; } public int getReadinessTcpProbePeriod() { return readinessTcpProbePeriod; } public void setReadinessTcpProbePeriod(int readinessTcpProbePeriod) { this.readinessTcpProbePeriod = readinessTcpProbePeriod; } public Integer getReadinessTcpProbePort() { return readinessTcpProbePort; } public void setReadinessTcpProbePort(Integer readinessTcpProbePort) { this.readinessTcpProbePort = readinessTcpProbePort; } public int getReadinessCommandProbeDelay() { return readinessCommandProbeDelay; } public void setReadinessCommandProbeDelay(int readinessCommandProbeDelay) { this.readinessCommandProbeDelay = readinessCommandProbeDelay; } public int getReadinessCommandProbePeriod() { return readinessCommandProbePeriod; } public void setReadinessCommandProbePeriod(int readinessCommandProbePeriod) { this.readinessCommandProbePeriod = readinessCommandProbePeriod; } public String getReadinessCommandProbeCommand() { return readinessCommandProbeCommand; } public void setReadinessCommandProbeCommand(String readinessCommandProbeCommand) { this.readinessCommandProbeCommand = readinessCommandProbeCommand; } public int getLivenessCommandProbeDelay() { return livenessCommandProbeDelay; } public void setLivenessCommandProbeDelay(int livenessCommandProbeDelay) { this.livenessCommandProbeDelay = livenessCommandProbeDelay; } public int getLivenessCommandProbePeriod() { return livenessCommandProbePeriod; } public void setLivenessCommandProbePeriod(int livenessCommandProbePeriod) { this.livenessCommandProbePeriod = livenessCommandProbePeriod; } public String getLivenessCommandProbeCommand() { return livenessCommandProbeCommand; } public void setLivenessCommandProbeCommand(String livenessCommandProbeCommand) { this.livenessCommandProbeCommand = livenessCommandProbeCommand; } public String getProbeCredentialsSecret() { return probeCredentialsSecret; } public void setProbeCredentialsSecret(String probeCredentialsSecret) { this.probeCredentialsSecret = probeCredentialsSecret; } public ProbeType getProbeType() { return probeType; } public void setProbeType(ProbeType probeType) { this.probeType = probeType; } public StatefulSet getStatefulSet() { return statefulSet; } public void setStatefulSet( StatefulSet statefulSet) { this.statefulSet = statefulSet; } public List getTolerations() { return tolerations; } public void setTolerations(List tolerations) { this.tolerations = tolerations; } public List getSecretKeyRefs() { return secretKeyRefs; } public void setSecretKeyRefs(List secretKeyRefs) { this.secretKeyRefs = secretKeyRefs; } public List getConfigMapKeyRefs() { return configMapKeyRefs; } public void setConfigMapKeyRefs(List configMapKeyRefs) { this.configMapKeyRefs = configMapKeyRefs; } public List getConfigMapRefs() { return configMapRefs; } public void setConfigMapRefs(List configMapRefs) { this.configMapRefs = configMapRefs; } public List getSecretRefs() { return secretRefs; } public void setSecretRefs(List secretRefs) { this.secretRefs = secretRefs; } public String[] getEnvironmentVariables() { return environmentVariables; } public void setEnvironmentVariables(String[] environmentVariables) { this.environmentVariables = environmentVariables; } public EntryPointStyle getEntryPointStyle() { return entryPointStyle; } public void setEntryPointStyle(EntryPointStyle entryPointStyle) { this.entryPointStyle = entryPointStyle; } public boolean isCreateLoadBalancer() { return createLoadBalancer; } public void setCreateLoadBalancer(boolean createLoadBalancer) { this.createLoadBalancer = createLoadBalancer; } public String getServiceAnnotations() { return serviceAnnotations; } public void setServiceAnnotations(String serviceAnnotations) { this.serviceAnnotations = serviceAnnotations; } public String getPodAnnotations() { return podAnnotations; } public void setPodAnnotations(String podAnnotations) { this.podAnnotations = podAnnotations; } public String getJobAnnotations() { return jobAnnotations; } public void setJobAnnotations(String jobAnnotations) { this.jobAnnotations = jobAnnotations; } public int getMinutesToWaitForLoadBalancer() { return minutesToWaitForLoadBalancer; } public void setMinutesToWaitForLoadBalancer(int minutesToWaitForLoadBalancer) { this.minutesToWaitForLoadBalancer = minutesToWaitForLoadBalancer; } public int getMaxTerminatedErrorRestarts() { return maxTerminatedErrorRestarts; } public void setMaxTerminatedErrorRestarts(int maxTerminatedErrorRestarts) { this.maxTerminatedErrorRestarts = maxTerminatedErrorRestarts; } public int getMaxCrashLoopBackOffRestarts() { return maxCrashLoopBackOffRestarts; } public void setMaxCrashLoopBackOffRestarts(int maxCrashLoopBackOffRestarts) { this.maxCrashLoopBackOffRestarts = maxCrashLoopBackOffRestarts; } public ImagePullPolicy getImagePullPolicy() { return imagePullPolicy; } public void setImagePullPolicy(ImagePullPolicy imagePullPolicy) { this.imagePullPolicy = imagePullPolicy; } public LimitsResources getLimits() { return limits; } public void setLimits(LimitsResources limits) { this.limits = limits; } public RequestsResources getRequests() { return requests; } public void setRequests(RequestsResources requests) { this.requests = requests; } public List getVolumeMounts() { return volumeMounts; } public void setVolumeMounts(List volumeMounts) { this.volumeMounts = volumeMounts; } public List getVolumes() { return volumes; } public void setVolumes(List volumes) { this.volumes = volumes; } public boolean isHostNetwork() { return hostNetwork; } public void setHostNetwork(boolean hostNetwork) { this.hostNetwork = hostNetwork; } public boolean isCreateJob() { return createJob; } public void setCreateJob(boolean createJob) { this.createJob = createJob; } public String getDeploymentServiceAccountName() { return deploymentServiceAccountName; } public void setDeploymentServiceAccountName(String deploymentServiceAccountName) { this.deploymentServiceAccountName = deploymentServiceAccountName; } public int getMaximumConcurrentTasks() { return maximumConcurrentTasks; } public void setMaximumConcurrentTasks(int maximumConcurrentTasks) { this.maximumConcurrentTasks = maximumConcurrentTasks; } public void setNodeSelector(String nodeSelector) { this.nodeSelector = nodeSelector; } public String getNodeSelector() { return nodeSelector; } public void setPodSecurityContext(PodSecurityContext podSecurityContext) { this.podSecurityContext = podSecurityContext; } public PodSecurityContext getPodSecurityContext() { return podSecurityContext; } public void setContainerSecurityContext(ContainerSecurityContext containerSecurityContext) { this.containerSecurityContext = containerSecurityContext; } public ContainerSecurityContext getContainerSecurityContext() { return containerSecurityContext; } public NodeAffinity getNodeAffinity() { return nodeAffinity; } public void setNodeAffinity(NodeAffinity nodeAffinity) { this.nodeAffinity = nodeAffinity; } public PodAffinity getPodAffinity() { return podAffinity; } public void setPodAffinity(PodAffinity podAffinity) { this.podAffinity = podAffinity; } public PodAntiAffinity getPodAntiAffinity() { return podAntiAffinity; } public void setPodAntiAffinity(PodAntiAffinity podAntiAffinity) { this.podAntiAffinity = podAntiAffinity; } public String getStatefulSetInitContainerImageName() { return statefulSetInitContainerImageName; } public void setStatefulSetInitContainerImageName(String statefulSetInitContainerImageName) { this.statefulSetInitContainerImageName = statefulSetInitContainerImageName; } public InitContainer getInitContainer() { return initContainer; } public void setInitContainer(InitContainer initContainer) { this.initContainer = initContainer; } public List getAdditionalContainers() { return this.additionalContainers; } public void setAdditionalContainers(List additionalContainers) { this.additionalContainers = additionalContainers; } public String getLivenessHttpProbeScheme() { return livenessHttpProbeScheme; } public void setLivenessHttpProbeScheme(String livenessHttpProbeScheme) { this.livenessHttpProbeScheme = livenessHttpProbeScheme; } public String getReadinessHttpProbeScheme() { return readinessHttpProbeScheme; } public void setReadinessHttpProbeScheme(String readinessHttpProbeScheme) { this.readinessHttpProbeScheme = readinessHttpProbeScheme; } Lifecycle getLifecycle() { return lifecycle; } void setLifecycle(Lifecycle lifecycle) { this.lifecycle = lifecycle; } public String getDeploymentLabels() { return deploymentLabels; } public void setDeploymentLabels(String deploymentLabels) { this.deploymentLabels = deploymentLabels; } public AppAdmin getAppAdmin() { return appAdmin; } public void setAppAdmin(AppAdmin appAdmin) { this.appAdmin = appAdmin; } public CronConfig getCron() { return cron; } public void setCron(CronConfig cron) { this.cron = cron; } } ================================================ FILE: src/main/java/org/springframework/cloud/deployer/spi/kubernetes/LivenessCommandProbeCreator.java ================================================ /* * Copyright 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.cloud.deployer.spi.kubernetes; import org.springframework.cloud.deployer.spi.util.CommandLineTokenizer; import org.springframework.util.StringUtils; /** * Creates a command based liveness probe * * @author Chris Schaefer * @author Corneil du Plessis * @since 2.5 */ class LivenessCommandProbeCreator extends CommandProbeCreator { LivenessCommandProbeCreator(KubernetesDeployerProperties kubernetesDeployerProperties, ContainerConfiguration containerConfiguration) { super(kubernetesDeployerProperties, containerConfiguration); } @Override int getInitialDelay() { return getProbeIntProperty(LIVENESS_DEPLOYER_PROPERTY_PREFIX, "Command", "ProbeDelay", getKubernetesDeployerProperties().getLivenessCommandProbeDelay()); } @Override int getPeriod() { return getProbeIntProperty(LIVENESS_DEPLOYER_PROPERTY_PREFIX, "Command", "ProbePeriod", getKubernetesDeployerProperties().getLivenessCommandProbePeriod()); } @Override int getFailure() { return getProbeIntProperty(LIVENESS_DEPLOYER_PROPERTY_PREFIX, "Command", "ProbeFailure", getKubernetesDeployerProperties().getLivenessCommandProbeFailure()); } @Override int getSuccess() { return getProbeIntProperty(LIVENESS_DEPLOYER_PROPERTY_PREFIX, "Command", "ProbeSuccess", getKubernetesDeployerProperties().getLivenessCommandProbeSuccess()); } @Override String[] getCommand() { String probeCommandValue = getDeploymentPropertyValue(LIVENESS_DEPLOYER_PROPERTY_PREFIX + "CommandProbeCommand"); if (StringUtils.hasText(probeCommandValue)) { return new CommandLineTokenizer(probeCommandValue).getArgs().toArray(new String[0]); } if (getKubernetesDeployerProperties().getLivenessCommandProbeCommand() != null) { return new CommandLineTokenizer(getKubernetesDeployerProperties().getLivenessCommandProbeCommand()) .getArgs().toArray(new String[0]); } throw new IllegalArgumentException("The livenessCommandProbeCommand property must be set."); } } ================================================ FILE: src/main/java/org/springframework/cloud/deployer/spi/kubernetes/LivenessHttpProbeCreator.java ================================================ /* * Copyright 2018-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.cloud.deployer.spi.kubernetes; import org.springframework.util.StringUtils; /** * Creates an HTTP Liveness probe * * @author Chris Schaefer * @author Ilayaperumal Gopinathan * @author Corneil du Plessis */ class LivenessHttpProbeCreator extends HttpProbeCreator { LivenessHttpProbeCreator(KubernetesDeployerProperties kubernetesDeployerProperties, ContainerConfiguration containerConfiguration) { super(kubernetesDeployerProperties, containerConfiguration); } @Override public Integer getPort() { String probePortValue = getProbeProperty(LIVENESS_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbePort"); if (StringUtils.hasText(probePortValue)) { return Integer.parseInt(probePortValue); } if (getKubernetesDeployerProperties().getLivenessHttpProbePort() != null) { return getKubernetesDeployerProperties().getLivenessHttpProbePort(); } if (getDefaultPort() != null) { return getDefaultPort(); } return null; } @Override protected String getProbePath() { String probePathValue = getProbeProperty(LIVENESS_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbePath", getKubernetesDeployerProperties().getLivenessHttpProbePath()); if (StringUtils.hasText(probePathValue)) { return probePathValue; } if (useBoot1ProbePath()) { return BOOT_1_LIVENESS_PROBE_PATH; } return BOOT_2_LIVENESS_PROBE_PATH; } @Override protected String getScheme() { String probeSchemeValue = getProbeProperty(LIVENESS_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbeScheme", getKubernetesDeployerProperties().getLivenessHttpProbeScheme()); if (StringUtils.hasText(probeSchemeValue)) { return probeSchemeValue; } return DEFAULT_PROBE_SCHEME; } @Override protected int getTimeout() { return getProbeIntProperty(LIVENESS_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbeTimeout", getKubernetesDeployerProperties().getLivenessHttpProbeTimeout()); } @Override protected int getInitialDelay() { return getProbeIntProperty(LIVENESS_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbeDelay", getKubernetesDeployerProperties().getLivenessHttpProbeDelay()); } @Override protected int getPeriod() { return getProbeIntProperty(LIVENESS_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbePeriod", getKubernetesDeployerProperties().getLivenessHttpProbePeriod()); } @Override int getFailure() { return getProbeIntProperty(LIVENESS_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbeFailure", getKubernetesDeployerProperties().getLivenessHttpProbeFailure()); } @Override int getSuccess() { return getProbeIntProperty(LIVENESS_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbeSuccess", getKubernetesDeployerProperties().getLivenessHttpProbeSuccess()); } } ================================================ FILE: src/main/java/org/springframework/cloud/deployer/spi/kubernetes/LivenessTcpProbeCreator.java ================================================ /* * Copyright 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.cloud.deployer.spi.kubernetes; import org.springframework.util.StringUtils; /** * Creates a TCP liveness probe * * @author Chris Schaefer * @author Corneil du Plessis * @since 2.5 */ class LivenessTcpProbeCreator extends TcpProbeCreator { LivenessTcpProbeCreator(KubernetesDeployerProperties kubernetesDeployerProperties, ContainerConfiguration containerConfiguration) { super(kubernetesDeployerProperties, containerConfiguration); } @Override int getInitialDelay() { return getProbeIntProperty(LIVENESS_DEPLOYER_PROPERTY_PREFIX, "Tcp", "ProbeDelay", getKubernetesDeployerProperties().getLivenessTcpProbeDelay()); } @Override int getPeriod() { return getProbeIntProperty(LIVENESS_DEPLOYER_PROPERTY_PREFIX, "Tcp", "ProbePeriod", getKubernetesDeployerProperties().getLivenessTcpProbePeriod()); } @Override protected int getTimeout() { return getProbeIntProperty(LIVENESS_DEPLOYER_PROPERTY_PREFIX, "Tcp", "ProbeTimeout", getKubernetesDeployerProperties().getLivenessTcpProbeTimeout()); } @Override Integer getPort() { String probePortValue = getProbeProperty(LIVENESS_DEPLOYER_PROPERTY_PREFIX, "Tcp", "ProbePort"); if (StringUtils.hasText(probePortValue)) { if (!probePortValue.chars().allMatch(Character::isDigit)) { throw new IllegalArgumentException("LivenessTcpProbePort must contain all digits"); } return Integer.parseInt(probePortValue); } if (getKubernetesDeployerProperties().getLivenessTcpProbePort() != null) { return getKubernetesDeployerProperties().getLivenessTcpProbePort(); } throw new IllegalArgumentException("The livenessTcpProbePort property must be set."); } @Override int getFailure() { return getProbeIntProperty(LIVENESS_DEPLOYER_PROPERTY_PREFIX, "Tcp", "ProbeFailure", getKubernetesDeployerProperties().getLivenessTcpProbeFailure()); } @Override int getSuccess() { return getProbeIntProperty(LIVENESS_DEPLOYER_PROPERTY_PREFIX, "Tcp", "ProbeSuccess", getKubernetesDeployerProperties().getLivenessTcpProbeSuccess()); } } ================================================ FILE: src/main/java/org/springframework/cloud/deployer/spi/kubernetes/ProbeCreator.java ================================================ /* * Copyright 2018-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.cloud.deployer.spi.kubernetes; import java.util.Map; import io.fabric8.kubernetes.api.model.Probe; import org.springframework.cloud.deployer.spi.kubernetes.support.PropertyParserUtils; import org.springframework.util.StringUtils; /** * Base class for creating Probe's * * @author Chris Schaefer * @author Ilayaperumal Gopinathan */ abstract class ProbeCreator { static final String KUBERNETES_DEPLOYER_PREFIX = "spring.cloud.deployer.kubernetes"; static final String LIVENESS_DEPLOYER_PROPERTY_PREFIX = KUBERNETES_DEPLOYER_PREFIX + ".liveness"; static final String READINESS_DEPLOYER_PROPERTY_PREFIX = KUBERNETES_DEPLOYER_PREFIX + ".readiness"; static final String STARTUP_DEPLOYER_PROPERTY_PREFIX = KUBERNETES_DEPLOYER_PREFIX + ".startup"; private ContainerConfiguration containerConfiguration; private KubernetesDeployerProperties kubernetesDeployerProperties; ProbeCreator(KubernetesDeployerProperties kubernetesDeployerProperties, ContainerConfiguration containerConfiguration) { this.containerConfiguration = containerConfiguration; this.kubernetesDeployerProperties = kubernetesDeployerProperties; } abstract Probe create(); abstract int getInitialDelay(); abstract int getPeriod(); abstract int getFailure(); abstract int getSuccess(); KubernetesDeployerProperties getKubernetesDeployerProperties() { return kubernetesDeployerProperties; } private Map getDeploymentProperties() { return this.containerConfiguration.getAppDeploymentRequest().getDeploymentProperties(); } protected String getDeploymentPropertyValue(String propertyName) { return PropertyParserUtils.getDeploymentPropertyValue(getDeploymentProperties(), propertyName); } protected String getDeploymentPropertyValue(String propertyName, String defaultValue) { return PropertyParserUtils.getDeploymentPropertyValue(getDeploymentProperties(), propertyName, defaultValue); } ContainerConfiguration getContainerConfiguration() { return containerConfiguration; } // used to resolve deprecated HTTP probe property names that do not include "Http" in them // can be removed when deprecated HTTP probes without "Http" in them get removed String getProbeProperty(String propertyPrefix, String probeName, String propertySuffix) { String defaultValue = getDeploymentPropertyValue(propertyPrefix + probeName + propertySuffix); return StringUtils.hasText(defaultValue) ? defaultValue : getDeploymentPropertyValue(propertyPrefix + propertySuffix); } String getProbeProperty(String propertyPrefix, String probeName, String propertySuffix, String defaultValue) { return getDeploymentPropertyValue(propertyPrefix + probeName + propertySuffix, getDeploymentPropertyValue(propertyPrefix + propertySuffix, defaultValue) ); } int getProbeIntProperty(String propertyPrefix, String probeName, String propertySuffix, int defaultValue) { String propertyValue = getDeploymentPropertyValue(propertyPrefix + probeName + propertySuffix, getDeploymentPropertyValue(propertyPrefix + propertySuffix) ); return StringUtils.hasText(propertyValue) ? Integer.parseInt(propertyValue) : defaultValue; } } ================================================ FILE: src/main/java/org/springframework/cloud/deployer/spi/kubernetes/ProbeCreatorFactory.java ================================================ /* * Copyright 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.cloud.deployer.spi.kubernetes; import io.fabric8.kubernetes.api.model.Probe; /** * Creates health check probes * * @author Chris Schaefer * @since 2.5 */ class ProbeCreatorFactory { static Probe createStartupProbe(ContainerConfiguration containerConfiguration, KubernetesDeployerProperties kubernetesDeployerProperties, ProbeType probeType) { switch (probeType) { case HTTP: return new StartupHttpProbeCreator(kubernetesDeployerProperties, containerConfiguration).create(); case TCP: return new StartupTcpProbeCreator(kubernetesDeployerProperties, containerConfiguration).create(); case COMMAND: return new StartupCommandProbeCreator(kubernetesDeployerProperties, containerConfiguration).create(); default: throw new IllegalArgumentException("Unknown startup probe type: " + probeType); } } static Probe createReadinessProbe(ContainerConfiguration containerConfiguration, KubernetesDeployerProperties kubernetesDeployerProperties, ProbeType probeType) { switch (probeType) { case HTTP: return new ReadinessHttpProbeCreator(kubernetesDeployerProperties, containerConfiguration).create(); case TCP: return new ReadinessTcpProbeCreator(kubernetesDeployerProperties, containerConfiguration).create(); case COMMAND: return new ReadinessCommandProbeCreator(kubernetesDeployerProperties, containerConfiguration).create(); default: throw new IllegalArgumentException("Unknown readiness probe type: " + probeType); } } static Probe createLivenessProbe(ContainerConfiguration containerConfiguration, KubernetesDeployerProperties kubernetesDeployerProperties, ProbeType probeType) { switch (probeType) { case HTTP: return new LivenessHttpProbeCreator(kubernetesDeployerProperties, containerConfiguration).create(); case TCP: return new LivenessTcpProbeCreator(kubernetesDeployerProperties, containerConfiguration).create(); case COMMAND: return new LivenessCommandProbeCreator(kubernetesDeployerProperties, containerConfiguration).create(); default: throw new IllegalArgumentException("Unknown liveness probe type: " + probeType); } } } ================================================ FILE: src/main/java/org/springframework/cloud/deployer/spi/kubernetes/ReadinessCommandProbeCreator.java ================================================ /* * Copyright 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.cloud.deployer.spi.kubernetes; import org.springframework.cloud.deployer.spi.util.CommandLineTokenizer; import org.springframework.util.StringUtils; /** * Creates a command based readiness probe * * @author Chris Schaefer * @author Corneil du Plessis * @since 2.5 */ class ReadinessCommandProbeCreator extends CommandProbeCreator { ReadinessCommandProbeCreator(KubernetesDeployerProperties kubernetesDeployerProperties, ContainerConfiguration containerConfiguration) { super(kubernetesDeployerProperties, containerConfiguration); } @Override int getInitialDelay() { return getProbeIntProperty(READINESS_DEPLOYER_PROPERTY_PREFIX, "Command", "ProbeDelay", getKubernetesDeployerProperties().getReadinessCommandProbeDelay()); } @Override int getPeriod() { return getProbeIntProperty(READINESS_DEPLOYER_PROPERTY_PREFIX, "Command", "ProbePeriod", getKubernetesDeployerProperties().getReadinessCommandProbePeriod()); } @Override int getFailure() { return getProbeIntProperty(READINESS_DEPLOYER_PROPERTY_PREFIX, "Command", "ProbeFailure", getKubernetesDeployerProperties().getReadinessCommandProbeFailure()); } @Override int getSuccess() { return getProbeIntProperty(READINESS_DEPLOYER_PROPERTY_PREFIX, "Command", "ProbeSuccess", getKubernetesDeployerProperties().getReadinessCommandProbeSuccess()); } @Override String[] getCommand() { String probeCommandValue = getDeploymentPropertyValue(READINESS_DEPLOYER_PROPERTY_PREFIX + "CommandProbeCommand"); if (StringUtils.hasText(probeCommandValue)) { return new CommandLineTokenizer(probeCommandValue).getArgs().toArray(new String[0]); } if (getKubernetesDeployerProperties().getReadinessCommandProbeCommand() != null) { return new CommandLineTokenizer(getKubernetesDeployerProperties().getReadinessCommandProbeCommand()) .getArgs().toArray(new String[0]); } throw new IllegalArgumentException("The readinessCommandProbeCommand property must be set."); } } ================================================ FILE: src/main/java/org/springframework/cloud/deployer/spi/kubernetes/ReadinessHttpProbeCreator.java ================================================ /* * Copyright 2018-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.cloud.deployer.spi.kubernetes; import org.springframework.util.StringUtils; /** * Creates an HTTP Readiness Probe. * * @author Chris Schaefer * @author Ilayaperumal Gopinathan * @author Corneil du Plessis */ class ReadinessHttpProbeCreator extends HttpProbeCreator { ReadinessHttpProbeCreator(KubernetesDeployerProperties kubernetesDeployerProperties, ContainerConfiguration containerConfiguration) { super(kubernetesDeployerProperties, containerConfiguration); } @Override public Integer getPort() { String probePortValue = getProbeProperty(READINESS_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbePort"); if (StringUtils.hasText(probePortValue)) { if (!probePortValue.chars().allMatch(Character::isDigit)) { throw new IllegalArgumentException("ReadinessHttpProbeCreator must contain all digits"); } return Integer.parseInt(probePortValue); } if (getKubernetesDeployerProperties().getReadinessHttpProbePort() != null) { return getKubernetesDeployerProperties().getReadinessHttpProbePort(); } if (getDefaultPort() != null) { return getDefaultPort(); } return null; } @Override protected String getProbePath() { String probePathValue = getProbeProperty(READINESS_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbePath"); if (StringUtils.hasText(probePathValue)) { return probePathValue; } if (getKubernetesDeployerProperties().getReadinessHttpProbePath() != null) { return getKubernetesDeployerProperties().getReadinessHttpProbePath(); } if (useBoot1ProbePath()) { return BOOT_1_READINESS_PROBE_PATH; } return BOOT_2_READINESS_PROBE_PATH; } @Override protected String getScheme() { String probeSchemeValue = getProbeProperty(READINESS_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbeScheme"); if (StringUtils.hasText(probeSchemeValue)) { return probeSchemeValue; } if (getKubernetesDeployerProperties().getReadinessHttpProbeScheme() != null) { return getKubernetesDeployerProperties().getReadinessHttpProbeScheme(); } return DEFAULT_PROBE_SCHEME; } @Override protected int getTimeout() { return getProbeIntProperty(READINESS_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbeTimeout", getKubernetesDeployerProperties().getReadinessHttpProbeTimeout()); } @Override protected int getInitialDelay() { return getProbeIntProperty(READINESS_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbeDelay", getKubernetesDeployerProperties().getReadinessHttpProbeDelay()); } @Override protected int getPeriod() { return getProbeIntProperty(READINESS_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbePeriod", getKubernetesDeployerProperties().getReadinessHttpProbePeriod()); } @Override int getFailure() { return getProbeIntProperty(READINESS_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbeFailure", getKubernetesDeployerProperties().getReadinessHttpProbeFailure()); } @Override int getSuccess() { return getProbeIntProperty(READINESS_DEPLOYER_PROPERTY_PREFIX, "Http", "ProbeSuccess", getKubernetesDeployerProperties().getReadinessHttpProbeSuccess()); } } ================================================ FILE: src/main/java/org/springframework/cloud/deployer/spi/kubernetes/ReadinessTcpProbeCreator.java ================================================ /* * Copyright 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.cloud.deployer.spi.kubernetes; import org.springframework.util.StringUtils; /** * Creates a TCP readiness probe * * @author Chris Schaefer * @author Corneil du Plessis * @since 2.5 */ class ReadinessTcpProbeCreator extends TcpProbeCreator { ReadinessTcpProbeCreator(KubernetesDeployerProperties kubernetesDeployerProperties, ContainerConfiguration containerConfiguration) { super(kubernetesDeployerProperties, containerConfiguration); } @Override int getInitialDelay() { return getProbeIntProperty(READINESS_DEPLOYER_PROPERTY_PREFIX, "Tcp", "ProbeDelay", getKubernetesDeployerProperties().getReadinessTcpProbeDelay()); } @Override int getPeriod() { return getProbeIntProperty(READINESS_DEPLOYER_PROPERTY_PREFIX, "Tcp", "ProbePeriod", getKubernetesDeployerProperties().getReadinessTcpProbePeriod()); } @Override protected int getTimeout() { return getProbeIntProperty(READINESS_DEPLOYER_PROPERTY_PREFIX, "Tcp", "ProbeTimeout", getKubernetesDeployerProperties().getReadinessTcpProbeTimeout()); } @Override Integer getPort() { String probePortValue = getProbeProperty(READINESS_DEPLOYER_PROPERTY_PREFIX, "Tcp", "ProbePort"); if (StringUtils.hasText(probePortValue)) { if (!probePortValue.chars().allMatch(Character::isDigit)) { throw new IllegalArgumentException("ReadinessTcpProbePort must contain all digits"); } return Integer.parseInt(probePortValue); } if (getKubernetesDeployerProperties().getReadinessTcpProbePort() != null) { return getKubernetesDeployerProperties().getReadinessTcpProbePort(); } throw new IllegalArgumentException("A readinessTcpProbePort property must be set."); } @Override int getFailure() { return getProbeIntProperty(READINESS_DEPLOYER_PROPERTY_PREFIX, "Tcp", "ProbeFailure", getKubernetesDeployerProperties().getReadinessTcpProbeFailure()); } @Override int getSuccess() { return getProbeIntProperty(READINESS_DEPLOYER_PROPERTY_PREFIX, "Tcp", "ProbeSuccess", getKubernetesDeployerProperties().getReadinessTcpProbeSuccess()); } } ================================================ FILE: src/main/java/org/springframework/cloud/deployer/spi/local/AbstractLocalDeployerSupport.java ================================================ /* * Copyright 2016-2022 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.cloud.deployer.spi.local; import java.io.IOException; import java.net.URL; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; import java.util.Set; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; import org.springframework.cloud.deployer.resource.docker.DockerResource; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; import org.springframework.cloud.deployer.spi.util.RuntimeVersionUtils; import org.springframework.http.ResponseEntity; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.util.Assert; import org.springframework.web.client.RestTemplate; /** * Base class for local app deployer and task launcher providing support for common * functionality. * * @author Janne Valkealahti * @author Mark Fisher * @author Ilayaperumal Gopinathan * @author Thomas Risberg * @author Oleg Zhurakousky * @author Vinicius Carvalho * @author Michael Minella * @author David Turanski * @author Christian Tzolov */ public abstract class AbstractLocalDeployerSupport { protected static Set usedPorts = Collections.newSetFromMap(new LinkedHashMap() { @Override protected boolean removeEldestEntry(Map.Entry eldest) { return size() > 1000; } }); protected final Logger logger = LoggerFactory.getLogger(getClass()); public static final String SPRING_APPLICATION_JSON = "SPRING_APPLICATION_JSON"; public static final int DEFAULT_SERVER_PORT = 8080; private static final String USE_SPRING_APPLICATION_JSON_KEY = LocalDeployerProperties.PREFIX + ".use-spring-application-json"; static final String SERVER_PORT_KEY = "server.port"; static final String SERVER_PORT_KEY_COMMAND_LINE_ARG = "--" + SERVER_PORT_KEY + "="; private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private final LocalDeployerProperties localDeployerProperties; private final RestTemplate restTemplate; private final JavaCommandBuilder javaCommandBuilder; private final DockerCommandBuilder dockerCommandBuilder; /** * Instantiates a new abstract deployer support. * * @param localDeployerProperties the local deployer properties */ public AbstractLocalDeployerSupport(LocalDeployerProperties localDeployerProperties) { Assert.notNull(localDeployerProperties, "LocalDeployerProperties must not be null"); this.localDeployerProperties = localDeployerProperties; this.javaCommandBuilder = new JavaCommandBuilder(localDeployerProperties); this.dockerCommandBuilder = new DockerCommandBuilder(localDeployerProperties.getDocker().getNetwork()); this.restTemplate = buildRestTemplate(localDeployerProperties); } /** * Builds a {@link RestTemplate} used for calling app's shutdown endpoint. If needed can * be overridden from an implementing class. This default implementation sets connection * and read timeouts for {@link SimpleClientHttpRequestFactory} and configures * {@link RestTemplate} to use that factory. If shutdown timeout in properties negative, * returns default {@link RestTemplate} which doesn't use timeouts. * * @param properties the local deployer properties * @return the rest template */ protected RestTemplate buildRestTemplate(LocalDeployerProperties properties) { if (properties != null && properties.getShutdownTimeout() > -1) { SimpleClientHttpRequestFactory clientHttpRequestFactory = new SimpleClientHttpRequestFactory(); clientHttpRequestFactory.setConnectTimeout(properties.getShutdownTimeout() * 1000); clientHttpRequestFactory.setReadTimeout(properties.getShutdownTimeout() * 1000); return new RestTemplate(clientHttpRequestFactory); } // fall back to plain default constructor return new RestTemplate(); } /** * Create the RuntimeEnvironmentInfo. * * @return the local runtime environment info */ protected RuntimeEnvironmentInfo createRuntimeEnvironmentInfo(Class spiClass, Class implementationClass) { return new RuntimeEnvironmentInfo.Builder().spiClass(spiClass) .implementationName(implementationClass.getSimpleName()) .implementationVersion(RuntimeVersionUtils.getVersion(implementationClass)).platformType("Local") .platformApiVersion(System.getProperty("os.name") + " " + System.getProperty("os.version")) .platformClientVersion(System.getProperty("os.version")) .platformHostVersion(System.getProperty("os.version")).build(); } /** * Gets the local deployer properties. * * @return the local deployer properties */ final protected LocalDeployerProperties getLocalDeployerProperties() { return localDeployerProperties; } /** * Builds the process builder. Application properties are expected to be calculated prior * to this method. No additional consolidation of application properties is done while * creating the {@code ProcessBuilder}. * * @param request the request * @param appInstanceEnv the instance environment variables * @return the process builder */ protected ProcessBuilder buildProcessBuilder(AppDeploymentRequest request, Map appInstanceEnv, Optional appInstanceNumber, String deploymentId) { Assert.notNull(request, "AppDeploymentRequest must be set"); Map appPropertiesToUse = formatApplicationProperties(request, appInstanceEnv); if (logger.isInfoEnabled()) { logger.info("Preparing to run an application from {}. This may take some time if the artifact must be " + "downloaded from a remote host.", request.getResource()); } LocalDeployerProperties localDeployerProperties = bindDeploymentProperties(request.getDeploymentProperties()); Optional debugAddressOption = DebugAddress.from(localDeployerProperties, appInstanceNumber.orElse(0)); ProcessBuilder builder = getCommandBuilder(request) .buildExecutionCommand(request, appPropertiesToUse, deploymentId, appInstanceNumber, localDeployerProperties, debugAddressOption); logger.info(String.format("Command to be executed: %s", String.join(" ", builder.command()))); //logger.debug(String.format("Environment Variables to be used : %s", builder.environment().entrySet().stream() // .map(entry -> entry.getKey() + " : " + entry.getValue()).collect(Collectors.joining(", ")))); return builder; } /** * Detects the Command builder by the type of the resource in the requests. * @param request deployment request containing information about the resource to be deployed. * @return Returns a command builder compatible with the type of the resource set in the request. */ protected CommandBuilder getCommandBuilder(AppDeploymentRequest request) { return (request.getResource() instanceof DockerResource) ? this.dockerCommandBuilder : this.javaCommandBuilder; } /** * tweak escaping double quotes needed for windows * @param commands * @return */ public static String[] windowsSupport(String[] commands) { if (LocalDeployerUtils.isWindows()) { for (int i = 0; i < commands.length; i++) { commands[i] = commands[i].replace("\"", "\\\""); } } return commands; } /** * This will merge the deployment properties that were passed in at runtime with the * deployment properties of the Deployer instance. * @param runtimeDeploymentProperties deployment properties passed in at runtime * @return merged deployer properties */ protected LocalDeployerProperties bindDeploymentProperties(Map runtimeDeploymentProperties) { LocalDeployerProperties copyOfDefaultProperties = new LocalDeployerProperties(this.localDeployerProperties); return new Binder(new MapConfigurationPropertySource(runtimeDeploymentProperties)) .bind(LocalDeployerProperties.PREFIX, Bindable.ofInstance(copyOfDefaultProperties)) .orElse(copyOfDefaultProperties); } protected Map formatApplicationProperties(AppDeploymentRequest request, Map appInstanceEnvToUse) { Map applicationPropertiesToUse = new HashMap<>(appInstanceEnvToUse); if (useSpringApplicationJson(request)) { try { // If SPRING_APPLICATION_JSON is found, explode it and merge back into appProperties if (applicationPropertiesToUse.containsKey(SPRING_APPLICATION_JSON)) { applicationPropertiesToUse .putAll(OBJECT_MAPPER.readValue(applicationPropertiesToUse.get(SPRING_APPLICATION_JSON), new TypeReference>() { })); applicationPropertiesToUse.remove(SPRING_APPLICATION_JSON); } } catch (IOException e) { throw new IllegalArgumentException( "Unable to read existing SPRING_APPLICATION_JSON to merge properties", e); } try { String saj = OBJECT_MAPPER.writeValueAsString(applicationPropertiesToUse); applicationPropertiesToUse = new HashMap<>(1); applicationPropertiesToUse.put(SPRING_APPLICATION_JSON, saj); } catch (JsonProcessingException e) { throw new IllegalArgumentException( "Unable to create SPRING_APPLICATION_JSON from application properties", e); } } return applicationPropertiesToUse; } /** * Shut down the {@link Process} backing the application {@link Instance}. If the * application exposes a {@code /shutdown} endpoint, that will be invoked followed by a * wait that will not exceed the number of seconds indicated by * {@link LocalDeployerProperties#getShutdownTimeout()} . If the timeout period is exceeded (or * if the {@code /shutdown} endpoint is not exposed), the process will be shut down via * {@link Process#destroy()}. * * @param instance the application instance to shut down */ protected void shutdownAndWait(Instance instance) { try { int timeout = getLocalDeployerProperties().getShutdownTimeout(); if (timeout > 0) { logger.debug("About to call shutdown endpoint for the instance {}", instance); ResponseEntity response = restTemplate.postForEntity( instance.getBaseUrl() + "/shutdown", null, String.class); logger.debug("Response for shutdown endpoint completed for the instance {} with response {}", instance, response); if (response.getStatusCode().is2xxSuccessful()) { long timeoutTimestamp = System.currentTimeMillis() + (timeout * 1000); while (isAlive(instance.getProcess()) && System.currentTimeMillis() < timeoutTimestamp) { Thread.sleep(1000); } } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (Exception e) { // ignore all other errors as we're going to // destroy process if it's alive } finally { if (isAlive(instance.getProcess())) { logger.debug("About to call destroy the process for the instance {}", instance); instance.getProcess().destroy(); logger.debug("Call completed to destroy the process for the instance {}", instance); } } } // Copy-pasting of JDK8+ isAlive method to retain JDK7 compatibility protected boolean isAlive(Process process) { try { logger.debug("About to call exitValue of the process {}", process); process.exitValue(); logger.debug("Call to exitValue of the process {} complete, return false", process); return false; } catch (IllegalThreadStateException e) { logger.debug("Call to exitValue of the process {} threw exception, return true", process); return true; } } protected boolean useSpringApplicationJson(AppDeploymentRequest request) { return request.getDefinition().getProperties().containsKey(USE_SPRING_APPLICATION_JSON_KEY) || this.localDeployerProperties.isUseSpringApplicationJson(); } // TODO (tzolov): This method has a treacherous side affect! Apart from returning the computed Port it also modifies // the appInstanceEnvVars map! Later is used for down stream app deployment configuration. // As a consequence if you place the calcServerPort in wrong place (for example after the buildProcessBuilder(..) // call then the Port configuration won't be known to the command builder). // Proper solution is to (1) either make the method void and rename it to calcAndSetServerPort or (2) make the // method return the mutated appInstanceEnvVars. Sync with the SCT team because the method is used by the // LocalTaskLauncher (e.g. prod. grade) protected int calcServerPort(AppDeploymentRequest request, boolean useDynamicPort, Map appInstanceEnvVars) { int port = DEFAULT_SERVER_PORT; Integer commandLineArgPort = isServerPortKeyPresentOnArgs(request); if (useDynamicPort) { port = getRandomPort(request); appInstanceEnvVars.put(LocalAppDeployer.SERVER_PORT_KEY, String.valueOf(port)); } else if (commandLineArgPort != null) { port = commandLineArgPort; } else if (request.getDefinition().getProperties().containsKey(LocalAppDeployer.SERVER_PORT_KEY)) { port = Integer.parseInt(request.getDefinition().getProperties().get(LocalAppDeployer.SERVER_PORT_KEY)); } return port; } /** * Will check if {@link LocalDeployerProperties#INHERIT_LOGGING} is set by checking * deployment properties. */ protected boolean shouldInheritLogging(AppDeploymentRequest request) { LocalDeployerProperties bindDeployerProperties = bindDeploymentProperties(request.getDeploymentProperties()); return bindDeployerProperties.isInheritLogging(); } public synchronized int getRandomPort(AppDeploymentRequest request) { Set availPorts = new HashSet<>(); // SocketUtils.findAvailableTcpPorts retries 6 times, add additional retry on top. for (int retryCount = 0; retryCount < 5; retryCount++) { int randomInt = getCommandBuilder(request).getPortSuggestion(localDeployerProperties); try { availPorts = DeployerSocketUtils.findAvailableTcpPorts(5, randomInt, randomInt + 5); try { // Give some time for the system to release up ports that were scanned. Thread.sleep(100); } catch (InterruptedException e) { logger.debug(e.getMessage() + "Retrying to find available ports."); } break; } catch (IllegalStateException e) { logger.debug(e.getMessage() + " Retrying to find available ports."); } } if (availPorts.isEmpty()) { throw new IllegalStateException( "Could not find an available TCP port in the range" + localDeployerProperties.getPortRange()); } int finalPort = -1; logger.debug("Available Ports: " + availPorts); for (Integer freePort : availPorts) { if (!usedPorts.contains(freePort)) { finalPort = freePort; usedPorts.add(finalPort); break; } } if (finalPort == -1) { throw new IllegalStateException( "Could not find a free random port range " + localDeployerProperties.getPortRange()); } logger.debug("Using Port: " + finalPort); return finalPort; } protected Integer isServerPortKeyPresentOnArgs(AppDeploymentRequest request) { return request.getCommandlineArguments().stream() .filter(argument -> argument.startsWith(SERVER_PORT_KEY_COMMAND_LINE_ARG)) .map(argument -> Integer.parseInt(argument.replace(SERVER_PORT_KEY_COMMAND_LINE_ARG, "").trim())) .findFirst() .orElse(null); } protected interface Instance { URL getBaseUrl(); Process getProcess(); } } ================================================ FILE: src/main/java/org/springframework/cloud/deployer/spi/local/CommandBuilder.java ================================================ /* * Copyright 2016-2021 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.cloud.deployer.spi.local; import java.net.URL; import java.util.Map; import java.util.Optional; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; /** * Strategy interface for Execution Command builder. * * @author Ilayaperumal Gopinathan * @author Thomas Risberg * @author Michael Minella * @author Christian Tzolov */ public interface CommandBuilder { /** * Builds the execution command for an application. * * @param request the request for the application to execute. * @param appInstanceNumber application instance id. * @param appInstanceEnv the env vars tha might be needed when building the execution command. * @param debugAddress application remote debug address. * @return the build command as a string array. */ ProcessBuilder buildExecutionCommand(AppDeploymentRequest request, Map appInstanceEnv, String deployerId, Optional appInstanceNumber, LocalDeployerProperties localDeployerProperties, Optional debugAddress); /** * Compute an App unique URL over apps deployerId, instance index and computed port. * @param deploymentId App deployment id. * @param index App instance index. * @param port App port. * @return Returns app's URL. */ URL getBaseUrl(String deploymentId, int index, int port); /** * Allow the concrete implementation to suggests the target application port. * @param localDeployerProperties * @return Returns a port suggestion. */ int getPortSuggestion(LocalDeployerProperties localDeployerProperties); /** * Computes the JDWP options with the provided suspend and address arguments. * @param suspend suspend debug argument. * @param address debug address. * @return Returns the JDWP options with the provided suspend and address arguments. */ default String getJdwpOptions(String suspend, String address) { return String.format("-agentlib:jdwp=transport=dt_socket,server=y,suspend=%s,address=%s", suspend, address); } } ================================================ FILE: src/main/java/org/springframework/cloud/deployer/spi/local/DebugAddress.java ================================================ /* * Copyright 2020-2021 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.cloud.deployer.spi.local; import java.util.Optional; import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.StringUtils; /** * Helper for parsing the Debugging address for both the legacy debug-port and the new debug-address properties. * The debug-port supports only Java 8 and is deprecated. The debug-address can be used for jdk 8 as well as * jdk 9 and newer. * When set the debug-address property has precedence over debug-port. * * @author Christian Tzolov */ public class DebugAddress { private static final Pattern HOSTNAME_PATTERN = Pattern.compile("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])$"); private static final Pattern IP_PATTERN = Pattern.compile("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"); private static final Pattern PORT_PATTERN = Pattern.compile("^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$"); public final static Logger logger = LoggerFactory.getLogger(DebugAddress.class); private final String host; private final String port; private final String address; private final String suspend; private DebugAddress(String host, int port, String suspend) { this.host = host; this.port = "" + port; this.suspend = (StringUtils.hasText(suspend)) ? suspend.trim() : "y"; this.address = (StringUtils.hasText(host)) ? String.format("%s:%s", host, port) : this.port; } public String getHost() { return host; } public String getPort() { return port; } public String getSuspend() { return suspend; } public String getAddress() { return this.address; } public static Optional from(LocalDeployerProperties deployerProperties, int instanceNumber) { if (!StringUtils.hasText(deployerProperties.getDebugAddress()) && deployerProperties.getDebugPort() == null) { return Optional.empty(); } String debugHost = null; String debugPort = ("" + deployerProperties.getDebugPort()).trim(); if (StringUtils.hasText(deployerProperties.getDebugAddress())) { String[] addressParts = deployerProperties.getDebugAddress().split(":"); if (addressParts.length == 1) { // JDK 8 only debugPort = addressParts[0].trim(); } else if (addressParts.length == 2) { // JDK 9+ debugHost = addressParts[0].trim(); debugPort = addressParts[1].trim(); if (!("*".equals(debugHost) || HOSTNAME_PATTERN.matcher(debugHost).matches() || IP_PATTERN.matcher(debugHost).matches())) { logger.warn("Invalid debug Host: {}", deployerProperties.getDebugAddress()); return Optional.empty(); } } else { logger.warn("Invalid debug address: {}", deployerProperties.getDebugAddress()); return Optional.empty(); } } if (!PORT_PATTERN.matcher(debugPort).matches()) { logger.warn("Invalid debug port: {}", debugPort); return Optional.empty(); } int portToUse = Integer.parseInt(debugPort) + instanceNumber; return Optional.of(new DebugAddress(debugHost, portToUse, deployerProperties.getDebugSuspend().toString())); } } ================================================ FILE: src/main/java/org/springframework/cloud/deployer/spi/local/DeployerSocketUtils.java ================================================ /* * Copyright 2016-2022 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.cloud.deployer.spi.local; import java.net.InetAddress; import java.net.ServerSocket; import java.util.Random; import java.util.SortedSet; import java.util.TreeSet; import javax.net.ServerSocketFactory; import org.springframework.util.Assert; /** * Simple utility methods for working with network sockets — for example, for * finding available ports on {@code localhost}. * * This is a replacement for SocketUtils. SocketUtils was introduced in Spring Framework 4.0, * primarily to assist in writing integration tests which start an external server on an available random port. * However, these utilities make no guarantee about the subsequent availability * of a given port and are therefore unreliable. Instead of using SocketUtils to * find an available local port for a server, it is recommended that you rely on a * server's ability to start on a random port that it selects or is assigned by the operating system. * To interact with that server, you should query the server for the port it is currently using. * * @author Sam Brannen * @author Ben Hale * @author Arjen Poutsma * @author Gunnar Hillert * @author Gary Russell * @author Glenn Renfro * @deprecated to be replaced with a more robust mechanism in https://github.com/spring-cloud/spring-cloud-deployer-local/issues/215 */ @Deprecated public class DeployerSocketUtils { /** * The default maximum value for port ranges used when finding an available socket * port. */ static final int PORT_RANGE_MAX = 65535; private static final Random random = new Random(System.nanoTime()); /** * Find an available TCP port randomly selected from the range [{@code minPort}, * {@value #PORT_RANGE_MAX}]. * @param minPort the minimum port number * @return an available TCP port number * @throws IllegalStateException if no available port could be found */ public static int findAvailableTcpPort(int minPort) { return findAvailableTcpPort(minPort, PORT_RANGE_MAX); } /** * Find an available TCP port randomly selected from the range [{@code minPort}, * {@code maxPort}]. * @param minPort the minimum port number * @param maxPort the maximum port number * @return an available TCP port number * @throws IllegalStateException if no available port could be found */ public static int findAvailableTcpPort(int minPort, int maxPort) { return SocketType.TCP.findAvailablePort(minPort, maxPort); } /** * Find the requested number of available TCP ports, each randomly selected from the * range [{@code minPort}, {@code maxPort}]. * @param numRequested the number of available ports to find * @param minPort the minimum port number * @param maxPort the maximum port number * @return a sorted set of available TCP port numbers * @throws IllegalStateException if the requested number of available ports could not * be found */ public static SortedSet findAvailableTcpPorts(int numRequested, int minPort, int maxPort) { return SocketType.TCP.findAvailablePorts(numRequested, minPort, maxPort); } private enum SocketType { TCP { @Override protected boolean isPortAvailable(int port) { try { ServerSocket serverSocket = ServerSocketFactory.getDefault().createServerSocket(port, 1, InetAddress.getByName("localhost")); serverSocket.close(); return true; } catch (Exception ex) { return false; } } }; /** * Determine if the specified port for this {@code SocketType} is currently * available on {@code localhost}. */ protected abstract boolean isPortAvailable(int port); /** * Find a pseudo-random port number within the range [{@code minPort}, * {@code maxPort}]. * @param minPort the minimum port number * @param maxPort the maximum port number * @return a random port number within the specified range */ private int findRandomPort(int minPort, int maxPort) { int portRange = maxPort - minPort; return minPort + random.nextInt(portRange + 1); } /** * Find an available port for this {@code SocketType}, randomly selected from the * range [{@code minPort}, {@code maxPort}]. * @param minPort the minimum port number * @param maxPort the maximum port number * @return an available port number for this socket type * @throws IllegalStateException if no available port could be found */ int findAvailablePort(int minPort, int maxPort) { Assert.isTrue(minPort > 0, "'minPort' must be greater than 0"); Assert.isTrue(maxPort >= minPort, "'maxPort' must be greater than or equal to 'minPort'"); Assert.isTrue(maxPort <= PORT_RANGE_MAX, "'maxPort' must be less than or equal to " + PORT_RANGE_MAX); int portRange = maxPort - minPort; int candidatePort; int searchCounter = 0; do { if (searchCounter > portRange) { throw new IllegalStateException( String.format("Could not find an available %s port in the range [%d, %d] after %d attempts", name(), minPort, maxPort, searchCounter)); } candidatePort = findRandomPort(minPort, maxPort); searchCounter++; } while (!isPortAvailable(candidatePort)); return candidatePort; } /** * Find the requested number of available ports for this {@code SocketType}, each * randomly selected from the range [{@code minPort}, {@code maxPort}]. * @param numRequested the number of available ports to find * @param minPort the minimum port number * @param maxPort the maximum port number * @return a sorted set of available port numbers for this socket type * @throws IllegalStateException if the requested number of available ports could * not be found */ SortedSet findAvailablePorts(int numRequested, int minPort, int maxPort) { Assert.isTrue(minPort > 0, "'minPort' must be greater than 0"); Assert.isTrue(maxPort > minPort, "'maxPort' must be greater than 'minPort'"); Assert.isTrue(maxPort <= PORT_RANGE_MAX, "'maxPort' must be less than or equal to " + PORT_RANGE_MAX); Assert.isTrue(numRequested > 0, "'numRequested' must be greater than 0"); Assert.isTrue((maxPort - minPort) >= numRequested, "'numRequested' must not be greater than 'maxPort' - 'minPort'"); SortedSet availablePorts = new TreeSet<>(); int attemptCount = 0; while ((++attemptCount <= numRequested + 100) && availablePorts.size() < numRequested) { availablePorts.add(findAvailablePort(minPort, maxPort)); } if (availablePorts.size() != numRequested) { throw new IllegalStateException( String.format("Could not find %d available %s ports in the range [%d, %d]", numRequested, name(), minPort, maxPort)); } return availablePorts; } } } ================================================ FILE: src/main/java/org/springframework/cloud/deployer/spi/local/DockerCommandBuilder.java ================================================ /* * Copyright 2016-2021 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.cloud.deployer.spi.local; import java.io.IOException; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.ThreadLocalRandom; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.deployer.resource.docker.DockerResource; import org.springframework.cloud.deployer.spi.app.AppDeployer; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.util.StringUtils; /** * Command builder used to craft the command used when running apps inside docker containers. * * @author Ilayaperumal Gopinathan * @author Eric Bottard * @author Henryk Konsek * @author Thomas Risberg * @author Michael Minella * @author Christian Tzolov */ public class DockerCommandBuilder implements CommandBuilder { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); /** * Name of the deployment property used to specify the container name pattern to use. */ public static final String DOCKER_CONTAINER_NAME_KEY = AppDeployer.PREFIX + "docker.container.name"; private final Logger logger = LoggerFactory.getLogger(getClass()); private final String dockerNetwork; public DockerCommandBuilder(String dockerNetwork) { this.dockerNetwork = dockerNetwork; } @Override public int getPortSuggestion(LocalDeployerProperties localDeployerProperties) { return ThreadLocalRandom.current().nextInt(localDeployerProperties.getDocker().getPortRange().getLow(), localDeployerProperties.getDocker().getPortRange().getHigh()); } @Override public URL getBaseUrl(String deploymentId, int index, int port) { try { return new URL("http", String.format("%s-%d", deploymentId, index), port, ""); } catch (Exception e) { throw new IllegalArgumentException(e); } } @Override public ProcessBuilder buildExecutionCommand(AppDeploymentRequest request, Map appInstanceEnv, String deployerId, Optional appInstanceNumber, LocalDeployerProperties localDeployerProperties, Optional debugAddressOption) { appInstanceEnv.put("deployerId", deployerId); List commands = addDockerOptions(request, appInstanceEnv, appInstanceNumber, localDeployerProperties, debugAddressOption); commands.addAll(request.getCommandlineArguments()); logger.debug("Docker Command = " + commands); return new ProcessBuilder(Arrays.asList(AbstractLocalDeployerSupport.windowsSupport(commands.toArray(new String[0])))); } private List addDockerOptions(AppDeploymentRequest request, Map appInstanceEnv, Optional appInstanceNumber, LocalDeployerProperties localDeployerProperties, Optional debugAddressOption) { List commands = new ArrayList<>(); commands.add("docker"); commands.add("run"); if (StringUtils.hasText(this.dockerNetwork)) { commands.add("--network"); commands.add(this.dockerNetwork); } if (localDeployerProperties.getDocker().isDeleteContainerOnExit()) { commands.add("--rm"); } // Add env vars for (String env : appInstanceEnv.keySet()) { commands.add("-e"); commands.add(String.format("%s=%s", env, appInstanceEnv.get(env))); } debugAddressOption.ifPresent(debugAddress -> { String debugCommand = getJdwpOptions(debugAddress.getSuspend(), debugAddress.getAddress()); logger.debug("Deploying app with Debug Command = [{}]", debugCommand); commands.add("-e"); commands.add("JAVA_TOOL_OPTIONS=" + debugCommand); commands.add("-p"); commands.add(String.format("%s:%s", debugAddress.getPort(), debugAddress.getPort())); }); String port = getPort(appInstanceEnv); if (StringUtils.hasText(port)) { commands.add("-p"); commands.add(String.format("%s:%s", port, port)); } applyPortMappings(commands,localDeployerProperties); applyVolumeMountings(commands,localDeployerProperties); if (request.getDeploymentProperties().containsKey(DOCKER_CONTAINER_NAME_KEY)) { if (appInstanceNumber.isPresent()) { commands.add(String.format("--name=%s-%d", request.getDeploymentProperties().get(DOCKER_CONTAINER_NAME_KEY), appInstanceNumber.get())); } else { commands.add(String.format("--name=%s", request.getDeploymentProperties().get(DOCKER_CONTAINER_NAME_KEY))); } } else { String group = request.getDeploymentProperties().get(AppDeployer.GROUP_PROPERTY_KEY); if (StringUtils.hasText(group)) { String deploymentId = String.format("%s.%s", group, request.getDefinition().getName()); int index = appInstanceNumber.orElse(0); commands.add(String.format("--name=%s-%d", deploymentId, index)); } } DockerResource dockerResource = (DockerResource) request.getResource(); try { String dockerImageURI = dockerResource.getURI().toString(); commands.add(dockerImageURI.substring("docker:".length())); } catch (IOException e) { throw new IllegalStateException(e); } return commands; } private void applyVolumeMountings(List commands, LocalDeployerProperties localDeployerProperties) { String volumeMounts = localDeployerProperties.getDocker().getVolumeMounts(); if (StringUtils.hasText(volumeMounts)) { for (String v : parseMapping(volumeMounts)) { commands.add("-v"); commands.add(v); } } } private void applyPortMappings(List commands, LocalDeployerProperties properties) { String portMappings = properties.getDocker().getPortMappings(); if (StringUtils.hasText(portMappings)) { for (String p : parseMapping(portMappings)) { commands.add("-p"); commands.add(p); } } } private String getPort(Map appInstanceEnv) { if (appInstanceEnv.containsKey(AbstractLocalDeployerSupport.SPRING_APPLICATION_JSON)) { try { HashMap flatProperties = new HashMap<>((OBJECT_MAPPER.readValue( appInstanceEnv.get(AbstractLocalDeployerSupport.SPRING_APPLICATION_JSON), new TypeReference>() {}))); if (flatProperties.containsKey(LocalAppDeployer.SERVER_PORT_KEY)) { return flatProperties.get(LocalAppDeployer.SERVER_PORT_KEY); } // fall back to appInstanceEnv. } catch (IOException e) { throw new IllegalArgumentException("Unable to determine server port from SPRING_APPLICATION_JSON"); } } return appInstanceEnv.get(LocalAppDeployer.SERVER_PORT_KEY); } private List parseMapping(String map) { Supplier> stream = () -> Arrays.stream(map.split(",")); stream.get().filter(s -> !s.contains(":")).forEach(s -> logger.warn("incomplete mapping {} will be ignored", s)); return stream.get().filter(s -> s.contains(":")).collect(Collectors.toList()); } } ================================================ FILE: src/main/java/org/springframework/cloud/deployer/spi/local/HttpProbeExecutor.java ================================================ /* * Copyright 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.cloud.deployer.spi.local; import java.net.URI; import java.net.URL; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.deployer.spi.local.LocalDeployerProperties.HttpProbe; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.DefaultUriBuilderFactory; /** * Simple probe executor using rest endpoints. * * @author Janne Valkealahti * */ public class HttpProbeExecutor { private static final Logger logger = LoggerFactory.getLogger(HttpProbeExecutor.class); private final RestTemplate restTemplate; private final URI uri; public HttpProbeExecutor(RestTemplate restTemplate, URI uri) { this.restTemplate = restTemplate; this.uri = uri; } public static HttpProbeExecutor from(URL baseUrl, HttpProbe httpProbe) { URI base = null; try { base = baseUrl.toURI(); } catch (Exception e) { } if (httpProbe == null || httpProbe.getPath() == null || base == null) { return null; } DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(base.toString()); uriBuilderFactory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.NONE); URI uri = uriBuilderFactory.builder().path("{path}").build(httpProbe.getPath()); return new HttpProbeExecutor(new RestTemplate(), uri); } public boolean probe() { try { logger.info("Probing for {}", this.uri); ResponseEntity response = restTemplate.getForEntity(uri, Void.class); HttpStatus statusCode = response.getStatusCode(); boolean ok = statusCode.is2xxSuccessful(); if (!ok) { logger.info("Probe for {} returned {}", this.uri, statusCode); } return ok; } catch (Exception e) { logger.trace("Probe error for {}", this.uri, e); } return false; } } ================================================ FILE: src/main/java/org/springframework/cloud/deployer/spi/local/JavaCommandBuilder.java ================================================ /* * Copyright 2016-2021 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.cloud.deployer.spi.local; import java.io.IOException; import java.net.Inet4Address; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.concurrent.ThreadLocalRandom; import java.util.regex.Pattern; import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; import org.springframework.cloud.deployer.spi.app.AppDeployer; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.util.ByteSizeUtils; import org.springframework.core.io.Resource; import org.springframework.util.StringUtils; /** * @author Mark Pollack * @author Ilayaperumal Gopinathan * @author Thomas Risberg * @author Michael Minella */ public class JavaCommandBuilder implements CommandBuilder { private final Logger logger = LoggerFactory.getLogger(getClass()); private final LocalDeployerProperties properties; public JavaCommandBuilder(LocalDeployerProperties properties) { this.properties = properties; } @Override public int getPortSuggestion(LocalDeployerProperties localDeployerProperties) { return ThreadLocalRandom.current().nextInt(localDeployerProperties.getPortRange().getLow(), localDeployerProperties.getPortRange().getHigh()); } @Override public URL getBaseUrl(String deploymentId, int index, int port) { try { return new URL("http", Inet4Address.getLocalHost().getHostAddress(), port, ""); } catch (Exception e) { throw new IllegalArgumentException(e); } } @Override public ProcessBuilder buildExecutionCommand(AppDeploymentRequest request, Map appInstanceEnv, String deployerId, Optional appInstanceNumber, LocalDeployerProperties localDeployerProperties, Optional debugAddressOption) { ArrayList commands = new ArrayList<>(); Map deploymentProperties = request.getDeploymentProperties(); commands.add(bindDeploymentProperties(deploymentProperties).getJavaCmd()); debugAddressOption.ifPresent(debugAddress -> { commands.add(getJdwpOptions(debugAddress.getSuspend(), debugAddress.getAddress())); }); // Add Java System Properties (ie -Dmy.prop=val) before main class or -jar addJavaOptions(commands, deploymentProperties, properties); addJavaExecutionOptions(commands, request); commands.addAll(request.getCommandlineArguments()); logger.debug("Java Command = " + commands); ProcessBuilder builder = new ProcessBuilder(AbstractLocalDeployerSupport.windowsSupport(commands.toArray(new String[0]))); // retain before we put in app related variables. retainEnvVars(builder.environment(), localDeployerProperties); builder.environment().putAll(appInstanceEnv); return builder; } /** * Retain the environment variable strings in the provided set indicated by * {@link LocalDeployerProperties#getEnvVarsToInherit}. * This assumes that the provided set can be modified. * * @param vars set of environment variable strings * @param localDeployerProperties local deployer properties */ protected void retainEnvVars(Map vars, LocalDeployerProperties localDeployerProperties) { List patterns = new ArrayList<>(Arrays.asList(localDeployerProperties.getEnvVarsToInherit())); for (Iterator> iterator = vars.entrySet().iterator(); iterator.hasNext();) { Entry entry = iterator.next(); String var = entry.getKey(); boolean retain = false; for (String pattern : patterns) { if (Pattern.matches(pattern, var)) { retain = true; break; } } if (!retain) { iterator.remove(); } } } protected void addJavaOptions(List commands, Map deploymentProperties, LocalDeployerProperties localDeployerProperties) { String memory = null; if (deploymentProperties.containsKey(AppDeployer.MEMORY_PROPERTY_KEY)) { memory = "-Xmx" + ByteSizeUtils.parseToMebibytes(deploymentProperties.get(AppDeployer.MEMORY_PROPERTY_KEY)) + "m"; } String javaOptsString = bindDeploymentProperties(deploymentProperties).getJavaOpts(); if (javaOptsString == null && memory != null) { commands.add(memory); } if (javaOptsString != null) { String[] javaOpts = StringUtils.tokenizeToStringArray(javaOptsString, " "); boolean noJavaMemoryOption = Stream.of(javaOpts).noneMatch(s -> s.startsWith("-Xmx")); if (noJavaMemoryOption && memory != null) { commands.add(memory); } commands.addAll(Arrays.asList(javaOpts)); } else { if (localDeployerProperties.getJavaOpts() != null) { String[] javaOpts = StringUtils.tokenizeToStringArray(localDeployerProperties.getJavaOpts(), " "); commands.addAll(Arrays.asList(javaOpts)); } } } protected void addJavaExecutionOptions(List commands, AppDeploymentRequest request) { commands.add("-jar"); Resource resource = request.getResource(); try { commands.add(resource.getFile().getAbsolutePath()); } catch (IOException e) { throw new IllegalStateException(e); } } /** * This will merge the deployment properties that were passed in at runtime with the deployment properties * of the Deployer instance. * @param runtimeDeploymentProperties deployment properties passed in at runtime * @return merged deployer properties */ protected LocalDeployerProperties bindDeploymentProperties(Map runtimeDeploymentProperties) { LocalDeployerProperties copyOfDefaultProperties = new LocalDeployerProperties(this.properties); return new Binder(new MapConfigurationPropertySource(runtimeDeploymentProperties)) .bind(LocalDeployerProperties.PREFIX, Bindable.ofInstance(copyOfDefaultProperties)) .orElse(copyOfDefaultProperties); } } ================================================ FILE: src/main/java/org/springframework/cloud/deployer/spi/local/LocalActuatorTemplate.java ================================================ /* * Copyright 2022 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.cloud.deployer.spi.local; import org.springframework.cloud.deployer.spi.app.AbstractActuatorTemplate; import org.springframework.cloud.deployer.spi.app.AppAdmin; import org.springframework.cloud.deployer.spi.app.AppDeployer; import org.springframework.cloud.deployer.spi.app.AppInstanceStatus; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; /** * @author David Turanski */ public class LocalActuatorTemplate extends AbstractActuatorTemplate { public LocalActuatorTemplate(RestTemplate restTemplate, AppDeployer appDeployer, AppAdmin appAdmin) { super(restTemplate, appDeployer, appAdmin); } @Override protected String actuatorUrlForInstance(AppInstanceStatus appInstanceStatus) { return UriComponentsBuilder.fromHttpUrl(appInstanceStatus.getAttributes().get("url")) .path("/actuator").toUriString(); } } ================================================ FILE: src/main/java/org/springframework/cloud/deployer/spi/local/LocalAppDeployer.java ================================================ /* * Copyright 2016-2022 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.cloud.deployer.spi.local; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.lang.ProcessBuilder.Redirect; import java.lang.reflect.Field; import java.net.HttpURLConnection; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; import jakarta.annotation.PreDestroy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.deployer.spi.app.AppDeployer; import org.springframework.cloud.deployer.spi.app.AppInstanceStatus; import org.springframework.cloud.deployer.spi.app.AppScaleRequest; import org.springframework.cloud.deployer.spi.app.AppStatus; import org.springframework.cloud.deployer.spi.app.DeploymentState; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; import org.springframework.cloud.deployer.spi.local.LocalDeployerProperties.HttpProbe; import org.springframework.util.Assert; import org.springframework.util.FileCopyUtils; import org.springframework.util.StringUtils; /** * An {@link AppDeployer} implementation that spins off a new JVM process per app * instance. * * @author Eric Bottard * @author Marius Bogoevici * @author Mark Fisher * @author Ilayaperumal Gopinathan * @author Janne Valkealahti * @author Patrick Peralta * @author Thomas Risberg * @author Oleg Zhurakousky * @author Michael Minella * @author Glenn Renfro * @author Christian Tzolov * @author David Turanski */ public class LocalAppDeployer extends AbstractLocalDeployerSupport implements AppDeployer { private static final Logger logger = LoggerFactory.getLogger(LocalAppDeployer.class); private static final String JMX_DEFAULT_DOMAIN_KEY = "spring.jmx.default-domain"; private static final String ENDPOINTS_SHUTDOWN_ENABLED_KEY = "endpoints.shutdown.enabled"; private final Map running = new ConcurrentHashMap<>(); /** * Instantiates a new local app deployer. * * @param properties the properties */ public LocalAppDeployer(LocalDeployerProperties properties) { super(properties); } /** * Returns the process exit value. We explicitly use Integer instead of int to indicate * that if {@code NULL} is returned, the process is still running. * @param process the process * @return the process exit value or {@code NULL} if process is still alive */ private static Integer getProcessExitValue(Process process) { try { return process.exitValue(); } catch (IllegalThreadStateException e) { // process is still alive return null; } } /** * Gets the local process pid if available. This should be a safe workaround for unix * systems where reflection can be used to get pid. More reliable way should land with * jdk9. * * @param p the process * @return the local process pid */ private static synchronized int getLocalProcessPid(Process p) { int pid = 0; try { if (p.getClass().getName().equals("java.lang.UNIXProcess")) { Field f = p.getClass().getDeclaredField("pid"); f.setAccessible(true); pid = f.getInt(p); f.setAccessible(false); } } catch (Exception e) { pid = 0; } return pid; } @Override public String deploy(AppDeploymentRequest request) { String group = request.getDeploymentProperties().get(GROUP_PROPERTY_KEY); String deploymentId = String.format("%s.%s", group, request.getDefinition().getName()); validateStatus(deploymentId, DeploymentState.unknown); List processes = new ArrayList<>(); running.put(deploymentId, new AppInstancesHolder(processes, request)); try { Path workDir = createWorkingDir(request.getDeploymentProperties(), deploymentId); String countProperty = request.getDeploymentProperties().get(COUNT_PROPERTY_KEY); int count = (StringUtils.hasText(countProperty)) ? Integer.parseInt(countProperty) : 1; for (int index = 0; index < count; index++) { processes.add(deployApp(request, workDir, group, deploymentId, index, request.getDeploymentProperties())); } } catch (IOException e) { throw new RuntimeException("Exception trying to deploy " + request, e); } return deploymentId; } @Override public void scale(AppScaleRequest appScaleRequest) { validateStatus(appScaleRequest.getDeploymentId(), DeploymentState.deployed); AppInstancesHolder holder = running.get(appScaleRequest.getDeploymentId()); List instances = holder != null ? holder.instances : null; if (instances == null) { throw new IllegalStateException( "Can't find existing instances for deploymentId " + appScaleRequest.getDeploymentId()); } AppDeploymentRequest request = holder.request; String group = request.getDeploymentProperties().get(GROUP_PROPERTY_KEY); String deploymentId = String.format("%s.%s", group, request.getDefinition().getName()); try { Path workDir = createWorkingDir(request.getDeploymentProperties(), deploymentId); int deltaCount = appScaleRequest.getCount() - instances.size(); int targetCount = instances.size() + deltaCount; if (deltaCount > 0) { for (int index = instances.size(); index < targetCount; index++) { instances.add(deployApp(request, workDir, group, deploymentId, index, request.getDeploymentProperties())); } } else if (deltaCount < 0) { List processes = new ArrayList<>(); for (int index = instances.size() - 1; index >= targetCount; index--) { processes.add(instances.remove(index)); } for (AppInstance instance : processes) { if (isAlive(instance.getProcess())) { logger.info("Un-deploying app with deploymentId {} instance {}.", deploymentId, instance.getInstanceNumber()); shutdownAndWait(instance); } } } } catch (IOException e) { throw new RuntimeException("Exception trying to deploy " + request, e); } } @Override public void undeploy(String id) { AppInstancesHolder holder = running.get(id); List processes = holder != null ? holder.instances : null; if (processes != null) { for (AppInstance instance : processes) { if (isAlive(instance.getProcess())) { logger.info("Un-deploying app with deploymentId {} instance {}.", id, instance.getInstanceNumber()); shutdownAndWait(instance); } } running.remove(id); } else { throw new IllegalStateException(String.format("App with deploymentId %s is not in a deployed state.", id)); } } @Override public AppStatus status(String id) { AppInstancesHolder holder = running.get(id); List instances = holder != null ? holder.instances : null; AppStatus.Builder builder = AppStatus.of(id); if (instances != null) { for (AppInstance instance : instances) { builder.with(instance); } } return builder.build(); } @Override public String getLog(String id) { AppInstancesHolder holder = running.get(id); List instances = holder != null ? holder.instances : null; StringBuilder stringBuilder = new StringBuilder(); if (instances != null) { for (AppInstance instance : instances) { String stderr = instance.getStdErr(); if (StringUtils.hasText(stderr)) { stringBuilder.append("stderr:\n"); stringBuilder.append(stderr); } String stdout = instance.getStdOut(); if (StringUtils.hasText(stdout)) { stringBuilder.append("stdout:\n"); stringBuilder.append(stdout); } } } return stringBuilder.toString(); } @Override public RuntimeEnvironmentInfo environmentInfo() { return super.createRuntimeEnvironmentInfo(AppDeployer.class, this.getClass()); } @PreDestroy public void shutdown() { for (String deploymentId : running.keySet()) { undeploy(deploymentId); } } private AppInstance deployApp(AppDeploymentRequest request, Path workDir, String group, String deploymentId, int index, Map deploymentProperties) throws IOException { LocalDeployerProperties localDeployerPropertiesToUse = bindDeploymentProperties(deploymentProperties); // consolidatedAppProperties is a Map of all application properties to be used by // the app being launched. These values should end up as environment variables // either explicitly or as a SPRING_APPLICATION_JSON value. HashMap consolidatedAppProperties = new HashMap<>(request.getDefinition().getProperties()); consolidatedAppProperties.put(JMX_DEFAULT_DOMAIN_KEY, deploymentId); if (!request.getDefinition().getProperties().containsKey(ENDPOINTS_SHUTDOWN_ENABLED_KEY)) { consolidatedAppProperties.put(ENDPOINTS_SHUTDOWN_ENABLED_KEY, "true"); } consolidatedAppProperties.put("endpoints.jmx.unique-names", "true"); if (group != null) { consolidatedAppProperties.put("spring.cloud.application.group", group); } // This Map is the consolidated application properties *for the instance* // to be deployed in this iteration Map appInstanceEnv = new HashMap<>(consolidatedAppProperties); // we only set 'normal' style props reflecting what we set for env format // for cross reference to work inside SAJ. // looks like for now we can't remove these env style formats as i.e. // DeployerIntegrationTestProperties in tests really assume 'INSTANCE_INDEX' and // this might be indication that we can't yet fully remove those. String guid = toGuid(deploymentId, index); if (useSpringApplicationJson(request)) { appInstanceEnv.put("instance.index", Integer.toString(index)); appInstanceEnv.put("spring.cloud.stream.instanceIndex", Integer.toString(index)); appInstanceEnv.put("spring.application.index", Integer.toString(index)); appInstanceEnv.put("spring.cloud.application.guid", guid); } else { appInstanceEnv.put("INSTANCE_INDEX", Integer.toString(index)); appInstanceEnv.put("SPRING_APPLICATION_INDEX", Integer.toString(index)); appInstanceEnv.put("SPRING_CLOUD_APPLICATION_GUID", guid); } this.getLocalDeployerProperties().getAppAdmin().addCredentialsToAppEnvironmentAsProperties(appInstanceEnv); boolean useDynamicPort = !request.getDefinition().getProperties().containsKey(SERVER_PORT_KEY); // WATCH OUT: The calcServerPort sets the computed port in the appInstanceEnv#SERVER_PORT_KEY. // Later is implicitly passed to and used inside the command builder. Therefore the calcServerPort() method // must always be called before the buildProcessBuilder(..)! int port = calcServerPort(request, useDynamicPort, appInstanceEnv); ProcessBuilder builder = buildProcessBuilder(request, appInstanceEnv, Optional.of(index), deploymentId) .inheritIO(); builder.directory(workDir.toFile()); URL baseUrl = (StringUtils.hasText(localDeployerPropertiesToUse.getHostname())) ? new URL("http", localDeployerPropertiesToUse.getHostname(), port, "") : getCommandBuilder(request).getBaseUrl(deploymentId, index, port); AppInstance instance = new AppInstance(deploymentId, index, port, baseUrl, localDeployerPropertiesToUse.getStartupProbe(), localDeployerPropertiesToUse.getHealthProbe()); if (this.shouldInheritLogging(request)) { instance.start(builder, workDir); logger.info("Deploying app with deploymentId {} instance {}.\n Logs will be inherited.", deploymentId, index); } else { instance.start(builder, workDir, getLocalDeployerProperties().isDeleteFilesOnExit()); logger.info("Deploying app with deploymentId {} instance {}.\n Logs will be in {}", deploymentId, index, workDir); } return instance; } private Path createWorkingDir(Map deploymentProperties, String deploymentId) throws IOException { LocalDeployerProperties localDeployerPropertiesToUse = bindDeploymentProperties(deploymentProperties); Path workingDirectoryRoot = Files.createDirectories(localDeployerPropertiesToUse.getWorkingDirectoriesRoot()); Path workDir = Files.createDirectories(workingDirectoryRoot.resolve(Long.toString(System.currentTimeMillis())).resolve(deploymentId)); if (getLocalDeployerProperties().isDeleteFilesOnExit()) { workDir.toFile().deleteOnExit(); } return workDir; } private void validateStatus(String deploymentId, DeploymentState expectedState) { DeploymentState state = status(deploymentId).getState(); Assert.state(state == expectedState, String.format("App with deploymentId [%s] with state [%s] doesn't match expected state [%s]", deploymentId, state, expectedState)); } private static String toGuid(String deploymentId, int appIndex) { return String.format("%s-%s", deploymentId, appIndex); } private static class AppInstance implements Instance, AppInstanceStatus { private final String deploymentId; private final int instanceNumber; private final URL baseUrl; private final Map attributes = new TreeMap<>(); private int pid; private Process process; private File workFile; private File stdout; private File stderr; private int port; private HttpProbeExecutor startupProbeExecutor; private HttpProbeExecutor healthProbeExecutor; private boolean startupProbeOk; private AppInstance(String deploymentId, int instanceNumber, int port, URL baseUrl, HttpProbe startupProbe, HttpProbe healthProbe) { this.deploymentId = deploymentId; this.instanceNumber = instanceNumber; this.port = port; this.baseUrl = baseUrl; this.attributes.put("port", Integer.toString(port)); this.attributes.put("guid", toGuid(deploymentId, instanceNumber)); this.attributes.put("url", baseUrl.toString()); this.startupProbeExecutor = HttpProbeExecutor.from(baseUrl, startupProbe); this.healthProbeExecutor = HttpProbeExecutor.from(baseUrl, healthProbe); } @Override public String getId() { return deploymentId + "-" + instanceNumber; } @Override public URL getBaseUrl() { return this.baseUrl; } @Override public Process getProcess() { return this.process; } @Override public String toString() { return String.format("%s [%s]", getId(), getState()); } @Override public DeploymentState getState() { Integer exit = getProcessExitValue(process); // TODO: consider using exit code mapper concept from batch if (exit != null) { return DeploymentState.failed; } if (port < 1) { // Support case where user passed in zero or negative port indicating fully random // chosen by OS. In this case we simply return deployed if process is up. // Also we can't even try http check as we would not know port to connect to. return DeploymentState.deployed; } // do startup probe first and only until we're deployed if (startupProbeExecutor != null && !startupProbeOk) { boolean ok = startupProbeExecutor.probe(); if (ok) { startupProbeOk = true; return DeploymentState.deployed; } else { return DeploymentState.deploying; } } // now deployed, checking health probe if (healthProbeExecutor != null) { return healthProbeExecutor.probe() ? DeploymentState.deployed : DeploymentState.failed; } try { HttpURLConnection urlConnection = (HttpURLConnection) baseUrl.openConnection(); urlConnection.setConnectTimeout(100); urlConnection.connect(); urlConnection.disconnect(); return DeploymentState.deployed; } catch (IOException e) { return DeploymentState.deploying; } } public String getStdOut() { try { return FileCopyUtils.copyToString(new InputStreamReader(new FileInputStream(this.stdout))); } catch (IOException e) { return "Log retrieval returned " + e.getMessage(); } } public String getStdErr() { try { return FileCopyUtils.copyToString(new InputStreamReader(new FileInputStream(this.stderr))); } catch (IOException e) { return "Log retrieval returned " + e.getMessage(); } } public int getInstanceNumber() { return instanceNumber; } @Override public Map getAttributes() { return this.attributes; } /** * Will start the process while redirecting 'out' and 'err' streams to the 'out' and 'err' * streams of this process. */ private void start(ProcessBuilder builder, Path workDir) throws IOException { if (logger.isDebugEnabled()) { logger.debug("Local App Deployer Commands: " + String.join(",", builder.command()) + ", Environment: " + builder.environment()); } this.workFile = workDir.toFile(); this.attributes.put("working.dir", this.workFile.getAbsolutePath()); this.process = builder.start(); this.pid = getLocalProcessPid(this.process); if (pid > 0) { // add pid if we got it attributes.put("pid", Integer.toString(pid)); } } private void start(ProcessBuilder builder, Path workDir, boolean deleteOnExit) throws IOException { String workDirPath = workDir.toFile().getAbsolutePath(); this.stdout = Files.createFile(Paths.get(workDirPath, "stdout_" + instanceNumber + ".log")).toFile(); this.attributes.put("stdout", stdout.getAbsolutePath()); this.stderr = Files.createFile(Paths.get(workDirPath, "stderr_" + instanceNumber + ".log")).toFile(); this.attributes.put("stderr", stderr.getAbsolutePath()); if (deleteOnExit) { this.stdout.deleteOnExit(); this.stderr.deleteOnExit(); } builder.redirectOutput(Redirect.to(this.stdout)); builder.redirectError(Redirect.to(this.stderr)); this.start(builder, workDir); } } private static class AppInstancesHolder { final List instances; final AppDeploymentRequest request; public AppInstancesHolder(List instances, AppDeploymentRequest request) { this.instances = instances; this.request = request; } } } ================================================ FILE: src/main/java/org/springframework/cloud/deployer/spi/local/LocalDeployerAutoConfiguration.java ================================================ /* * Copyright 2016-2022 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.cloud.deployer.spi.local; import org.springframework.boot.autoconfigure.AutoConfigureOrder; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.deployer.spi.app.ActuatorOperations; import org.springframework.cloud.deployer.spi.app.AppDeployer; import org.springframework.cloud.deployer.spi.task.TaskLauncher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.web.client.RestTemplate; /** * Creates a {@link LocalAppDeployer} and {@link LocalTaskLauncher} * * @author Mark Fisher * @author David Turanski */ @Configuration @EnableConfigurationProperties(LocalDeployerProperties.class) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) public class LocalDeployerAutoConfiguration { @Bean @ConditionalOnMissingBean(AppDeployer.class) public AppDeployer appDeployer(LocalDeployerProperties properties) { return new LocalAppDeployer(properties); } @Bean @ConditionalOnMissingBean(TaskLauncher.class) public TaskLauncher taskLauncher(LocalDeployerProperties properties) { return new LocalTaskLauncher(properties); } @Bean @ConditionalOnMissingBean RestTemplate actuatorRestTemplate() { return new RestTemplate(); } @Bean @ConditionalOnMissingBean(ActuatorOperations.class) ActuatorOperations actuatorOperations(RestTemplate actuatorRestTemplate, AppDeployer appDeployer, LocalDeployerProperties properties) { return new LocalActuatorTemplate(actuatorRestTemplate, appDeployer, properties.getAppAdmin()); } } ================================================ FILE: src/main/java/org/springframework/cloud/deployer/spi/local/LocalDeployerProperties.java ================================================ /* * Copyright 2015-2022 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.cloud.deployer.spi.local; import java.io.File; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; import javax.validation.constraints.Min; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.cloud.deployer.spi.app.AppAdmin; import org.springframework.core.style.ToStringCreator; import org.springframework.util.Assert; import org.springframework.validation.annotation.Validated; /** * Configuration properties for the local deployer. * * @author Eric Bottard * @author Mark Fisher * @author Ilayaperumal Gopinathan * @author Oleg Zhurakousky * @author Vinicius Carvalho * @author David Turanski * @author Christian Tzolov */ @Validated @ConfigurationProperties(prefix = LocalDeployerProperties.PREFIX) public class LocalDeployerProperties { /** * Top level prefix for local deployer configuration properties. */ public static final String PREFIX = "spring.cloud.deployer.local"; /** * Deployer property allowing logging to be redirected to the output stream of * the process that triggered child process. Could be set per the entire * deployment (i.e. {@literal deployer.*.local.inheritLogging=true}) or * per individual application (i.e. * {@literal deployer..local.inheritLogging=true}). */ public static final String INHERIT_LOGGING = PREFIX + ".inherit-logging"; /** * Remote debugging property allowing one to specify port for the remote debug * session. Must be set per individual application (i.e. * {@literal deployer..local.debugPort=9999}). * @deprecated This is only JDK 8 compatible. Use the {@link #DEBUG_ADDRESS} instead for supporting all JDKs. */ public static final String DEBUG_PORT = PREFIX + ".debug-port"; /** * Remote debugging property allowing one to specify the address for the remote debug * session. On Java versions 1.8 or older use the port format. On Java versions 1.9 or greater use the * host:port format. The host could default to *. May be set for individual applications (i.e. * {@literal deployer..local.debugAddress=*:9999}). */ public static final String DEBUG_ADDRESS = PREFIX + ".debug-address"; /** * Remote debugging property allowing one to specify if the startup of the * application should be suspended until remote debug session is established. * Values must be either 'y' or 'n'. Must be set per individual application * (i.e. {@literal deployer..local.debugSuspend=y}). */ public static final String DEBUG_SUSPEND = PREFIX + ".debug-suspend"; private static final Logger logger = LoggerFactory.getLogger(LocalDeployerProperties.class); private static final String JAVA_COMMAND = LocalDeployerUtils.isWindows() ? "java.exe" : "java"; // looks like some windows systems uses 'Path' but process builder give it as 'PATH' private static final String[] ENV_VARS_TO_INHERIT_DEFAULTS_WIN = { "TMP", "TEMP", "PATH", "Path", AbstractLocalDeployerSupport.SPRING_APPLICATION_JSON }; private static final String[] ENV_VARS_TO_INHERIT_DEFAULTS_OTHER = { "TMP", "LANG", "LANGUAGE", "LC_.*", "PATH", AbstractLocalDeployerSupport.SPRING_APPLICATION_JSON }; /** * Directory in which all created processes will run and create log files. */ private Path workingDirectoriesRoot = new File(System.getProperty("java.io.tmpdir")).toPath(); /** * Whether to delete created files and directories on JVM exit. */ private boolean deleteFilesOnExit = true; /** * Array of regular expression patterns for environment variables that should be * passed to launched applications. */ private String[] envVarsToInherit = LocalDeployerUtils.isWindows() ? ENV_VARS_TO_INHERIT_DEFAULTS_WIN : ENV_VARS_TO_INHERIT_DEFAULTS_OTHER; /** * The command to run java. */ private String javaCmd = deduceJavaCommand(); /** * Maximum number of seconds to wait for application shutdown. via the * {@code /shutdown} endpoint. A timeout value of 0 specifies an infinite * timeout. Default is 30 seconds. */ @Min(-1) private int shutdownTimeout = 30; /** * The Java Options to pass to the JVM, e.g -Dtest=foo */ private String javaOpts; /** * Flag to indicate whether application properties are passed as command line * args or in a SPRING_APPLICATION_JSON environment variable. Default value is * {@code true}. */ private boolean useSpringApplicationJson = true; private final PortRange portRange = new PortRange(); /** * The maximum concurrent tasks allowed for this platform instance. */ @Min(1) private int maximumConcurrentTasks = 20; /** * Set remote debugging port for JDK 8 runtimes. * @deprecated Use the {@link #debugAddress} instead! */ private Integer debugPort; /** * Debugging address for the remote clients to attache to. Addresses have the format ":" where * is the host name and is the socket port number at which it attaches or listens. * For JDK 8 or earlier, the address consists of the port number alone (the host name is implicit to localhost). * Example addresses for JDK version 9 or higher: *:20075, 192.168.178.10:20075. * Example addresses for JDK version 8 or earlier: 20075. */ private String debugAddress; public enum DebugSuspendType {y, n}; /** * Suspend defines whether the JVM should suspend and wait for a debugger to attach or not */ private DebugSuspendType debugSuspend = DebugSuspendType.y; private boolean inheritLogging; private final Docker docker = new Docker(); /** * (optional) hostname to use when computing the URL of the deployed application. * By default the {@link CommandBuilder} implementations decide how to build the hostname. */ private String hostname; private AppAdmin appAdmin = new AppAdmin(); public LocalDeployerProperties() { } public LocalDeployerProperties(LocalDeployerProperties from) { this.debugPort = from.getDebugPort(); this.debugAddress = from.getDebugAddress(); this.debugSuspend = from.getDebugSuspend(); this.deleteFilesOnExit = from.isDeleteFilesOnExit(); this.docker.network = from.getDocker().getNetwork(); this.docker.deleteContainerOnExit = from.getDocker().isDeleteContainerOnExit(); this.docker.portRange = from.getDocker().getPortRange(); this.envVarsToInherit = new String[from.getEnvVarsToInherit().length]; System.arraycopy(from.getEnvVarsToInherit(), 0, this.envVarsToInherit, 0, from.getEnvVarsToInherit().length); this.inheritLogging = from.isInheritLogging(); this.javaCmd = from.getJavaCmd(); this.javaOpts = from.getJavaOpts(); this.maximumConcurrentTasks = from.getMaximumConcurrentTasks(); this.portRange.high = from.getPortRange().getHigh(); this.portRange.low = from.getPortRange().getLow(); this.shutdownTimeout = from.getShutdownTimeout(); this.useSpringApplicationJson = from.isUseSpringApplicationJson(); this.workingDirectoriesRoot = Paths.get(from.getWorkingDirectoriesRoot().toUri()); this.hostname =from.getHostname(); this.appAdmin = from.appAdmin; } public static class PortRange { /** * Lower bound for computing applications's random port. */ private int low = 20000; /** * Upper bound for computing applications's random port. */ private int high = 61000; public int getLow() { return low; } public void setLow(int low) { this.low = low; } public int getHigh() { return high; } public void setHigh(int high) { this.high = high; } @Override public String toString() { return "{ low=" + low + ", high=" + high + '}'; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + high; result = prime * result + low; return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } PortRange other = (PortRange) obj; if (high != other.high) { return false; } if (low != other.low) { return false; } return true; } } public static class Docker { /** * Container network */ private String network = "bridge"; /** * Whether to delete the container on container exit. */ private boolean deleteContainerOnExit = true; /** * Allow the Docker command builder use its own port range. */ private PortRange portRange = new PortRange(); /** * Set port mappings for container */ private String portMappings; /** * Set volume mappings */ private String volumeMounts; public PortRange getPortRange() { return portRange; } public String getNetwork() { return network; } public void setNetwork(String network) { this.network = network; } public boolean isDeleteContainerOnExit() { return deleteContainerOnExit; } public void setDeleteContainerOnExit(boolean deleteContainerOnExit) { this.deleteContainerOnExit = deleteContainerOnExit; } public String getPortMappings() { return portMappings; } public void setPortMappings(String portMappings) { this.portMappings = portMappings; } public String getVolumeMounts() { return volumeMounts; } public void setVolumeMounts(String volumeMounts) { this.volumeMounts = volumeMounts; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((network == null) ? 0 : network.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } Docker other = (Docker) obj; if (network == null) { return other.network == null; } else return network.equals(other.network); } } public Docker getDocker() { return docker; } public String getHostname() { return hostname; } public void setHostname(String hostname) { this.hostname = hostname; } public Integer getDebugPort() { return debugPort; } public DebugSuspendType getDebugSuspend() { return debugSuspend; } public void setDebugSuspend(DebugSuspendType debugSuspend) { this.debugSuspend = debugSuspend; } public void setDebugPort(Integer debugPort) { logger.warn("The debugPort is deprecated! It supports only pre Java 9 environments. " + "Please use the debugAddress property instead!"); this.debugPort = debugPort; } public String getDebugAddress() { return debugAddress; } public void setDebugAddress(String debugAddress) { this.debugAddress = debugAddress; } public boolean isInheritLogging() { return inheritLogging; } public void setInheritLogging(boolean inheritLogging) { this.inheritLogging = inheritLogging; } public String getJavaCmd() { return javaCmd; } public void setJavaCmd(String javaCmd) { this.javaCmd = javaCmd; } public Path getWorkingDirectoriesRoot() { return workingDirectoriesRoot; } public void setWorkingDirectoriesRoot(String workingDirectoriesRoot) { this.workingDirectoriesRoot = Paths.get(workingDirectoriesRoot); } public void setWorkingDirectoriesRoot(Path workingDirectoriesRoot) { this.workingDirectoriesRoot = workingDirectoriesRoot; } public boolean isDeleteFilesOnExit() { return deleteFilesOnExit; } public void setDeleteFilesOnExit(boolean deleteFilesOnExit) { this.deleteFilesOnExit = deleteFilesOnExit; } public String[] getEnvVarsToInherit() { return envVarsToInherit; } public void setEnvVarsToInherit(String[] envVarsToInherit) { this.envVarsToInherit = envVarsToInherit; } public int getShutdownTimeout() { return shutdownTimeout; } public LocalDeployerProperties setShutdownTimeout(int shutdownTimeout) { this.shutdownTimeout = shutdownTimeout; return this; } public String getJavaOpts() { return javaOpts; } public void setJavaOpts(String javaOpts) { this.javaOpts = javaOpts; } public boolean isUseSpringApplicationJson() { return useSpringApplicationJson; } public void setUseSpringApplicationJson(boolean useSpringApplicationJson) { this.useSpringApplicationJson = useSpringApplicationJson; } public PortRange getPortRange() { return portRange; } public int getMaximumConcurrentTasks() { return maximumConcurrentTasks; } public void setMaximumConcurrentTasks(int maximumConcurrentTasks) { this.maximumConcurrentTasks = maximumConcurrentTasks; } private HttpProbe startupProbe = new HttpProbe(); private HttpProbe healthProbe = new HttpProbe(); public HttpProbe getStartupProbe() { return startupProbe; } public void setStartupProbe(HttpProbe startupProbe) { this.startupProbe = startupProbe; } public HttpProbe getHealthProbe() { return healthProbe; } public void setHealthProbe(HttpProbe healthProbe) { this.healthProbe = healthProbe; } public AppAdmin getAppAdmin() { return appAdmin; } public void setAppAdmin(AppAdmin appAdmin) { this.appAdmin = appAdmin; } public static class HttpProbe { /** Path to check as a probe */ private String path; public String getPath() { return path; } public void setPath(String path) { this.path = path; } } private String deduceJavaCommand() { String javaExecutablePath = JAVA_COMMAND; String javaHome = System.getProperty("java.home"); if (javaHome != null) { File javaExecutable = new File(javaHome, "bin" + File.separator + javaExecutablePath); Assert.isTrue(javaExecutable.canExecute(), "Java executable'" + javaExecutable + "'discovered via 'java.home' system property '" + javaHome + "' is not executable or does not exist."); javaExecutablePath = javaExecutable.getAbsolutePath(); } else { logger.warn("System property 'java.home' is not set. Defaulting to the java executable path as " + JAVA_COMMAND + " assuming it's in PATH."); } return javaExecutablePath; } @Override public String toString() { return new ToStringCreator(this).append("workingDirectoriesRoot", this.workingDirectoriesRoot) .append("javaOpts", this.javaOpts).append("envVarsToInherit", this.envVarsToInherit).toString(); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((debugPort == null) ? 0 : debugPort.hashCode()); result = prime * result + ((debugSuspend == null) ? 0 : debugSuspend.hashCode()); result = prime * result + (deleteFilesOnExit ? 1231 : 1237); result = prime * result + ((docker == null) ? 0 : docker.hashCode()); result = prime * result + Arrays.hashCode(envVarsToInherit); result = prime * result + (inheritLogging ? 1231 : 1237); result = prime * result + ((javaCmd == null) ? 0 : javaCmd.hashCode()); result = prime * result + ((javaOpts == null) ? 0 : javaOpts.hashCode()); result = prime * result + maximumConcurrentTasks; result = prime * result + ((portRange == null) ? 0 : portRange.hashCode()); result = prime * result + shutdownTimeout; result = prime * result + (useSpringApplicationJson ? 1231 : 1237); result = prime * result + ((workingDirectoriesRoot == null) ? 0 : workingDirectoriesRoot.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } LocalDeployerProperties other = (LocalDeployerProperties) obj; if (debugPort == null) { if (other.debugPort != null) { return false; } } else if (!debugPort.equals(other.debugPort)) { return false; } if (debugAddress == null) { if (other.debugAddress != null) { return false; } } else if (!debugAddress.equals(other.debugAddress)) { return false; } if (debugSuspend == null) { if (other.debugSuspend != null) { return false; } } else if (!debugSuspend.equals(other.debugSuspend)) { return false; } if (deleteFilesOnExit != other.deleteFilesOnExit) { return false; } if (docker == null) { if (other.docker != null) { return false; } } else if (!docker.equals(other.docker)) { return false; } if (!Arrays.equals(envVarsToInherit, other.envVarsToInherit)) { return false; } if (inheritLogging != other.inheritLogging) { return false; } if (javaCmd == null) { if (other.javaCmd != null) { return false; } } else if (!javaCmd.equals(other.javaCmd)) { return false; } if (javaOpts == null) { if (other.javaOpts != null) { return false; } } else if (!javaOpts.equals(other.javaOpts)) { return false; } if (maximumConcurrentTasks != other.maximumConcurrentTasks) { return false; } if (portRange == null) { if (other.portRange != null) { return false; } } else if (!portRange.equals(other.portRange)) { return false; } if (shutdownTimeout != other.shutdownTimeout) { return false; } if (useSpringApplicationJson != other.useSpringApplicationJson) { return false; } if (workingDirectoriesRoot == null) { if (other.workingDirectoriesRoot != null) { return false; } } else if (!workingDirectoriesRoot.equals(other.workingDirectoriesRoot)) { return false; } return true; } } ================================================ FILE: src/main/java/org/springframework/cloud/deployer/spi/local/LocalDeployerUtils.java ================================================ /* * Copyright 2017-2018 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.cloud.deployer.spi.local; /** * Deployer utility functions. * * @author Janne Valkealahti * @author Michael Minella * */ public class LocalDeployerUtils { /** * Checks if jvm is running on windows. * * @return true if windows detected */ protected static boolean isWindows() { String osName = System.getProperty("os.name"); return osName != null && osName.toLowerCase(Locale.ROOT).startsWith("windows"); } } ================================================ FILE: src/main/java/org/springframework/cloud/deployer/spi/local/LocalTaskLauncher.java ================================================ /* * Copyright 2016-2022 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.cloud.deployer.spi.local; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.Inet4Address; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import jakarta.annotation.PreDestroy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; import org.springframework.cloud.deployer.spi.task.LaunchState; import org.springframework.cloud.deployer.spi.task.TaskLauncher; import org.springframework.cloud.deployer.spi.task.TaskStatus; import org.springframework.util.FileCopyUtils; import org.springframework.util.StringUtils; /** * A {@link TaskLauncher} implementation that spins off a new JVM process per task launch. * * @author Eric Bottard * @author Marius Bogoevici * @author Mark Fisher * @author Janne Valkealahti * @author Thomas Risberg * @author Oleg Zhurakousky * @author Michael Minella * @author Christian Tzolov * @author David Turanski * @author Glenn Renfro * @author Ben Blinebury */ public class LocalTaskLauncher extends AbstractLocalDeployerSupport implements TaskLauncher { private static final Logger logger = LoggerFactory.getLogger(LocalTaskLauncher.class); private static final String JMX_DEFAULT_DOMAIN_KEY = "spring.jmx.default-domain"; private final Map running = new ConcurrentHashMap<>(); private final Map> taskInstanceHistory = new ConcurrentHashMap<>(); /** * Instantiates a new local task launcher. * * @param properties the properties */ public LocalTaskLauncher(LocalDeployerProperties properties) { super(properties); } @Override public String launch(AppDeploymentRequest request) { if (this.maxConcurrentExecutionsReached()) { throw new IllegalStateException( String.format("Cannot launch task %s. The maximum concurrent task executions is at its limit [%d].", request.getDefinition().getName(), this.getMaximumConcurrentTasks()) ); } String taskLaunchId = request.getDefinition().getName() + "-" + UUID.randomUUID().toString(); pruneTaskInstanceHistory(request.getDefinition().getName(), taskLaunchId); HashMap args = new HashMap<>(); args.putAll(request.getDefinition().getProperties()); args.put(JMX_DEFAULT_DOMAIN_KEY, taskLaunchId); args.put("endpoints.shutdown.enabled", "true"); args.put("endpoints.jmx.unique-names", "true"); try { Path workingDirectory = createWorkingDirectory(request.getDeploymentProperties(), taskLaunchId); boolean useDynamicPort = isDynamicPort(request); int port = calcServerPort(request, useDynamicPort, args); ProcessBuilder builder = buildProcessBuilder(request, args, Optional.empty(), taskLaunchId).inheritIO(); TaskInstance instance = new TaskInstance(builder, workingDirectory, port); if (this.shouldInheritLogging(request)) { instance.start(builder); logger.info("launching task {}\n Logs will be inherited.", taskLaunchId); } else { instance.start(builder, getLocalDeployerProperties().isDeleteFilesOnExit()); logger.info("launching task {}\n Logs will be in {}", taskLaunchId, workingDirectory); } running.put(taskLaunchId, instance); } catch (IOException e) { throw new RuntimeException("Exception trying to launch " + request, e); } return taskLaunchId; } private void pruneTaskInstanceHistory(String taskDefinitionName, String taskLaunchId) { CopyOnWriteArrayList oldTaskInstanceIds = taskInstanceHistory.get(taskDefinitionName); if (oldTaskInstanceIds == null) { oldTaskInstanceIds = new CopyOnWriteArrayList<>(); taskInstanceHistory.put(taskDefinitionName, oldTaskInstanceIds); } for (String oldTaskInstanceId : oldTaskInstanceIds) { TaskInstance oldTaskInstance = running.get(oldTaskInstanceId); if (oldTaskInstance != null && oldTaskInstance.getState() != LaunchState.running && oldTaskInstance.getState() != LaunchState.launching) { running.remove(oldTaskInstanceId); oldTaskInstanceIds.remove(oldTaskInstanceId); } else { oldTaskInstanceIds.remove(oldTaskInstanceId); } } oldTaskInstanceIds.add(taskLaunchId); } private boolean isDynamicPort(AppDeploymentRequest request) { boolean isServerPortKeyonArgs = isServerPortKeyPresentOnArgs(request) != null; return !request.getDefinition().getProperties().containsKey(SERVER_PORT_KEY) && !isServerPortKeyonArgs; } @Override public void cancel(String id) { TaskInstance instance = running.get(id); if (instance != null) { instance.cancelled = true; if (isAlive(instance.getProcess())) { shutdownAndWait(instance); } } } @Override public TaskStatus status(String id) { TaskInstance instance = running.get(id); if (instance != null) { return new TaskStatus(id, instance.getState(), instance.getAttributes()); } return new TaskStatus(id, LaunchState.unknown, null); } @Override public String getLog(String id) { TaskInstance instance = running.get(id); if (instance != null) { StringBuilder stringBuilder = new StringBuilder(); String stderr = instance.getStdErr(); if (StringUtils.hasText(stderr)) { stringBuilder.append("stderr:\n"); stringBuilder.append(stderr); } String stdout = instance.getStdOut(); if (StringUtils.hasText(stdout)) { stringBuilder.append("stdout:\n"); stringBuilder.append(stdout); } return stringBuilder.toString(); } else { return "Log could not be retrieved as the task instance is not running."; } } @Override public void cleanup(String id) { } @Override public void destroy(String appName) { } @Override public RuntimeEnvironmentInfo environmentInfo() { return super.createRuntimeEnvironmentInfo(TaskLauncher.class, this.getClass()); } @Override public int getMaximumConcurrentTasks() { return getLocalDeployerProperties().getMaximumConcurrentTasks(); } @Override public int getRunningTaskExecutionCount() { int runningExecutionCount = 0; for (TaskInstance taskInstance: running.values()) { if (taskInstance.getProcess().isAlive()) { runningExecutionCount++; } } return runningExecutionCount; } private synchronized boolean maxConcurrentExecutionsReached() { return getRunningTaskExecutionCount() >= getMaximumConcurrentTasks(); } @PreDestroy public void shutdown() throws Exception { for (String taskLaunchId : running.keySet()) { cancel(taskLaunchId); } taskInstanceHistory.clear(); } private Path createWorkingDirectory(Map deploymentProperties, String taskLaunchId) throws IOException { LocalDeployerProperties localDeployerPropertiesToUse = bindDeploymentProperties(deploymentProperties); Path workingDirectoryRoot = Files.isSymbolicLink(localDeployerPropertiesToUse.getWorkingDirectoriesRoot()) ? Files.readSymbolicLink(localDeployerPropertiesToUse.getWorkingDirectoriesRoot()) : Files.createDirectories(localDeployerPropertiesToUse.getWorkingDirectoriesRoot()); Path workingDirectory = Files.createDirectories(workingDirectoryRoot.resolve(Long.toString(System.nanoTime())).resolve(taskLaunchId)); if (localDeployerPropertiesToUse.isDeleteFilesOnExit()) { workingDirectory.toFile().deleteOnExit(); } return workingDirectory; } private static class TaskInstance implements Instance { private Process process; private final Path workDir; private File stdout; private File stderr; private final URL baseUrl; private boolean cancelled; private TaskInstance(ProcessBuilder builder, Path workDir, int port) throws IOException { builder.directory(workDir.toFile()); this.workDir = workDir; this.baseUrl = new URL("http", Inet4Address.getLocalHost().getHostAddress(), port, ""); if (logger.isDebugEnabled()) { logger.debug("Local Task Launcher Commands: " + String.join(",", builder.command()) + ", Environment: " + builder.environment()); } } @Override public URL getBaseUrl() { return this.baseUrl; } @Override public Process getProcess() { return this.process; } public LaunchState getState() { if (cancelled) { return LaunchState.cancelled; } Integer exit = getProcessExitValue(process); // TODO: consider using exit code mapper concept from batch if (exit != null) { if (exit == 0) { return LaunchState.complete; } else { return LaunchState.failed; } } try { HttpURLConnection urlConnection = (HttpURLConnection) baseUrl.openConnection(); urlConnection.setConnectTimeout(100); urlConnection.connect(); urlConnection.disconnect(); return LaunchState.running; } catch (IOException e) { return LaunchState.launching; } } public String getStdOut() { try { return FileCopyUtils.copyToString(new InputStreamReader(new FileInputStream(this.stdout))); } catch (IOException e) { return "Log retrieval returned " + e.getMessage(); } } public String getStdErr() { try { return FileCopyUtils.copyToString(new InputStreamReader(new FileInputStream(this.stderr))); } catch (IOException e) { return "Log retrieval returned " + e.getMessage(); } } /** * Will start the process while redirecting 'out' and 'err' streams to the 'out' and 'err' * streams of this process. */ private void start(ProcessBuilder builder) throws IOException { if (logger.isDebugEnabled()) { logger.debug("Local Task Launcher Commands: " + String.join(",", builder.command()) + ", Environment: " + builder.environment()); } this.process = builder.start(); } private void start(ProcessBuilder builder, boolean deleteOnExit) throws IOException { String workDirPath = workDir.toFile().getAbsolutePath(); this.stdout = Files.createFile(Paths.get(workDirPath, "stdout.log")).toFile(); this.stderr = Files.createFile(Paths.get(workDirPath, "stderr.log")).toFile(); builder.redirectOutput(this.stdout); builder.redirectError(this.stderr); this.process = builder.start(); if(deleteOnExit) { this.stdout.deleteOnExit(); this.stderr.deleteOnExit(); } } private Map getAttributes() { HashMap result = new HashMap<>(); result.put("working.dir", workDir.toFile().getAbsolutePath()); if(this.stdout != null) { result.put("stdout", stdout.getAbsolutePath()); } if(this.stderr != null) { result.put("stderr", stderr.getAbsolutePath()); } result.put("url", baseUrl.toString()); return result; } } /** * Returns the process exit value. We explicitly use Integer instead of int * to indicate that if {@code NULL} is returned, the process is still running. * * @param process the process * @return the process exit value or {@code NULL} if process is still alive */ private static Integer getProcessExitValue(Process process) { try { return process.exitValue(); } catch (IllegalThreadStateException e) { // process is still alive return null; } } } ================================================ FILE: src/main/resources/META-INF/additional-spring-configuration-metadata.json ================================================ { "hints": [ { "name": "spring.cloud.deployer.local.debug-suspend", "values": [ { "value": "y" }, { "value": "n" } ] } ] } ================================================ FILE: src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ================================================ org.springframework.cloud.deployer.spi.local.LocalDeployerAutoConfiguration ================================================ FILE: src/main/resources/META-INF/spring.factories ================================================ ================================================ FILE: src/scripts/next-minor-parent-snapshot-version ================================================ #!/bin/sh # ---------------------------------------------------------------------------- # Script maintaining versions # # Bump up next minor development version for parents. # ---------------------------------------------------------------------------- find_basedir() { local basedir=$(cd -P -- "$(dirname -- "$0")" && cd .. && cd .. && pwd -P) echo "${basedir}" } export PROJECTBASEDIR=$(find_basedir) (cd $PROJECTBASEDIR && ./mvnw build-helper:parse-version versions:update-parent -DgenerateBackupPoms=false -DallowSnapshots=true -DparentVersion='${parsedVersion.majorVersion}.${parsedVersion.nextMinorVersion}') ================================================ FILE: src/scripts/next-minor-snapshot-version ================================================ #!/bin/sh # ---------------------------------------------------------------------------- # Script maintaining versions # # Bump up next minor development version. # ---------------------------------------------------------------------------- find_basedir() { local basedir=$(cd -P -- "$(dirname -- "$0")" && cd .. && cd .. && pwd -P) echo "${basedir}" } export PROJECTBASEDIR=$(find_basedir) (cd $PROJECTBASEDIR && ./mvnw build-helper:parse-version versions:set -DprocessAllModules=false -DgenerateBackupPoms=false -DnewVersion='${parsedVersion.majorVersion}.${parsedVersion.nextMinorVersion}.0-SNAPSHOT' && ./mvnw -pl spring-cloud-deployer-dependencies build-helper:parse-version versions:set -DprocessAllModules=false -DgenerateBackupPoms=false -DnewVersion='${parsedVersion.majorVersion}.${parsedVersion.nextMinorVersion}.0-SNAPSHOT') ================================================ FILE: src/scripts/next-parent-snapshot-version ================================================ #!/bin/sh # ---------------------------------------------------------------------------- # Script maintaining versions # # Bump up next build development version for parents. # ---------------------------------------------------------------------------- find_basedir() { local basedir=$(cd -P -- "$(dirname -- "$0")" && cd .. && cd .. && pwd -P) echo "${basedir}" } export PROJECTBASEDIR=$(find_basedir) (cd $PROJECTBASEDIR && ./mvnw build-helper:parse-version versions:update-parent -DgenerateBackupPoms=false -DallowSnapshots=true -DparentVersion='[${parsedVersion.majorVersion}.${parsedVersion.minorVersion},${parsedVersion.majorVersion}.${parsedVersion.nextMinorVersion})') ================================================ FILE: src/scripts/next-snapshot-version ================================================ #!/bin/sh # ---------------------------------------------------------------------------- # Script maintaining versions # # Bump up next build development version. # ---------------------------------------------------------------------------- find_basedir() { local basedir=$(cd -P -- "$(dirname -- "$0")" && cd .. && cd .. && pwd -P) echo "${basedir}" } export PROJECTBASEDIR=$(find_basedir) (cd $PROJECTBASEDIR && ./mvnw build-helper:parse-version versions:set -DprocessAllModules=false -DgenerateBackupPoms=false -DnewVersion='${parsedVersion.majorVersion}.${parsedVersion.minorVersion}.${parsedVersion.nextIncrementalVersion}-SNAPSHOT' && ./mvnw -pl spring-cloud-deployer-dependencies build-helper:parse-version versions:set -DprocessAllModules=false -DgenerateBackupPoms=false -DnewVersion='${parsedVersion.majorVersion}.${parsedVersion.minorVersion}.${parsedVersion.nextIncrementalVersion}-SNAPSHOT') ================================================ FILE: src/test/java/org/springframework/cloud/deployer/spi/kubernetes/DefaultContainerFactoryTests.java ================================================ /* * Copyright 2016-2022 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.cloud.deployer.spi.kubernetes; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.Stream; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import io.fabric8.kubernetes.api.model.Container; import io.fabric8.kubernetes.api.model.ContainerPort; import io.fabric8.kubernetes.api.model.ContainerPortBuilder; import io.fabric8.kubernetes.api.model.EnvVar; import io.fabric8.kubernetes.api.model.HTTPHeader; import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.Probe; import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.api.model.VolumeMount; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.deployer.resource.docker.DockerResource; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.core.io.Resource; import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Unit tests for {@link DefaultContainerFactory}. * * @author Will Kennedy * @author Donovan Muller * @author Chris Schaefer * @author David Turanski * @author Ilayaperumal Gopinathan * @author Glenn Renfro */ @ExtendWith(SpringExtension.class) @SpringBootTest(classes = {KubernetesAutoConfiguration.class}) public class DefaultContainerFactoryTests { @Test public void create() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.limits.memory", "128Mi"); props.put("spring.cloud.deployer.kubernetes.environment-variables", "JAVA_OPTIONS=-Xmx64m,KUBERNETES_NAMESPACE=test-space"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); assertThat(container.getEnv().size()).isEqualTo(3); EnvVar envVar1 = container.getEnv().get(0); EnvVar envVar2 = container.getEnv().get(1); assertThat(envVar1.getName()).isEqualTo("JAVA_OPTIONS"); assertThat(envVar1.getValue()).isEqualTo("-Xmx64m"); assertThat(envVar2.getName()).isEqualTo("KUBERNETES_NAMESPACE"); assertThat(envVar2.getValue()).isEqualTo("test-space"); } @Test public void createWithContainerCommand() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.containerCommand", "echo arg1 'arg2'"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); assertThat(container.getCommand()).containsExactly("echo", "arg1", "arg2"); } @Test public void createWithPorts() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.containerPorts", "8081, 8082, 65535"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); List containerPorts = container.getPorts(); assertThat(containerPorts).isNotNull(); assertThat(containerPorts.size()).isEqualTo(3); assertThat(containerPorts.get(0).getContainerPort()).isEqualTo(8081); assertThat(containerPorts.get(1).getContainerPort()).isEqualTo(8082); assertThat(containerPorts.get(2).getContainerPort()).isEqualTo(65535); } @Test public void createWithInvalidPort() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.containerPorts", "8081, 8082, invalid, 9212"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, props); //Attempting to create with an invalid integer set for a port should cause an exception to bubble up. ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest); assertThatThrownBy(() -> { defaultContainerFactory.create(containerConfiguration); }).isInstanceOf(NumberFormatException.class); } @Test public void createWithPortAndHostNetwork() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.containerPorts", "8081, 8082, 65535"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); List containerPorts = container.getPorts(); assertThat(containerPorts).isNotNull(); assertThat(containerPorts.size()).isEqualTo(3); assertThat(containerPorts.get(0).getContainerPort()).isEqualTo(8081); assertThat(containerPorts.get(0).getHostPort()).isEqualTo(8081); assertThat(containerPorts.get(1).getContainerPort()).isEqualTo(8082); assertThat(containerPorts.get(1).getHostPort()).isEqualTo(8082); assertThat(containerPorts.get(2).getContainerPort()).isEqualTo(65535); assertThat(containerPorts.get(2).getHostPort()).isEqualTo(65535); } @Test public void createWithEntryPointStyle() throws JsonProcessingException { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); Map appProps = new HashMap<>(); appProps.put("foo.bar.baz", "test"); AppDefinition definition = new AppDefinition("app-test", appProps); Resource resource = getResource(); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.entryPointStyle", "shell"); AppDeploymentRequest appDeploymentRequestShell = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration shellContainerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequestShell); Container containerShell = defaultContainerFactory.create(shellContainerConfiguration); assertThat(containerShell).isNotNull(); assertThat(containerShell.getEnv().get(0).getName()).isEqualTo("FOO_BAR_BAZ"); assertThat(containerShell.getArgs()).isEmpty(); List cmdLineArgs = new ArrayList<>(); cmdLineArgs.add("--foo.bar=value1"); cmdLineArgs.add("--spring.cloud.task.executionid=1"); cmdLineArgs.add("--spring.cloud.data.flow.platformname=platform1"); cmdLineArgs.add("--spring.cloud.data.flow.taskappname==a1"); cmdLineArgs.add("blah=chacha"); appDeploymentRequestShell = new AppDeploymentRequest(definition, resource, props, cmdLineArgs); shellContainerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequestShell); containerShell = defaultContainerFactory.create(shellContainerConfiguration); assertThat(containerShell).isNotNull(); assertThat(containerShell.getEnv()).hasSize(7); assertThat(containerShell.getArgs()).isEmpty(); String envVarString = containerShell.getEnv().toString(); assertThat(envVarString.contains("name=FOO_BAR_BAZ, value=test")).isTrue(); assertThat(envVarString.contains("name=FOO_BAR, value=value1")).isTrue(); assertThat(envVarString.contains("name=SPRING_CLOUD_TASK_EXECUTIONID, value=1")).isTrue(); assertThat(envVarString.contains("name=SPRING_CLOUD_DATA_FLOW_TASKAPPNAME, value==a1")).isTrue(); assertThat(envVarString.contains("name=SPRING_CLOUD_DATA_FLOW_PLATFORMNAME, value=platform1")).isTrue(); assertThat(envVarString.contains("name=BLAH, value=chacha")).isTrue(); props.put("spring.cloud.deployer.kubernetes.entryPointStyle", "exec"); AppDeploymentRequest appDeploymentRequestExec = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration execContainerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequestExec); Container containerExec = defaultContainerFactory.create(execContainerConfiguration); assertThat(containerExec).isNotNull(); assertThat(containerExec.getEnv()).hasSize(1); assertThat(containerExec.getArgs().get(0)).isEqualTo("--foo.bar.baz=test"); props.put("spring.cloud.deployer.kubernetes.entryPointStyle", "boot"); AppDeploymentRequest appDeploymentRequestBoot = new AppDeploymentRequest(definition, resource, props, Arrays.asList("--arg1=val1", "--arg2=val2")); ContainerConfiguration bootContainerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequestBoot); Container containerBoot = defaultContainerFactory.create(bootContainerConfiguration); assertThat(containerBoot).isNotNull(); assertThat(containerBoot.getEnv().get(0).getName()).isEqualTo("SPRING_APPLICATION_JSON"); assertThat(containerBoot.getEnv().get(0).getValue()).isEqualTo(new ObjectMapper().writeValueAsString(appProps)); assertThat(containerBoot.getArgs()).hasSize(2); assertThat(containerBoot.getArgs().get(0)).isEqualTo("--arg1=val1"); assertThat(containerBoot.getArgs().get(1)).isEqualTo("--arg2=val2"); } @Test public void createWithVolumeMounts() { // test volume mounts defined as deployer properties KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory(kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.volumeMounts", "[" + "{name: 'testhostpath', mountPath: '/test/hostPath'}, " + "{name: 'testpvc', mountPath: '/test/pvc', readOnly: 'true'}, " + "{name: 'testnfs', mountPath: '/test/nfs'}" + "]"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container.getVolumeMounts()).containsOnly( new VolumeMount("/test/hostPath", null, "testhostpath", null, null, null), new VolumeMount("/test/pvc", null, "testpvc", true, null, null), new VolumeMount("/test/nfs", null, "testnfs", null, null, null)); // test volume mounts defined as app deployment property, overriding the deployer property kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties .setVolumeMounts(Stream.of( new VolumeMount("/test/hostPath", null, "testhostpath", false, null, null), new VolumeMount("/test/pvc", null, "testpvc", true, null, null), new VolumeMount("/test/nfs", null, "testnfs", false, null, null)) .collect(Collectors.toList())); defaultContainerFactory = new DefaultContainerFactory(kubernetesDeployerProperties); props.clear(); props.put("spring.cloud.deployer.kubernetes.volumeMounts", "[" + "{name: 'testpvc', mountPath: '/test/pvc/overridden'}, " + "{name: 'testnfs', mountPath: '/test/nfs/overridden', readOnly: 'true'}" + "]"); containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest); container = defaultContainerFactory.create(containerConfiguration); assertThat(container.getVolumeMounts()).containsOnly( new VolumeMount("/test/hostPath", null, "testhostpath", false, null, null), new VolumeMount("/test/pvc/overridden", null, "testpvc", null, null, null), new VolumeMount("/test/nfs/overridden", null, "testnfs", true, null, null)); } /** * @deprecated {@see {@link #createCustomLivenessHttpPortFromProperties()}} */ @Test @Deprecated public void createCustomLivenessPortFromProperties() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setLivenessProbePort(8090); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); Map props = new HashMap<>(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withExternalPort(8080) .withHostNetwork(true); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); List containerPorts = container.getPorts(); assertThat(containerPorts).isNotNull(); assertThat(containerPorts).as("Only two container ports should be set").hasSize(2); assertThat((int) containerPorts.get(0).getContainerPort()).isEqualTo(8080); assertThat((int) containerPorts.get(0).getHostPort()).isEqualTo(8080); assertThat((int) containerPorts.get(1).getContainerPort()).isEqualTo(8090); assertThat((int) containerPorts.get(1).getHostPort()).isEqualTo(8090); assertThat((int) container.getLivenessProbe().getHttpGet().getPort().getIntVal()).isEqualTo(8090); } @Test public void createCustomLivenessHttpPortFromProperties() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setLivenessHttpProbePort(8090); kubernetesDeployerProperties.setStartupHttpProbePort(kubernetesDeployerProperties.getLivenessHttpProbePort()); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); Map props = new HashMap<>(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withExternalPort(8080) .withHostNetwork(true); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); List containerPorts = container.getPorts(); assertThat(containerPorts).isNotNull(); assertThat(containerPorts).as("Only two container ports should be set").hasSize(2); assertThat((int) containerPorts.get(0).getContainerPort()).isEqualTo(8080); assertThat((int) containerPorts.get(0).getHostPort()).isEqualTo(8080); assertThat((int) containerPorts.get(1).getContainerPort()).isEqualTo(8090); assertThat((int) containerPorts.get(1).getHostPort()).isEqualTo(8090); assertThat((int) container.getLivenessProbe().getHttpGet().getPort().getIntVal()).isEqualTo(8090); } @Test public void createCustomLivenessHttpProbeSchemeFromProperties() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setLivenessHttpProbeScheme("HTTPS"); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); Map props = new HashMap<>(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withExternalPort(8080) .withHostNetwork(true); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); List containerPorts = container.getPorts(); assertThat(containerPorts).isNotNull(); assertThat((int) containerPorts.get(0).getContainerPort()).isEqualTo(8080); assertThat((int) containerPorts.get(0).getHostPort()).isEqualTo(8080); assertThat((int) container.getLivenessProbe().getHttpGet().getPort().getIntVal()).isEqualTo(8080); assertThat(container.getLivenessProbe().getHttpGet().getScheme()).isEqualTo("HTTPS"); } @Test public void createCustomReadinessHttpSchemeFromProperties() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setReadinessHttpProbeScheme("HTTPS"); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); Map props = new HashMap<>(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); List containerPorts = container.getPorts(); assertThat(containerPorts).isNotNull(); assertThat((int) containerPorts.get(0).getContainerPort()).isEqualTo(8080); assertThat((int) containerPorts.get(0).getHostPort()).isEqualTo(8080); assertThat((int) container.getReadinessProbe().getHttpGet().getPort().getIntVal()).isEqualTo(8080); assertThat(container.getReadinessProbe().getHttpGet().getScheme()).isEqualTo("HTTPS"); } @Test public void createCustomLivenessAndReadinessHttpProbeSchemeFromAppRequest() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); Map appProperties = new HashMap<>(); appProperties.put("spring.cloud.deployer.kubernetes.livenessHttpProbeScheme", "HTTPS"); appProperties.put("spring.cloud.deployer.kubernetes.readinessHttpProbeScheme", "HTTPS"); AppDefinition definition = new AppDefinition("app-test", appProperties); Resource resource = getResource(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, appProperties); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); assertThat(container.getReadinessProbe().getHttpGet().getPath()).isNotNull(); assertThat(container.getReadinessProbe().getHttpGet().getScheme()).isEqualTo("HTTPS"); assertThat(container.getLivenessProbe().getHttpGet().getPath()).isNotNull(); assertThat(container.getLivenessProbe().getHttpGet().getScheme()).isEqualTo("HTTPS"); } /** * @deprecated {@see {@link #createCustomLivenessHttpPortFromAppRequest()}} */ @Test @Deprecated public void createCustomLivenessPortFromAppRequest() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.liveness-probe-port", Integer.toString(8090)); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); List containerPorts = container.getPorts(); assertThat(containerPorts).isNotNull(); assertThat(containerPorts).hasSize(2); assertThat(containerPorts.get(0).getContainerPort()).isEqualTo(8080); assertThat(containerPorts.get(0).getHostPort()).isEqualTo(8080); assertThat(containerPorts.get(1).getContainerPort()).isEqualTo(8090); assertThat(containerPorts.get(1).getHostPort()).isEqualTo(8090); assertThat(container.getLivenessProbe().getHttpGet().getPort().getIntVal()).isEqualTo(8090); } @Test public void createCustomLivenessHttpPortFromAppRequest() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.liveness-http-probe-port", Integer.toString(8090)); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); List containerPorts = container.getPorts(); assertThat(containerPorts).isNotNull(); assertThat(containerPorts).hasSize(2); assertThat(containerPorts.get(0).getContainerPort()).isEqualTo(8080); assertThat(containerPorts.get(0).getHostPort()).isEqualTo(8080); assertThat(containerPorts.get(1).getContainerPort()).isEqualTo(8090); assertThat(containerPorts.get(1).getHostPort()).isEqualTo(8090); assertThat(container.getLivenessProbe().getHttpGet().getPort().getIntVal()).isEqualTo(8090); } /** * @deprecated {@see {@link #createCustomReadinessHttpPortFromAppRequest()}} */ @Test @Deprecated public void createCustomReadinessPortFromAppRequest() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.readinessProbePort", Integer.toString(8090)); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); List containerPorts = container.getPorts(); assertThat(containerPorts).isNotNull(); assertThat(containerPorts).hasSize(2); assertThat(containerPorts.get(0).getContainerPort()).isEqualTo(8080); assertThat(containerPorts.get(0).getHostPort()).isEqualTo(8080); assertThat(containerPorts.get(1).getContainerPort()).isEqualTo(8090); assertThat(containerPorts.get(1).getHostPort()).isEqualTo(8090); assertThat(container.getReadinessProbe().getHttpGet().getPort().getIntVal()).isEqualTo(8090); } @Test public void createCustomReadinessHttpPortFromAppRequest() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.readinessHttpProbePort", Integer.toString(8090)); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); List containerPorts = container.getPorts(); assertThat(containerPorts).isNotNull(); assertThat(containerPorts).hasSize(2); assertThat(containerPorts.get(0).getContainerPort()).isEqualTo(8080); assertThat(containerPorts.get(0).getHostPort()).isEqualTo(8080); assertThat(containerPorts.get(1).getContainerPort()).isEqualTo(8090); assertThat(containerPorts.get(1).getHostPort()).isEqualTo(8090); assertThat(container.getReadinessProbe().getHttpGet().getPort().getIntVal()).isEqualTo(8090); } /** * @deprecated {@see {@link #createCustomReadinessHttpPortFromProperties()}} */ @Test @Deprecated public void createCustomReadinessPortFromProperties() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setReadinessProbePort(8090); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); Map props = new HashMap<>(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); List containerPorts = container.getPorts(); assertThat(containerPorts).isNotNull(); assertThat(containerPorts).hasSize(2); assertThat(containerPorts.get(0).getContainerPort()).isEqualTo(8080); assertThat(containerPorts.get(0).getHostPort()).isEqualTo(8080); assertThat(containerPorts.get(1).getContainerPort()).isEqualTo(8090); assertThat(containerPorts.get(1).getHostPort()).isEqualTo(8090); assertThat(container.getReadinessProbe().getHttpGet().getPort().getIntVal()).isEqualTo(8090); } @Test public void createCustomReadinessHttpPortFromProperties() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setReadinessHttpProbePort(8090); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); Map props = new HashMap<>(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); List containerPorts = container.getPorts(); assertThat(containerPorts).isNotNull(); assertThat(containerPorts).hasSize(2); assertThat(containerPorts.get(0).getContainerPort()).isEqualTo(8080); assertThat(containerPorts.get(0).getHostPort()).isEqualTo(8080); assertThat(containerPorts.get(1).getContainerPort()).isEqualTo(8090); assertThat(containerPorts.get(1).getHostPort()).isEqualTo(8090); assertThat(container.getReadinessProbe().getHttpGet().getPort().getIntVal()).isEqualTo(8090); } @Test public void createDefaultHttpProbePorts() { int defaultPort = 8080; KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); Map props = new HashMap<>(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(defaultPort); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); List containerPorts = container.getPorts(); assertThat(containerPorts).isNotNull(); assertThat(containerPorts).as("Only the default container port should set").hasSize(1); assertThat((int) containerPorts.get(0).getContainerPort()).isEqualTo(8080); assertThat((int) containerPorts.get(0).getHostPort()).isEqualTo(8080); assertThat((int) container.getLivenessProbe().getHttpGet().getPort().getIntVal()).isEqualTo(8080); assertThat((int) container.getReadinessProbe().getHttpGet().getPort().getIntVal()).isEqualTo(8080); } @Test public void createHttpProbesWithDefaultEndpoints() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); Map props = new HashMap<>(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, props); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); assertThat(container.getReadinessProbe().getHttpGet().getPath()).isNotNull(); assertThat(container.getReadinessProbe().getHttpGet().getPath()).isEqualTo(HttpProbeCreator.BOOT_2_READINESS_PROBE_PATH); assertThat(container.getLivenessProbe().getHttpGet().getPath()).isNotNull(); assertThat(container.getLivenessProbe().getHttpGet().getPath()).isEqualTo(HttpProbeCreator.BOOT_2_LIVENESS_PROBE_PATH); } @Test public void createHttpProbesWithBoot1Endpoints() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); Map appProperties = new HashMap<>(); appProperties.put("spring.cloud.deployer.kubernetes.boot-major-version", "1"); AppDefinition definition = new AppDefinition("app-test", appProperties); Resource resource = getResource(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, appProperties); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); assertThat(container.getReadinessProbe().getHttpGet().getPath()).isNotNull(); assertThat(container.getReadinessProbe().getHttpGet().getPath()).isEqualTo(HttpProbeCreator.BOOT_1_READINESS_PROBE_PATH); assertThat(container.getLivenessProbe().getHttpGet().getPath()).isNotNull(); assertThat(container.getLivenessProbe().getHttpGet().getPath()).isEqualTo(HttpProbeCreator.BOOT_1_LIVENESS_PROBE_PATH); } /** * @deprecated {@see {@link #createHttpProbesWithOverrides()}} */ @Test @Deprecated public void createProbesWithOverrides() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); Map appProperties = new HashMap<>(); appProperties.put("spring.cloud.deployer.kubernetes.livenessProbePath", "/liveness"); appProperties.put("spring.cloud.deployer.kubernetes.readinessProbePath", "/readiness"); AppDefinition definition = new AppDefinition("app-test", appProperties); Resource resource = getResource(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, appProperties); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); assertThat(container.getReadinessProbe().getHttpGet().getPath()).isNotNull(); assertThat(container.getReadinessProbe().getHttpGet().getPath()).isEqualTo("/readiness"); assertThat(container.getLivenessProbe().getHttpGet().getPath()).isNotNull(); assertThat(container.getLivenessProbe().getHttpGet().getPath()).isEqualTo("/liveness"); } @Test public void createHttpProbesWithOverrides() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); Map appProperties = new HashMap<>(); appProperties.put("spring.cloud.deployer.kubernetes.livenessHttpProbePath", "/liveness"); appProperties.put("spring.cloud.deployer.kubernetes.readinessHttpProbePath", "/readiness"); AppDefinition definition = new AppDefinition("app-test", appProperties); Resource resource = getResource(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, appProperties); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); assertThat(container.getReadinessProbe().getHttpGet().getPath()).isNotNull(); assertThat(container.getReadinessProbe().getHttpGet().getPath()).isEqualTo("/readiness"); assertThat(container.getLivenessProbe().getHttpGet().getPath()).isNotNull(); assertThat(container.getLivenessProbe().getHttpGet().getPath()).isEqualTo("/liveness"); } /** * @deprecated {@see {@link #createHttpProbesWithPropertyOverrides()}} */ @Test @Deprecated public void createProbesWithPropertyOverrides() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setReadinessProbePath("/readiness"); kubernetesDeployerProperties.setLivenessProbePath("/liveness"); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, null); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); assertThat(container.getReadinessProbe().getHttpGet().getPath()).isNotNull(); assertThat(container.getReadinessProbe().getHttpGet().getPath()).isEqualTo("/readiness"); assertThat(container.getLivenessProbe().getHttpGet().getPath()).isNotNull(); assertThat(container.getLivenessProbe().getHttpGet().getPath()).isEqualTo("/liveness"); } @Test public void createHttpProbesWithPropertyOverrides() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setReadinessHttpProbePath("/readiness"); kubernetesDeployerProperties.setLivenessHttpProbePath("/liveness"); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, null); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); assertThat(container.getReadinessProbe().getHttpGet().getPath()).isNotNull(); assertThat(container.getReadinessProbe().getHttpGet().getPath()).isEqualTo("/readiness"); assertThat(container.getLivenessProbe().getHttpGet().getPath()).isNotNull(); assertThat(container.getLivenessProbe().getHttpGet().getPath()).isEqualTo("/liveness"); } @Test public void testHttpProbeCredentialsSecret() { Secret secret = randomSecret(); String secretName = secret.getMetadata().getName(); Map appProperties = new HashMap<>(); appProperties.put("spring.cloud.deployer.kubernetes.probeCredentialsSecret", secretName); AppDefinition definition = new AppDefinition("app-test", appProperties); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), appProperties); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withExternalPort(8080) .withProbeCredentialsSecret(secret); ContainerFactory containerFactory = new DefaultContainerFactory(new KubernetesDeployerProperties()); Container container = containerFactory.create(containerConfiguration); String credentials = containerConfiguration.getProbeCredentialsSecret().getData() .get(HttpProbeCreator.PROBE_CREDENTIALS_SECRET_KEY_NAME); HTTPHeader livenessProbeHeader = container.getLivenessProbe().getHttpGet().getHttpHeaders().get(0); assertThat(livenessProbeHeader.getName()).isEqualTo(HttpProbeCreator.AUTHORIZATION_HEADER_NAME); assertThat(livenessProbeHeader.getValue()).isEqualTo(ProbeAuthenticationType.Basic.name() + " " + credentials); HTTPHeader readinessProbeHeader = container.getReadinessProbe().getHttpGet().getHttpHeaders().get(0); assertThat(readinessProbeHeader.getName()).isEqualTo(HttpProbeCreator.AUTHORIZATION_HEADER_NAME); assertThat(readinessProbeHeader.getValue()).isEqualTo(ProbeAuthenticationType.Basic.name() + " " + credentials); } @Test public void testHttpProbeCredentialsInvalidSecret() { Secret secret = randomSecret(); secret.setData(Collections.singletonMap("unexpectedkey", "dXNlcjpwYXNz")); String secretName = secret.getMetadata().getName(); Map appProperties = new HashMap<>(); appProperties.put("spring.cloud.deployer.kubernetes.probeCredentialsSecret", secretName); AppDefinition definition = new AppDefinition("app-test", appProperties); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), appProperties); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withExternalPort(8080) .withProbeCredentialsSecret(secret); ContainerFactory containerFactory = new DefaultContainerFactory(new KubernetesDeployerProperties()); assertThatThrownBy(() -> { containerFactory.create(containerConfiguration); }).isInstanceOf(IllegalArgumentException.class); } @Test public void testHttpProbeHeadersWithoutAuth() { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource()); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withExternalPort(8080); ContainerFactory containerFactory = new DefaultContainerFactory(new KubernetesDeployerProperties()); Container container = containerFactory.create(containerConfiguration); assertThat(container.getLivenessProbe().getHttpGet().getHttpHeaders()).isEmpty(); assertThat(container.getReadinessProbe().getHttpGet().getHttpHeaders()).isEmpty(); } @Test public void createTcpProbe() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); Map appProperties = new HashMap<>(); appProperties.put("spring.cloud.deployer.kubernetes.probeType", ProbeType.TCP.name()); appProperties.put("spring.cloud.deployer.kubernetes.readinessTcpProbePort", "5050"); appProperties.put("spring.cloud.deployer.kubernetes.livenessTcpProbePort", "9090"); appProperties.put("spring.cloud.deployer.kubernetes.startupTcpProbePort", "9090"); AppDefinition definition = new AppDefinition("app-test", appProperties); Resource resource = getResource(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, appProperties); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); assertThat(container.getPorts()) .contains(new ContainerPortBuilder().withHostPort(5050).withContainerPort(5050).build()); assertThat(container.getPorts()) .contains(new ContainerPortBuilder().withHostPort(9090).withContainerPort(9090).build()); Probe livenessProbe = container.getLivenessProbe(); Probe readinessProbe = container.getReadinessProbe(); assertThat(livenessProbe).isNotNull(); assertThat(readinessProbe).isNotNull(); Integer livenessTcpProbePort = livenessProbe.getTcpSocket().getPort().getIntVal(); assertThat(livenessTcpProbePort).isNotNull(); assertThat(livenessTcpProbePort.intValue()).isEqualTo(9090); Integer readinessTcpProbePort = readinessProbe.getTcpSocket().getPort().getIntVal(); assertThat(readinessTcpProbePort).isNotNull(); assertThat(readinessTcpProbePort.intValue()).isEqualTo(5050); assertThat(livenessProbe.getPeriodSeconds()).isNotNull(); assertThat(readinessProbe.getPeriodSeconds()).isNotNull(); assertThat(livenessProbe.getInitialDelaySeconds()).isNotNull(); assertThat(readinessProbe.getInitialDelaySeconds()).isNotNull(); } @Test public void createTcpProbeMissingLivenessPort() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); Map appProperties = new HashMap<>(); appProperties.put("spring.cloud.deployer.kubernetes.probeType", ProbeType.TCP.name()); appProperties.put("spring.cloud.deployer.kubernetes.readinessTcpProbePort", "5050"); appProperties.put("spring.cloud.deployer.kubernetes.startupTcpProbePort", "9090"); AppDefinition definition = new AppDefinition("app-test", appProperties); Resource resource = getResource(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, appProperties); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); assertThatThrownBy(() -> { defaultContainerFactory.create(containerConfiguration); }).isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("The livenessTcpProbePort property must be set"); } @Test public void createTcpProbeMissingReadinessPort() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); Map appProperties = new HashMap<>(); appProperties.put("spring.cloud.deployer.kubernetes.probeType", ProbeType.TCP.name()); appProperties.put("spring.cloud.deployer.kubernetes.livenessTcpProbePort", "5050"); appProperties.put("spring.cloud.deployer.kubernetes.startupTcpProbePort", "5050"); AppDefinition definition = new AppDefinition("app-test", appProperties); Resource resource = getResource(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, appProperties); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); assertThatThrownBy(() -> { defaultContainerFactory.create(containerConfiguration); }).isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("A readinessTcpProbePort property must be set"); } @Test public void createReadinessTcpProbeWithNonDigitPort() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); Map appProperties = new HashMap<>(); appProperties.put("spring.cloud.deployer.kubernetes.probeType", ProbeType.TCP.name()); appProperties.put("spring.cloud.deployer.kubernetes.livenessTcpProbePort", "9090"); appProperties.put("spring.cloud.deployer.kubernetes.readinessTcpProbePort", "somePort"); appProperties.put("spring.cloud.deployer.kubernetes.startupTcpProbePort", "9090"); AppDefinition definition = new AppDefinition("app-test", appProperties); Resource resource = getResource(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, appProperties); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); assertThatThrownBy(() -> { defaultContainerFactory.create(containerConfiguration); }).isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("ReadinessTcpProbePort must contain all digits"); } @Test public void createLivenessTcpProbeWithNonDigitPort() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); Map appProperties = new HashMap<>(); appProperties.put("spring.cloud.deployer.kubernetes.probeType", ProbeType.TCP.name()); appProperties.put("spring.cloud.deployer.kubernetes.livenessTcpProbePort", "somePort"); appProperties.put("spring.cloud.deployer.kubernetes.readinessTcpProbePort", "5050"); appProperties.put("spring.cloud.deployer.kubernetes.startupTcpProbePort", "9090"); AppDefinition definition = new AppDefinition("app-test", appProperties); Resource resource = getResource(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, appProperties); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); assertThatThrownBy(() -> { defaultContainerFactory.create(containerConfiguration); }).isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("LivenessTcpProbePort must contain all digits"); } @Test public void createTcpProbeGlobalProperties() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setProbeType(ProbeType.TCP); kubernetesDeployerProperties.setReadinessTcpProbePort(5050); kubernetesDeployerProperties.setReadinessTcpProbeDelay(1); kubernetesDeployerProperties.setReadinessTcpProbePeriod(2); kubernetesDeployerProperties.setLivenessTcpProbePort(9090); kubernetesDeployerProperties.setLivenessTcpProbeDelay(3); kubernetesDeployerProperties.setLivenessTcpProbePeriod(4); kubernetesDeployerProperties.setStartupTcpProbePort(9090); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, null); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); assertThat(container.getPorts()) .contains(new ContainerPortBuilder().withHostPort(5050).withContainerPort(5050).build()); assertThat(container.getPorts()) .contains(new ContainerPortBuilder().withHostPort(9090).withContainerPort(9090).build()); Probe livenessProbe = container.getLivenessProbe(); Probe readinessProbe = container.getReadinessProbe(); assertThat(livenessProbe).isNotNull(); assertThat(readinessProbe).isNotNull(); Integer livenessTcpProbePort = livenessProbe.getTcpSocket().getPort().getIntVal(); assertThat(livenessTcpProbePort).isNotNull(); assertThat(livenessTcpProbePort.intValue()).isEqualTo(9090); Integer readinessTcpProbePort = readinessProbe.getTcpSocket().getPort().getIntVal(); assertThat(readinessTcpProbePort).isNotNull(); assertThat(readinessTcpProbePort.intValue()).isEqualTo(5050); Integer livenessProbePeriodSeconds = livenessProbe.getPeriodSeconds(); assertThat(livenessProbePeriodSeconds).isNotNull(); assertThat(livenessProbePeriodSeconds.intValue()).isEqualTo(4); Integer readinessProbePeriodSeconds = readinessProbe.getPeriodSeconds(); assertThat(readinessProbePeriodSeconds).isNotNull(); assertThat(readinessProbePeriodSeconds.intValue()).isEqualTo(2); Integer livenessProbeInitialDelaySeconds = livenessProbe.getInitialDelaySeconds(); assertThat(livenessProbeInitialDelaySeconds).isNotNull(); assertThat(livenessProbeInitialDelaySeconds.intValue()).isEqualTo(3); Integer readinessProbeInitialDelaySeconds = readinessProbe.getInitialDelaySeconds(); assertThat(readinessProbeInitialDelaySeconds).isNotNull(); assertThat(readinessProbeInitialDelaySeconds.intValue()).isEqualTo(1); } @Test public void createTcpProbeGlobalPropertyOverride() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setProbeType(ProbeType.TCP); kubernetesDeployerProperties.setReadinessTcpProbePort(5050); kubernetesDeployerProperties.setReadinessTcpProbeDelay(1); kubernetesDeployerProperties.setReadinessTcpProbePeriod(2); kubernetesDeployerProperties.setLivenessTcpProbePort(9090); kubernetesDeployerProperties.setLivenessTcpProbeDelay(3); kubernetesDeployerProperties.setLivenessTcpProbePeriod(4); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); Map appProperties = new HashMap<>(); appProperties.put("spring.cloud.deployer.kubernetes.probeType", ProbeType.TCP.name()); appProperties.put("spring.cloud.deployer.kubernetes.readinessTcpProbePort", "5050"); appProperties.put("spring.cloud.deployer.kubernetes.readinessProbePeriod", "11"); appProperties.put("spring.cloud.deployer.kubernetes.readinessProbeDelay", "12"); appProperties.put("spring.cloud.deployer.kubernetes.livenessTcpProbePort", "9090"); appProperties.put("spring.cloud.deployer.kubernetes.livenessProbePeriod", "13"); appProperties.put("spring.cloud.deployer.kubernetes.livenessProbeDelay", "14"); appProperties.put("spring.cloud.deployer.kubernetes.startupTcpProbePort", "9090"); AppDefinition definition = new AppDefinition("app-test", appProperties); Resource resource = getResource(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, appProperties); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); assertThat(container.getPorts()) .contains(new ContainerPortBuilder().withHostPort(5050).withContainerPort(5050).build()); assertThat(container.getPorts()) .contains(new ContainerPortBuilder().withHostPort(9090).withContainerPort(9090).build()); Probe livenessProbe = container.getLivenessProbe(); Probe readinessProbe = container.getReadinessProbe(); assertThat(livenessProbe).isNotNull(); assertThat(readinessProbe).isNotNull(); Integer livenessTcpProbePort = livenessProbe.getTcpSocket().getPort().getIntVal(); assertThat(livenessTcpProbePort).isNotNull(); assertThat(livenessTcpProbePort.intValue()).isEqualTo(9090); Integer readinessTcpProbePort = readinessProbe.getTcpSocket().getPort().getIntVal(); assertThat(readinessTcpProbePort).isNotNull(); assertThat(readinessTcpProbePort.intValue()).isEqualTo(5050); Integer livenessProbePeriodSeconds = livenessProbe.getPeriodSeconds(); assertThat(livenessProbePeriodSeconds).isNotNull(); assertThat(livenessProbePeriodSeconds.intValue()).isEqualTo(13); Integer readinessProbePeriodSeconds = readinessProbe.getPeriodSeconds(); assertThat(readinessProbePeriodSeconds).isNotNull(); assertThat(readinessProbePeriodSeconds.intValue()).isEqualTo(11); Integer livenessProbeInitialDelaySeconds = livenessProbe.getInitialDelaySeconds(); assertThat(livenessProbeInitialDelaySeconds).isNotNull(); assertThat(livenessProbeInitialDelaySeconds.intValue()).isEqualTo(14); Integer readinessProbeInitialDelaySeconds = readinessProbe.getInitialDelaySeconds(); assertThat(readinessProbeInitialDelaySeconds).isNotNull(); assertThat(readinessProbeInitialDelaySeconds.intValue()).isEqualTo(12); } @Test public void createCommandProbe() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); Map appProperties = new HashMap<>(); appProperties.put("spring.cloud.deployer.kubernetes.probeType", ProbeType.COMMAND.name()); appProperties.put("spring.cloud.deployer.kubernetes.readinessCommandProbeCommand", "ls /"); appProperties.put("spring.cloud.deployer.kubernetes.livenessCommandProbeCommand", "ls /dev"); appProperties.put("spring.cloud.deployer.kubernetes.startupCommandProbeCommand", "ls /dev"); AppDefinition definition = new AppDefinition("app-test", appProperties); Resource resource = getResource(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, appProperties); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); Probe livenessProbe = container.getLivenessProbe(); Probe readinessProbe = container.getReadinessProbe(); assertThat(livenessProbe).isNotNull(); assertThat(readinessProbe).isNotNull(); List livenessCommandProbeCommand = livenessProbe.getExec().getCommand(); assertThat(livenessCommandProbeCommand).containsExactly("ls", "/dev"); List readinessCommandProbeCommand = readinessProbe.getExec().getCommand(); assertThat(readinessCommandProbeCommand).containsExactly("ls", "/"); assertThat(livenessProbe.getPeriodSeconds()).isNotNull(); assertThat(readinessProbe.getPeriodSeconds()).isNotNull(); assertThat(livenessProbe.getInitialDelaySeconds()).isNotNull(); assertThat(readinessProbe.getInitialDelaySeconds()).isNotNull(); } @Test public void createCommandProbeMissingCommand() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); Map appProperties = new HashMap<>(); appProperties.put("spring.cloud.deployer.kubernetes.probeType", ProbeType.COMMAND.name()); appProperties.put("spring.cloud.deployer.kubernetes.startupCommandProbeCommand", "ls /"); AppDefinition definition = new AppDefinition("app-test", appProperties); Resource resource = getResource(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, appProperties); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); assertThatThrownBy(() -> { defaultContainerFactory.create(containerConfiguration); }).isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("The readinessCommandProbeCommand property must be set"); } @Test public void createCommandProbeGlobalProperties() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setProbeType(ProbeType.COMMAND); kubernetesDeployerProperties.setReadinessCommandProbeCommand("ls /"); kubernetesDeployerProperties.setReadinessCommandProbeDelay(1); kubernetesDeployerProperties.setReadinessCommandProbePeriod(2); kubernetesDeployerProperties.setLivenessCommandProbeCommand("ls /dev"); kubernetesDeployerProperties.setLivenessCommandProbeDelay(3); kubernetesDeployerProperties.setLivenessCommandProbePeriod(4); kubernetesDeployerProperties.setStartupCommandProbeCommand("ls /dev"); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); AppDefinition definition = new AppDefinition("app-test", null); Resource resource = getResource(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, null); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); Probe livenessProbe = container.getLivenessProbe(); Probe readinessProbe = container.getReadinessProbe(); assertThat(livenessProbe).isNotNull(); assertThat(readinessProbe).isNotNull(); String livenessTcpProbeCommand = String.join(" ", livenessProbe.getExec().getCommand()); assertThat(livenessTcpProbeCommand).isNotNull(); assertThat(livenessTcpProbeCommand).isEqualTo("ls /dev"); String readinessTcpProbeCommand = String.join(" ", readinessProbe.getExec().getCommand()); assertThat(readinessTcpProbeCommand).isNotNull(); assertThat(readinessTcpProbeCommand).isEqualTo("ls /"); Integer livenessProbePeriodSeconds = livenessProbe.getPeriodSeconds(); assertThat(livenessProbePeriodSeconds).isNotNull(); assertThat(livenessProbePeriodSeconds.intValue()).isEqualTo(4); Integer readinessProbePeriodSeconds = readinessProbe.getPeriodSeconds(); assertThat(readinessProbePeriodSeconds).isNotNull(); assertThat(readinessProbePeriodSeconds.intValue()).isEqualTo(2); Integer livenessProbeInitialDelaySeconds = livenessProbe.getInitialDelaySeconds(); assertThat(livenessProbeInitialDelaySeconds).isNotNull(); assertThat(livenessProbeInitialDelaySeconds.intValue()).isEqualTo(3); Integer readinessProbeInitialDelaySeconds = readinessProbe.getInitialDelaySeconds(); assertThat(readinessProbeInitialDelaySeconds).isNotNull(); assertThat(readinessProbeInitialDelaySeconds.intValue()).isEqualTo(1); } @Test public void createCommandProbeGlobalPropertyOverride() { KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setProbeType(ProbeType.COMMAND); kubernetesDeployerProperties.setReadinessCommandProbeCommand("ls /"); kubernetesDeployerProperties.setReadinessCommandProbeDelay(1); kubernetesDeployerProperties.setReadinessCommandProbePeriod(2); kubernetesDeployerProperties.setLivenessCommandProbeCommand("ls /dev"); kubernetesDeployerProperties.setLivenessCommandProbeDelay(3); kubernetesDeployerProperties.setLivenessCommandProbePeriod(4); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); Map appProperties = new HashMap<>(); appProperties.put("spring.cloud.deployer.kubernetes.probeType", ProbeType.COMMAND.name()); appProperties.put("spring.cloud.deployer.kubernetes.readinessCommandProbeCommand", "ls /"); appProperties.put("spring.cloud.deployer.kubernetes.readinessProbePeriod", "11"); appProperties.put("spring.cloud.deployer.kubernetes.readinessProbeDelay", "12"); appProperties.put("spring.cloud.deployer.kubernetes.livenessCommandProbeCommand", "ls /dev"); appProperties.put("spring.cloud.deployer.kubernetes.livenessProbePeriod", "13"); appProperties.put("spring.cloud.deployer.kubernetes.livenessProbeDelay", "14"); appProperties.put("spring.cloud.deployer.kubernetes.startupCommandProbeCommand", "ls /dev"); AppDefinition definition = new AppDefinition("app-test", appProperties); Resource resource = getResource(); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, resource, appProperties); ContainerConfiguration containerConfiguration = new ContainerConfiguration("app-test", appDeploymentRequest) .withHostNetwork(true) .withExternalPort(8080); Container container = defaultContainerFactory.create(containerConfiguration); assertThat(container).isNotNull(); Probe livenessProbe = container.getLivenessProbe(); Probe readinessProbe = container.getReadinessProbe(); assertThat(livenessProbe).isNotNull(); assertThat(readinessProbe).isNotNull(); String livenessTcpProbeCommand = String.join(" ", livenessProbe.getExec().getCommand()); assertThat(livenessTcpProbeCommand).isNotNull(); assertThat(livenessTcpProbeCommand).isEqualTo("ls /dev"); String readinessTcpProbeCommand = String.join(" ", readinessProbe.getExec().getCommand()); assertThat(readinessTcpProbeCommand).isNotNull(); assertThat(readinessTcpProbeCommand).isEqualTo("ls /"); Integer livenessProbePeriodSeconds = livenessProbe.getPeriodSeconds(); assertThat(livenessProbePeriodSeconds).isNotNull(); assertThat(livenessProbePeriodSeconds.intValue()).isEqualTo(13); Integer readinessProbePeriodSeconds = readinessProbe.getPeriodSeconds(); assertThat(readinessProbePeriodSeconds).isNotNull(); assertThat(readinessProbePeriodSeconds.intValue()).isEqualTo(11); Integer livenessProbeInitialDelaySeconds = livenessProbe.getInitialDelaySeconds(); assertThat(livenessProbeInitialDelaySeconds).isNotNull(); assertThat(livenessProbeInitialDelaySeconds.intValue()).isEqualTo(14); Integer readinessProbeInitialDelaySeconds = readinessProbe.getInitialDelaySeconds(); assertThat(readinessProbeInitialDelaySeconds).isNotNull(); assertThat(readinessProbeInitialDelaySeconds.intValue()).isEqualTo(12); } @Test public void testCommandLineArgsOverridesExistingProperties() { AppDefinition definition = new AppDefinition("app-test", Collections.singletonMap("foo", "bar")); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), null, Collections.singletonList("--foo=newValue")); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); assertThat(defaultContainerFactory.createCommandArgs(appDeploymentRequest)).containsExactly("--foo=newValue"); } @Test public void testCommandLineArgsNoAssignment() { List args = new ArrayList(); args.add("a"); args.add("--b = c"); args.add("d=e"); args.add("f = g"); AppDefinition definition = new AppDefinition("app-test", Collections.singletonMap("b", "d")); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), null, args); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); assertThat(defaultContainerFactory.createCommandArgs(appDeploymentRequest)).containsExactly("a", "--b = c", "d=e", "f = g"); } @Test public void testCommandLineArgsExcludesMalformedProperties() { Map properties = new HashMap<>(); properties.put("sun.cpu.isalist", ""); properties.put("foo", "bar"); AppDefinition definition = new AppDefinition("app-test", properties); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource()); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); DefaultContainerFactory defaultContainerFactory = new DefaultContainerFactory( kubernetesDeployerProperties); assertThat(defaultContainerFactory.createCommandArgs(appDeploymentRequest)).containsExactly("--foo=bar"); } private Resource getResource() { return new DockerResource( "springcloud/spring-cloud-deployer-spi-test-app:latest"); } private Secret randomSecret() { String secretName = "secret-" + UUID.randomUUID().toString().substring(0, 18); String secretValue = "dXNlcjpwYXNz"; // base64 encoded string of: user:pass ObjectMeta objectMeta = new ObjectMeta(); objectMeta.setName(secretName); Secret secret = new Secret(); secret.setData(Collections.singletonMap(HttpProbeCreator.PROBE_CREDENTIALS_SECRET_KEY_NAME, secretValue)); secret.setMetadata(objectMeta); return secret; } } ================================================ FILE: src/test/java/org/springframework/cloud/deployer/spi/kubernetes/KubernetesAppDeployerIntegrationIT.java ================================================ /* * Copyright 2016-2023 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.cloud.deployer.spi.kubernetes; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.lang.reflect.Type; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.stream.Collectors; import java.util.stream.Stream; import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.Container; import io.fabric8.kubernetes.api.model.ContainerBuilder; import io.fabric8.kubernetes.api.model.EnvFromSource; import io.fabric8.kubernetes.api.model.EnvVar; import io.fabric8.kubernetes.api.model.HostPathVolumeSource; import io.fabric8.kubernetes.api.model.HostPathVolumeSourceBuilder; import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.PersistentVolumeClaim; import io.fabric8.kubernetes.api.model.PersistentVolumeClaimSpec; import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.api.model.PodSecurityContext; import io.fabric8.kubernetes.api.model.PodSecurityContextBuilder; import io.fabric8.kubernetes.api.model.PodSpec; import io.fabric8.kubernetes.api.model.SELinuxOptions; import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.api.model.SecurityContext; import io.fabric8.kubernetes.api.model.SecurityContextBuilder; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.ServiceAccount; import io.fabric8.kubernetes.api.model.ServiceAccountBuilder; import io.fabric8.kubernetes.api.model.ServicePort; import io.fabric8.kubernetes.api.model.Volume; import io.fabric8.kubernetes.api.model.VolumeBuilder; import io.fabric8.kubernetes.api.model.VolumeMount; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.StatefulSet; import io.fabric8.kubernetes.api.model.apps.StatefulSetSpec; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.dsl.ExecListener; import io.fabric8.kubernetes.client.dsl.ExecWatch; import org.assertj.core.api.Assertions; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.mockito.stubbing.Answer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.deployer.resource.docker.DockerResource; import org.springframework.cloud.deployer.spi.app.AppAdmin; import org.springframework.cloud.deployer.spi.app.AppDeployer; import org.springframework.cloud.deployer.spi.app.AppScaleRequest; import org.springframework.cloud.deployer.spi.app.AppStatus; import org.springframework.cloud.deployer.spi.app.DeploymentState; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.test.AbstractAppDeployerIntegrationJUnit5Tests; import org.springframework.cloud.deployer.spi.test.Timeout; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.io.Resource; import org.springframework.http.HttpEntity; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.util.StringUtils; import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestTemplate; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.entry; import static org.awaitility.Awaitility.await; import static org.mockito.Mockito.doAnswer; /** * Integration tests for {@link KubernetesAppDeployer}. * * @author Thomas Risberg * @author Donovan Muller * @author David Turanski * @author Chris Schaefer * @author Christian Tzolov * @author Chris Bono */ @SpringBootTest(classes = {KubernetesAutoConfiguration.class}, properties = { "logging.level.org.springframework.cloud.deployer=DEBUG" }) public class KubernetesAppDeployerIntegrationIT extends AbstractAppDeployerIntegrationJUnit5Tests { @Autowired private AppDeployer appDeployer; @Autowired private KubernetesClient kubernetesClient; @Override protected AppDeployer provideAppDeployer() { return appDeployer; } @BeforeEach public void setup() { if (kubernetesClient.getNamespace() == null) { kubernetesClient.getConfiguration().setNamespace("default"); } } @Test public void testScaleStatefulSet() { logger.info("Testing {}...", "ScaleStatefulSet"); KubernetesAppDeployer appDeployer = kubernetesAppDeployer(); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); Map props = new HashMap<>(); props.put(AppDeployer.COUNT_PROPERTY_KEY, "3"); props.put(AppDeployer.INDEXED_PROPERTY_KEY, "true"); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, props); logger.info("Deploying {}...", request.getDefinition().getName()); Timeout timeout = deploymentTimeout(); String deploymentId = appDeployer.deploy(request); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); // assertThat(deploymentId, eventually(appInstanceCount(is(3)))); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getInstances()).hasSize(3); }); // Ensure that a StatefulSet is deployed Map selector = Collections.singletonMap(AbstractKubernetesDeployer.SPRING_APP_KEY, deploymentId); List statefulSets = kubernetesClient.apps().statefulSets().withLabels(selector).list().getItems(); assertThat(statefulSets).isNotNull(); assertThat(statefulSets).hasSize(1); StatefulSet statefulSet = statefulSets.get(0); StatefulSetSpec statefulSetSpec = statefulSet.getSpec(); Assertions.assertThat(statefulSetSpec.getPodManagementPolicy()).isEqualTo("Parallel"); Assertions.assertThat(statefulSetSpec.getReplicas()).isEqualTo(3); Assertions.assertThat(statefulSetSpec.getServiceName()).isEqualTo(deploymentId); Assertions.assertThat(statefulSet.getMetadata().getName()).isEqualTo(deploymentId); logger.info("Scale Down {}...", request.getDefinition().getName()); appDeployer.scale(new AppScaleRequest(deploymentId, 1)); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getInstances()).hasSize(1); }); statefulSets = kubernetesClient.apps().statefulSets().withLabels(selector).list().getItems(); assertThat(statefulSets).hasSize(1); statefulSetSpec = statefulSets.get(0).getSpec(); Assertions.assertThat(statefulSetSpec.getReplicas()).isEqualTo(1); Assertions.assertThat(statefulSetSpec.getServiceName()).isEqualTo(deploymentId); Assertions.assertThat(statefulSet.getMetadata().getName()).isEqualTo(deploymentId); appDeployer.undeploy(deploymentId); } @Test public void testScaleDeployment() { logger.info("Testing {}...", "ScaleDeployment"); KubernetesAppDeployer appDeployer = kubernetesAppDeployer(); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, Collections.emptyMap()); logger.info("Deploying {}...", request.getDefinition().getName()); Timeout timeout = deploymentTimeout(); String deploymentId = appDeployer.deploy(request); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getInstances()).hasSize(1); }); logger.info("Scale Up {}...", request.getDefinition().getName()); appDeployer.scale(new AppScaleRequest(deploymentId, 3)); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getInstances()).hasSize(3); }); logger.info("Scale Down {}...", request.getDefinition().getName()); appDeployer.scale(new AppScaleRequest(deploymentId, 1)); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getInstances()).hasSize(1); }); appDeployer.undeploy(deploymentId); } @Test public void testScaleWithNonExistingApps() { assertThatThrownBy(() -> { appDeployer.scale(new AppScaleRequest("Fake App", 10)); }).isInstanceOf(IllegalStateException.class); } @Test public void testFailedDeploymentWithLoadBalancer() { logger.info("Testing {}...", "FailedDeploymentWithLoadBalancer"); KubernetesDeployerProperties deployProperties = new KubernetesDeployerProperties(); deployProperties.setCreateLoadBalancer(true); deployProperties.setLivenessHttpProbePeriod(10); deployProperties.setMaxTerminatedErrorRestarts(1); deployProperties.setMaxCrashLoopBackOffRestarts(1); KubernetesAppDeployer lbAppDeployer = kubernetesAppDeployer(deployProperties); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.livenessHttpProbePath", "/invalidpath"); props.put("spring.cloud.deployer.kubernetes.livenessHttpProbeDelay", "1"); props.put("spring.cloud.deployer.kubernetes.livenessHttpProbePeriod", "1"); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, props); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = lbAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.failed); }); logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); lbAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); } @Test public void testGoodDeploymentWithLoadBalancer() { logger.info("Testing {}...", "GoodDeploymentWithLoadBalancer"); KubernetesDeployerProperties deployProperties = new KubernetesDeployerProperties(); deployProperties.setCreateLoadBalancer(true); deployProperties.setMinutesToWaitForLoadBalancer(1); KubernetesAppDeployer lbAppDeployer = kubernetesAppDeployer(deployProperties); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = lbAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); lbAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); } @Test @Disabled("Disabled until we can test lbs") public void testDeploymentWithLoadBalancerHasUrlAndAnnotation() { logger.info("Testing {}...", "DeploymentWithLoadBalancerShowsUrl"); KubernetesDeployerProperties deployProperties = new KubernetesDeployerProperties(); deployProperties.setCreateLoadBalancer(true); deployProperties.setMinutesToWaitForLoadBalancer(1); KubernetesAppDeployer lbAppDeployer = kubernetesAppDeployer(deployProperties); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, Collections.singletonMap("spring.cloud.deployer.kubernetes.serviceAnnotations", "foo:bar,fab:baz")); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = lbAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); logger.info("Checking instance attributes of {}...", request.getDefinition().getName()); AppStatus status = lbAppDeployer.status(deploymentId); for (String inst : status.getInstances().keySet()) { appDeployer().status(deploymentId).getInstances().get(inst); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getInstances().get(inst).getAttributes()).containsKey("url"); }); } logger.info("Checking service annotations of {}...", request.getDefinition().getName()); Map annotations = kubernetesClient.services().withName(request.getDefinition().getName()).get() .getMetadata().getAnnotations(); assertThat(annotations).isNotNull(); assertThat(annotations).hasSize(2); assertThat(annotations).containsKey("foo"); assertThat(annotations.get("foo")).isEqualTo("bar"); assertThat(annotations).containsKey("fab"); assertThat(annotations.get("fab")).isEqualTo("baz"); logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); lbAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); } @Test public void testAttributes() { logger.info("Testing {}...", "Attributes"); KubernetesDeployerProperties properties = new KubernetesDeployerProperties(); AppAdmin appAdmin = new AppAdmin(); appAdmin.setUser("user"); appAdmin.setPassword("password"); properties.setAppAdmin(appAdmin); KubernetesAppDeployer appDeployer = kubernetesAppDeployer(properties); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = appDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer.status(deploymentId).getInstances().values().stream() .map(ais -> ais.getAttributes()).filter(a -> StringUtils.hasText(a.get("pod.ip"))) .findFirst()).isPresent(); assertThat(appDeployer.status(deploymentId).getInstances().values().stream() .map(ais -> ais.getAttributes()).filter(a -> StringUtils.hasText(a.get("actuator.path"))) .map(a -> a.get("actuator.path")) .findFirst().get()).isEqualTo("/actuator"); assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); } @Test public void testDeploymentWithPodAnnotation() { logger.info("Testing {}...", "DeploymentWithPodAnnotation"); KubernetesAppDeployer appDeployer = kubernetesAppDeployer(); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, Collections.singletonMap("spring.cloud.deployer.kubernetes.podAnnotations", "iam.amazonaws.com/role:role-arn,foo:bar")); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = appDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); logger.info("Checking pod spec annotations of {}...", request.getDefinition().getName()); List pods = kubernetesClient.pods().withLabel("spring-deployment-id", request.getDefinition() .getName()).list().getItems(); assertThat(pods).hasSize(1); Pod pod = pods.get(0); Map annotations = pod.getMetadata().getAnnotations(); logger.info("Number of annotations found" + annotations.size()); for (Map.Entry annotationsEntry : annotations.entrySet()) { logger.info("Annotation key: " + annotationsEntry.getKey()); } assertThat(annotations.get("iam.amazonaws.com/role")).isEqualTo("role-arn"); assertThat(annotations.get("foo")).isEqualTo("bar"); logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); } @Test public void testDeploymentWithMountedHostPathVolume() throws IOException { logger.info("Testing {}...", "DeploymentWithMountedVolume"); String containerPath = "/tmp/"; String subPath = randomName(); String mountName = "mount"; HostPathVolumeSource hostPathVolumeSource = new HostPathVolumeSourceBuilder() .withPath("/tmp/" + randomName() + '/').build(); KubernetesDeployerProperties deployProperties = new KubernetesDeployerProperties(); deployProperties.setVolumes(Collections.singletonList(new VolumeBuilder() .withHostPath(hostPathVolumeSource) .withName(mountName) .build())); deployProperties.setVolumeMounts(Collections.singletonList(new VolumeMount(hostPathVolumeSource.getPath(), null, mountName, false, null, null))); KubernetesAppDeployer lbAppDeployer = kubernetesAppDeployer(deployProperties); AppDefinition definition = new AppDefinition(randomName(), Collections.singletonMap("logging.file", containerPath + subPath)); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = lbAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); Map selector = Collections.singletonMap(AbstractKubernetesDeployer.SPRING_APP_KEY, deploymentId); PodSpec spec = kubernetesClient.pods().withLabels(selector).list().getItems().get(0).getSpec(); assertThat(spec.getVolumes()).isNotNull(); Volume volume = spec.getVolumes().stream() .filter(v -> mountName.equals(v.getName())) .findAny() .orElseThrow(() -> new AssertionError("Volume not mounted")); assertThat(volume.getHostPath()).isNotNull(); assertThat(hostPathVolumeSource.getPath()).isEqualTo(volume.getHostPath().getPath()); logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); lbAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); } private void verifyAppEnv(String appId) { String ip = ""; int port = 0; KubernetesDeployerProperties properties = new KubernetesDeployerProperties(); boolean success = false; Service svc = kubernetesClient.services().withName(appId).get(); RestTemplate restTemplate = new RestTemplate(); if (svc != null && "LoadBalancer".equals(svc.getSpec().getType())) { int tries = 0; int maxWait = properties.getMinutesToWaitForLoadBalancer() * 6; // we check 6 times per minute while (tries++ < maxWait && !success) { if (svc.getStatus() != null && svc.getStatus().getLoadBalancer() != null && svc.getStatus().getLoadBalancer().getIngress() != null && !(svc.getStatus().getLoadBalancer().getIngress().isEmpty())) { ip = svc.getStatus().getLoadBalancer().getIngress().get(0).getIp(); if (ip == null) { ip = svc.getStatus().getLoadBalancer().getIngress().get(0).getHostname(); } port = svc.getSpec().getPorts().get(0).getPort(); success = true; try { String url = String.format("http://%s:%d/actuator/env", ip, port); restTemplate.exchange(url, HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>>() { @Override public Type getType() { return Map.class; } }); } catch (ResourceAccessException rae) { success = false; try { Thread.sleep(5000L); } catch (InterruptedException e) { } } } else { try { Thread.sleep(5000L); } catch (InterruptedException e) { } svc = kubernetesClient.services().withName(appId).get(); } } logger.debug(String.format("LoadBalancer Ingress: %s", svc.getStatus().getLoadBalancer().getIngress().toString())); } assertThat(success).as("cannot get service information for " + appId).isFalse(); String url = String.format("http://%s:%d/actuator/env", ip, port); logger.debug("getting app environment from " + url); restTemplate = new RestTemplate(); ResponseEntity>> response = restTemplate.exchange(url, HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>>() { @Override public Type getType() { return Map.class; } }); LinkedHashMap> env = response.getBody(); ArrayList propertySources = env.get("propertySources"); String hostName = null; String instanceIndex = null; for (LinkedHashMap propertySource : propertySources) { if (propertySource.get("name").equals("systemEnvironment")) { LinkedHashMap s = (LinkedHashMap) propertySource.get("properties"); hostName = (String) ((LinkedHashMap) s.get("HOSTNAME")).get("value"); } if (propertySource.get("name").equals("applicationConfig: [file:./config/application.properties]")) { LinkedHashMap s = (LinkedHashMap) propertySource.get("properties"); instanceIndex = (String) ((LinkedHashMap) s.get("INSTANCE_INDEX")).get("value"); } } assertThat(hostName).as("Hostname is null").isNotNull(); assertThat(instanceIndex).as("Instance index is null").isNotNull(); String expectedIndex = hostName.substring(hostName.lastIndexOf("-") + 1); assertThat(instanceIndex).isEqualTo(expectedIndex); } @Test @Disabled("Disabled until we can test lbs") public void testDeploymentWithGroupAndIndex() throws IOException { logger.info("Testing {}...", "DeploymentWithWithGroupAndIndex"); KubernetesDeployerProperties deployProperties = new KubernetesDeployerProperties(); deployProperties.setCreateLoadBalancer(true); deployProperties.setMinutesToWaitForLoadBalancer(1); KubernetesAppDeployer testAppDeployer = kubernetesAppDeployer(deployProperties); Map appProperties = new HashMap<>(); appProperties.put("security.basic.enabled", "false"); appProperties.put("management.security.enabled", "false"); AppDefinition definition = new AppDefinition(randomName(), appProperties); Resource resource = testApplication(); Map props = new HashMap<>(); props.put(AppDeployer.GROUP_PROPERTY_KEY, "foo"); props.put(AppDeployer.INDEXED_PROPERTY_KEY, "true"); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, props); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = testAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); Map selector = Collections.singletonMap(AbstractKubernetesDeployer.SPRING_APP_KEY, deploymentId); PodSpec spec = kubernetesClient.pods().withLabels(selector).list().getItems().get(0).getSpec(); Map envVars = new HashMap<>(); for (EnvVar e : spec.getContainers().get(0).getEnv()) { envVars.put(e.getName(), e.getValue()); } assertThat(envVars).contains(entry("SPRING_CLOUD_APPLICATION_GROUP", "foo")); verifyAppEnv(deploymentId); logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); testAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); } @Test public void testDeploymentServiceAccountName() { logger.info("Testing {}...", "DeploymentServiceAccountName"); ServiceAccount deploymentServiceAccount = new ServiceAccountBuilder().withNewMetadata().withName("appsa") .endMetadata().build(); this.kubernetesClient.serviceAccounts().create(deploymentServiceAccount); String serviceAccountName = deploymentServiceAccount.getMetadata().getName(); KubernetesDeployerProperties deployProperties = new KubernetesDeployerProperties(); deployProperties.setDeploymentServiceAccountName(serviceAccountName); ContainerFactory containerFactory = new DefaultContainerFactory(deployProperties); KubernetesAppDeployer appDeployer = kubernetesAppDeployer(deployProperties); AppDefinition definition = new AppDefinition(randomName(), null); AppDeploymentRequest request = new AppDeploymentRequest(definition, testApplication()); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = appDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); kubernetesClient.serviceAccounts().delete(deploymentServiceAccount); } @Test public void testCreateStatefulSet() throws Exception { Map props = new HashMap<>(); props.put(AppDeployer.COUNT_PROPERTY_KEY, "3"); props.put(AppDeployer.INDEXED_PROPERTY_KEY, "true"); AppDefinition definition = new AppDefinition(randomName(), null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, testApplication(), props); KubernetesAppDeployer deployer = kubernetesAppDeployer(); logger.info("Deploying {}...", appDeploymentRequest.getDefinition().getName()); String deploymentId = deployer.deploy(appDeploymentRequest); Map idMap = deployer.createIdMap(deploymentId, appDeploymentRequest); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); Map selector = Collections.singletonMap(AbstractKubernetesDeployer.SPRING_APP_KEY, deploymentId); List statefulSets = kubernetesClient.apps().statefulSets().withLabels(selector).list().getItems(); assertThat(statefulSets).hasSize(1); StatefulSet statefulSet = statefulSets.get(0); StatefulSetSpec statefulSetSpec = statefulSet.getSpec(); List statefulSetInitContainers = statefulSetSpec.getTemplate().getSpec().getInitContainers(); assertThat(statefulSetInitContainers).hasSize(1); Container statefulSetInitContainer = statefulSetInitContainers.get(0); assertThat(statefulSetInitContainer.getImage()).isEqualTo(DeploymentPropertiesResolver.STATEFUL_SET_IMAGE_NAME); assertThat(statefulSetInitContainer.getSecurityContext()).isNull(); Assertions.assertThat(statefulSetSpec.getPodManagementPolicy()).isEqualTo("Parallel"); Assertions.assertThat(statefulSetSpec.getReplicas()).isEqualTo(3); Assertions.assertThat(statefulSetSpec.getServiceName()).isEqualTo(deploymentId); Assertions.assertThat(statefulSet.getMetadata().getName()).isEqualTo(deploymentId); Assertions.assertThat(statefulSetSpec.getSelector().getMatchLabels()) .containsAllEntriesOf(deployer.createIdMap(deploymentId, appDeploymentRequest)); Assertions.assertThat(statefulSetSpec.getSelector().getMatchLabels()) .contains(entry(KubernetesAppDeployer.SPRING_MARKER_KEY, KubernetesAppDeployer.SPRING_MARKER_VALUE)); Assertions.assertThat(statefulSetSpec.getTemplate().getMetadata().getLabels()).containsAllEntriesOf(idMap); Assertions.assertThat(statefulSetSpec.getTemplate().getMetadata().getLabels()) .contains(entry(KubernetesAppDeployer.SPRING_MARKER_KEY, KubernetesAppDeployer.SPRING_MARKER_VALUE)); Container container = statefulSetSpec.getTemplate().getSpec().getContainers().get(0); Assertions.assertThat(container.getName()).isEqualTo(deploymentId); Assertions.assertThat(container.getPorts().get(0).getContainerPort()).isEqualTo(8080); Assertions.assertThat(container.getImage()).isEqualTo(testApplication().getURI().getSchemeSpecificPart()); PersistentVolumeClaim pvc = statefulSetSpec.getVolumeClaimTemplates().get(0); Assertions.assertThat(pvc.getMetadata().getName()).isEqualTo(deploymentId); PersistentVolumeClaimSpec pvcSpec = pvc.getSpec(); Assertions.assertThat(pvcSpec.getAccessModes()).containsOnly("ReadWriteOnce"); Assertions.assertThat(pvcSpec.getStorageClassName()).isNull(); Assertions.assertThat(pvcSpec.getResources().getLimits().get("storage").getAmount()).isEqualTo("10"); Assertions.assertThat(pvcSpec.getResources().getRequests().get("storage").getAmount()).isEqualTo("10"); Assertions.assertThat(pvcSpec.getResources().getLimits().get("storage").getFormat()).isEqualTo("Mi"); Assertions.assertThat(pvcSpec.getResources().getRequests().get("storage").getFormat()).isEqualTo("Mi"); logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); } @Test public void testCreateStatefulSetInitContainerImageNamePropOverride() throws Exception { Map props = new HashMap<>(); props.put(AppDeployer.COUNT_PROPERTY_KEY, "3"); props.put(AppDeployer.INDEXED_PROPERTY_KEY, "true"); String imageName = testApplication().getURI().getSchemeSpecificPart(); props.put("spring.cloud.deployer.kubernetes.statefulSetInitContainerImageName", imageName); AppDefinition definition = new AppDefinition(randomName(), null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, testApplication(), props); KubernetesAppDeployer deployer = kubernetesAppDeployer(); logger.info("Deploying {}...", appDeploymentRequest.getDefinition().getName()); String deploymentId = deployer.deploy(appDeploymentRequest); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); Map selector = Collections.singletonMap(AbstractKubernetesDeployer.SPRING_APP_KEY, deploymentId); List statefulSets = kubernetesClient.apps().statefulSets().withLabels(selector).list().getItems(); assertThat(statefulSets).hasSize(1); StatefulSet statefulSet = statefulSets.get(0); StatefulSetSpec statefulSetSpec = statefulSet.getSpec(); List statefulSetInitContainers = statefulSetSpec.getTemplate().getSpec().getInitContainers(); assertThat(statefulSetInitContainers).hasSize(1); Container statefulSetInitContainer = statefulSetInitContainers.get(0); assertThat(statefulSetInitContainer.getImage()).isEqualTo(imageName); logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); } @Test public void createStatefulSetInitContainerImageNameGlobalOverride() throws Exception { Map props = new HashMap<>(); props.put(AppDeployer.COUNT_PROPERTY_KEY, "3"); props.put(AppDeployer.INDEXED_PROPERTY_KEY, "true"); AppDefinition definition = new AppDefinition(randomName(), null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, testApplication(), props); String imageName = testApplication().getURI().getSchemeSpecificPart(); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setStatefulSetInitContainerImageName(imageName); KubernetesAppDeployer deployer = kubernetesAppDeployer(kubernetesDeployerProperties); logger.info("Deploying {}...", appDeploymentRequest.getDefinition().getName()); String deploymentId = deployer.deploy(appDeploymentRequest); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); Map selector = Collections.singletonMap(AbstractKubernetesDeployer.SPRING_APP_KEY, deploymentId); List statefulSets = kubernetesClient.apps().statefulSets().withLabels(selector).list().getItems(); assertThat(statefulSets).hasSize(1); StatefulSet statefulSet = statefulSets.get(0); StatefulSetSpec statefulSetSpec = statefulSet.getSpec(); List statefulSetInitContainers = statefulSetSpec.getTemplate().getSpec().getInitContainers(); assertThat(statefulSetInitContainers).hasSize(1); Container statefulSetInitContainer = statefulSetInitContainers.get(0); assertThat(statefulSetInitContainer.getImage()).isEqualTo(imageName); logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); } @Test public void createStatefulSetWithOverridingRequest() throws Exception { Map props = new HashMap<>(); props.put(AppDeployer.COUNT_PROPERTY_KEY, "3"); props.put(AppDeployer.INDEXED_PROPERTY_KEY, "true"); props.put("spring.cloud.deployer.kubernetes.statefulSet.volumeClaimTemplate.name", "mystorage"); props.put("spring.cloud.deployer.kubernetes.statefulSet.volumeClaimTemplate.storage", "1g"); AppDefinition definition = new AppDefinition(randomName(), null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, testApplication(), props); KubernetesAppDeployer deployer = kubernetesAppDeployer(); logger.info("Deploying {}...", appDeploymentRequest.getDefinition().getName()); String deploymentId = deployer.deploy(appDeploymentRequest); Map idMap = deployer.createIdMap(deploymentId, appDeploymentRequest); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); Map selector = Collections.singletonMap(AbstractKubernetesDeployer.SPRING_APP_KEY, deploymentId); StatefulSet statefulSet = kubernetesClient.apps().statefulSets().withLabels(selector).list().getItems().get(0); StatefulSetSpec statefulSetSpec = statefulSet.getSpec(); Assertions.assertThat(statefulSetSpec.getPodManagementPolicy()).isEqualTo("Parallel"); Assertions.assertThat(statefulSetSpec.getReplicas()).isEqualTo(3); Assertions.assertThat(statefulSetSpec.getServiceName()).isEqualTo(deploymentId); Assertions.assertThat(statefulSet.getMetadata().getName()).isEqualTo(deploymentId); Assertions.assertThat(statefulSetSpec.getSelector().getMatchLabels()) .containsAllEntriesOf(deployer.createIdMap(deploymentId, appDeploymentRequest)); Assertions.assertThat(statefulSetSpec.getSelector().getMatchLabels()) .contains(entry(KubernetesAppDeployer.SPRING_MARKER_KEY, KubernetesAppDeployer.SPRING_MARKER_VALUE)); Assertions.assertThat(statefulSetSpec.getTemplate().getMetadata().getLabels()).containsAllEntriesOf(idMap); Assertions.assertThat(statefulSetSpec.getTemplate().getMetadata().getLabels()) .contains(entry(KubernetesAppDeployer.SPRING_MARKER_KEY, KubernetesAppDeployer.SPRING_MARKER_VALUE)); Container container = statefulSetSpec.getTemplate().getSpec().getContainers().get(0); Assertions.assertThat(container.getName()).isEqualTo(deploymentId); Assertions.assertThat(container.getPorts().get(0).getContainerPort()).isEqualTo(8080); Assertions.assertThat(container.getImage()).isEqualTo(testApplication().getURI().getSchemeSpecificPart()); PersistentVolumeClaim pvc = statefulSetSpec.getVolumeClaimTemplates().get(0); Assertions.assertThat(pvc.getMetadata().getName()).isEqualTo("mystorage"); PersistentVolumeClaimSpec pvcSpec = pvc.getSpec(); Assertions.assertThat(pvcSpec.getAccessModes()).containsOnly("ReadWriteOnce"); Assertions.assertThat(pvcSpec.getResources().getLimits().get("storage").getAmount()).isEqualTo("1"); Assertions.assertThat(pvcSpec.getResources().getRequests().get("storage").getAmount()).isEqualTo("1"); Assertions.assertThat(pvcSpec.getResources().getLimits().get("storage").getFormat()).isEqualTo("Gi"); Assertions.assertThat(pvcSpec.getResources().getRequests().get("storage").getFormat()).isEqualTo("Gi"); logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); } @Test public void createStatefulSetWithPVCDefaultName() throws Exception { Map props = new HashMap<>(); props.put(AppDeployer.COUNT_PROPERTY_KEY, "3"); props.put(AppDeployer.INDEXED_PROPERTY_KEY, "true"); AppDefinition definition = new AppDefinition(randomName(), null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, testApplication(), props); KubernetesAppDeployer deployer = kubernetesAppDeployer(); logger.info("Deploying {}...", appDeploymentRequest.getDefinition().getName()); String deploymentId = deployer.deploy(appDeploymentRequest); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); Map selector = Collections.singletonMap(AbstractKubernetesDeployer.SPRING_APP_KEY, deploymentId); StatefulSet statefulSet = kubernetesClient.apps().statefulSets().withLabels(selector).list().getItems().get(0); StatefulSetSpec statefulSetSpec = statefulSet.getSpec(); Container container = statefulSetSpec.getTemplate().getSpec().getContainers().get(0); Assertions.assertThat(container.getName()).isEqualTo(deploymentId); PersistentVolumeClaim pvc = statefulSetSpec.getVolumeClaimTemplates().get(0); Assertions.assertThat(pvc.getMetadata().getName()).isEqualTo(deploymentId); logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); } @Test public void testStatefulSetPodAnnotations() { logger.info("Testing {}...", "StatefulSetPodAnnotations"); KubernetesAppDeployer appDeployer = kubernetesAppDeployer(); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); Map props = new HashMap<>(); props.put(AppDeployer.COUNT_PROPERTY_KEY, "3"); props.put(AppDeployer.INDEXED_PROPERTY_KEY, "true"); props.put("spring.cloud.deployer.kubernetes.podAnnotations", "iam.amazonaws.com/role:role-arn,foo:bar"); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, props); logger.info("Deploying {}...", request.getDefinition().getName()); Timeout timeout = deploymentTimeout(); String deploymentId = appDeployer.deploy(request); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getInstances()).hasSize(3); }); // Ensure that a StatefulSet is deployed Map selector = Collections.singletonMap(AbstractKubernetesDeployer.SPRING_APP_KEY, deploymentId); List statefulSets = kubernetesClient.apps().statefulSets().withLabels(selector).list().getItems(); assertThat(statefulSets).isNotNull(); assertThat(statefulSets).hasSize(1); StatefulSet statefulSet = statefulSets.get(0); StatefulSetSpec statefulSetSpec = statefulSet.getSpec(); Map annotations = statefulSetSpec.getTemplate().getMetadata().getAnnotations(); assertThat(annotations.get("iam.amazonaws.com/role")).isEqualTo("role-arn"); assertThat(annotations.get("foo")).isEqualTo("bar"); appDeployer.undeploy(deploymentId); } @Test public void testDeploymentLabels() { Map props = Collections.singletonMap("spring.cloud.deployer.kubernetes.deploymentLabels", "label1:value1,label2:value2"); AppDefinition definition = new AppDefinition(randomName(), null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, testApplication(), props); KubernetesAppDeployer deployer = kubernetesAppDeployer(); logger.info("Deploying {}...", appDeploymentRequest.getDefinition().getName()); String deploymentId = deployer.deploy(appDeploymentRequest); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); Map selector = Collections.singletonMap(AbstractKubernetesDeployer.SPRING_APP_KEY, deploymentId); List deployments = kubernetesClient.apps().deployments().withLabels(selector).list().getItems(); Map specLabels = deployments.get(0).getSpec().getTemplate().getMetadata().getLabels(); assertThat(specLabels.containsKey("label1")).as("Label 'label1' not found in deployment spec").isTrue(); assertThat(specLabels.get("label1")).as("Unexpected value for label1").isEqualTo("value1"); assertThat(specLabels).as("Label 'label2' not found in deployment spec").containsKey("label2"); assertThat(specLabels.get("label2")).as("Unexpected value for label1").isEqualTo("value2"); logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); } @Test public void testDeploymentLabelsStatefulSet() { logger.info("Testing {}...", "DeploymentLabelsForStatefulSet"); Map props = new HashMap<>(); props.put(AppDeployer.COUNT_PROPERTY_KEY, "2"); props.put(AppDeployer.INDEXED_PROPERTY_KEY, "true"); props.put("spring.cloud.deployer.kubernetes.deploymentLabels", "stateful-label1:stateful-value1,stateful-label2:stateful-value2"); AppDefinition definition = new AppDefinition(randomName(), null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, testApplication(), props); KubernetesAppDeployer deployer = kubernetesAppDeployer(); logger.info("Deploying {}...", appDeploymentRequest.getDefinition().getName()); String deploymentId = deployer.deploy(appDeploymentRequest); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); Map idMap = deployer.createIdMap(deploymentId, appDeploymentRequest); Map selector = Collections.singletonMap(AbstractKubernetesDeployer.SPRING_APP_KEY, deploymentId); StatefulSet statefulSet = kubernetesClient.apps().statefulSets().withLabels(selector).list().getItems().get(0); StatefulSetSpec statefulSetSpec = statefulSet.getSpec(); Assertions.assertThat(statefulSetSpec.getReplicas()).isEqualTo(2); Assertions.assertThat(statefulSetSpec.getTemplate().getMetadata().getLabels()).containsAllEntriesOf(idMap); //verify stateful set match labels Map setLabels = statefulSet.getMetadata().getLabels(); assertThat(setLabels).contains(entry("stateful-label1", "stateful-value1"), entry("stateful-label2", "stateful-value2")); //verify pod template labels Map specLabels = statefulSetSpec.getTemplate().getMetadata().getLabels(); assertThat(specLabels).contains(entry("stateful-label1", "stateful-value1"), entry("stateful-label2", "stateful-value2")); //verify that labels got replicated to one of the deployments List pods = kubernetesClient.pods().withLabels(selector).list().getItems(); Map podLabels = pods.get(0).getMetadata().getLabels(); assertThat(podLabels).contains(entry("stateful-label1", "stateful-value1"), entry("stateful-label2", "stateful-value2")); logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); } @Test public void testCleanupOnDeployFailure() throws InterruptedException { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, testApplication(), null); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); // simulate a pod going into an un-schedulable state KubernetesDeployerProperties.LimitsResources resources = new KubernetesDeployerProperties.LimitsResources(); resources.setCpu("9000000"); kubernetesDeployerProperties.setLimits(resources); KubernetesAppDeployer deployer = kubernetesAppDeployer(kubernetesDeployerProperties); logger.info("Deploying {}...", appDeploymentRequest.getDefinition().getName()); String deploymentId = deployer.deploy(appDeploymentRequest); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); // attempt to undeploy the failed deployment logger.info("Undeploying {}...", deploymentId); try { appDeployer.undeploy(deploymentId); } catch (Exception e) { logger.info("Got expected not not deployed exception on undeployment: " + e.getMessage()); } deployer = kubernetesAppDeployer(); logger.info("Deploying {}... again", deploymentId); // ensure a previous failed deployment with the same name was cleaned up and can be deployed again deployer.deploy(appDeploymentRequest); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); appDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); } @Test public void testMultipleContainersInPod() { logger.info("Testing {}...", "MultipleContainersInPod"); KubernetesAppDeployer kubernetesAppDeployer = Mockito.spy(kubernetesAppDeployer()); AppDefinition definition = new AppDefinition(randomName(), Collections.singletonMap("server.port", "9090")); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); doAnswer((Answer) invocationOnMock -> { PodSpec podSpec = (PodSpec) invocationOnMock.callRealMethod(); Container container = new ContainerBuilder().withName("asecondcontainer") .withImage(resource.getURI().getSchemeSpecificPart()).build(); podSpec.getContainers().add(container); return podSpec; }).when(kubernetesAppDeployer).createPodSpec(Mockito.any(AppDeploymentRequest.class)); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); } @Test public void testDefaultServicePort() { logger.info("Testing {}...", "DefaultServicePort"); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); List servicePorts = kubernetesClient.services().withName(request.getDefinition().getName()).get() .getSpec().getPorts(); assertThat(servicePorts).hasSize(1); assertThat(servicePorts.get(0).getPort()).isEqualTo(8080); assertThat(servicePorts.get(0).getName()).isEqualTo("port-8080"); logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); } @Test public void testDefaultServicePortOverride() { logger.info("Testing {}...", "DefaultServicePortOverride"); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(); AppDefinition definition = new AppDefinition(randomName(), Collections.singletonMap("server.port", "9090")); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); List servicePorts = kubernetesClient.services().withName(request.getDefinition().getName()).get() .getSpec().getPorts(); assertThat(servicePorts).hasSize(1); assertThat(servicePorts.get(0).getPort()).isEqualTo(9090); assertThat(servicePorts.get(0).getName()).isEqualTo("port-9090"); logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); } @Test public void testServiceWithMultiplePorts() { logger.info("Testing {}...", "ServiceWithMultiplePorts"); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, Collections.singletonMap("spring.cloud.deployer.kubernetes.servicePorts", "8080,9090")); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); List servicePorts = kubernetesClient.services().withName(request.getDefinition().getName()).get() .getSpec().getPorts(); assertThat(servicePorts).hasSize(2); assertThat(servicePorts.stream().anyMatch(o -> o.getPort().equals(8080))).isTrue(); assertThat(servicePorts.stream().anyMatch(o -> o.getName().equals("port-8080"))).isTrue(); assertThat(servicePorts.stream().anyMatch(o -> o.getPort().equals(9090))).isTrue(); assertThat(servicePorts.stream().anyMatch(o -> o.getName().equals("port-9090"))).isTrue(); logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); } @Test public void testCreateInitContainer() { logger.info("Testing {}...", "CreateInitContainer"); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(); Map props = Collections.singletonMap("spring.cloud.deployer.kubernetes.initContainer", "{containerName: 'test', imageName: 'busybox:latest', commands: ['sh', '-c', 'echo hello']}"); AppDefinition definition = new AppDefinition(randomName(), null); AppDeploymentRequest request = new AppDeploymentRequest(definition, testApplication(), props); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); Deployment deployment = kubernetesClient.apps().deployments().withName(request.getDefinition().getName()).get(); List initContainers = deployment.getSpec().getTemplate().getSpec().getInitContainers(); Optional initContainer = initContainers.stream().filter(i -> i.getName().equals("test")).findFirst(); assertThat(initContainer.isPresent()).as("Init container not found").isTrue(); Container testInitContainer = initContainer.get(); assertThat(testInitContainer.getName()).as("Unexpected init container name").isEqualTo("test"); assertThat(testInitContainer.getImage()).as("Unexpected init container image").isEqualTo("busybox:latest"); List commands = testInitContainer.getCommand(); assertThat(commands).contains("sh", "-c", "echo hello"); logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); } @Test public void testCreateInitContainerWithEnvVariables() { logger.info("Testing {}...", "CreateInitContainer"); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(); Map props = Collections.singletonMap("spring.cloud.deployer.kubernetes.initContainer", "{containerName: 'test', imageName: 'busybox:latest', commands: ['sh', '-c', 'echo hello'], environmentVariables: ['KEY1=VAL1', 'KEY2=VAL2']}"); AppDefinition definition = new AppDefinition(randomName(), null); AppDeploymentRequest request = new AppDeploymentRequest(definition, testApplication(), props); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); Deployment deployment = kubernetesClient.apps().deployments().withName(request.getDefinition().getName()).get(); List initContainers = deployment.getSpec().getTemplate().getSpec().getInitContainers(); Optional initContainer = initContainers.stream().filter(i -> i.getName().equals("test")).findFirst(); assertThat(initContainer.isPresent()).as("Init container not found").isTrue(); Container testInitContainer = initContainer.get(); List containerEnvs = testInitContainer.getEnv(); assertThat(containerEnvs).hasSize(2); assertThat(containerEnvs.stream().map(EnvVar::getName).collect(Collectors.toList())).contains("KEY1", "KEY2"); assertThat(containerEnvs.stream().map(EnvVar::getValue).collect(Collectors.toList())).contains("VAL1", "VAL2"); logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); } @Test public void testCreateInitContainerWithVolumeMounts() { logger.info("Testing {}...", "CreateInitContainerWithVolumeMounts"); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(); Map props = Stream.of(new String[][]{ { "spring.cloud.deployer.kubernetes.volumes", "[{name: 'test-volume', emptyDir: {}}]", }, { "spring.cloud.deployer.kubernetes.volumeMounts", "[{name: 'test-volume', mountPath: '/tmp'}]", }, { "spring.cloud.deployer.kubernetes.initContainer", "{containerName: 'test', imageName: 'busybox:latest', commands: ['sh', '-c', 'echo hello'], " + "volumeMounts: [{name: 'test-volume', mountPath: '/tmp', readOnly: true}]}", } }).collect(Collectors.toMap(data -> data[0], data -> data[1])); AppDefinition definition = new AppDefinition(randomName(), null); AppDeploymentRequest request = new AppDeploymentRequest(definition, testApplication(), props); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); Deployment deployment = kubernetesClient.apps().deployments().withName(request.getDefinition().getName()).get(); List initContainers = deployment.getSpec().getTemplate().getSpec().getInitContainers(); Optional initContainer = initContainers.stream().filter(i -> i.getName().equals("test")).findFirst(); assertThat(initContainer.isPresent()).as("Init container not found").isTrue(); Container testInitContainer = initContainer.get(); assertThat(testInitContainer.getName()).as("Unexpected init container name").isEqualTo("test"); assertThat(testInitContainer.getImage()).as("Unexpected init container image").isEqualTo("busybox:latest"); List commands = testInitContainer.getCommand(); assertThat(commands != null && !commands.isEmpty()).as("Init container commands missing").isTrue(); assertThat(commands).hasSize(3); assertThat(commands).contains("sh", "-c", "echo hello"); List volumeMounts = testInitContainer.getVolumeMounts(); assertThat(volumeMounts != null && !volumeMounts.isEmpty()).as("Init container volumeMounts missing").isTrue(); assertThat(volumeMounts).hasSize(1); VolumeMount vm = volumeMounts.get(0); assertThat(vm.getName()).as("Unexpected init container volume mount name").isEqualTo("test-volume"); assertThat(vm.getMountPath()).as("Unexpected init container volume mount path").isEqualTo("/tmp"); assertThat(vm.getReadOnly()).as("Expected read only volume mount").isTrue(); logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); } @Test public void testCreateAdditionalContainers() { logger.info("Testing {}...", "CreateAdditionalContainers"); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(); Map props = Stream.of(new String[][]{ { "spring.cloud.deployer.kubernetes.volumes", "[{name: 'test-volume', emptyDir: {}}]", }, { "spring.cloud.deployer.kubernetes.volumeMounts", "[{name: 'test-volume', mountPath: '/tmp'}]", }, { "spring.cloud.deployer.kubernetes.additional-containers", "[{name: 'c1', image: 'busybox:latest', command: ['sh', '-c', 'echo hello1'], volumeMounts: [{name: 'test-volume', mountPath: '/tmp', readOnly: true}]}," + "{name: 'c2', image: 'busybox:1.26.1', command: ['sh', '-c', 'echo hello2']}]" }}).collect(Collectors.toMap(data -> data[0], data -> data[1])); AppDefinition definition = new AppDefinition(randomName(), null); AppDeploymentRequest request = new AppDeploymentRequest(definition, testApplication(), props); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); Deployment deployment = kubernetesClient.apps().deployments().withName(request.getDefinition().getName()).get(); List containers = deployment.getSpec().getTemplate().getSpec().getContainers(); assertThat(containers).hasSize(3); Optional additionalContainer1 = containers.stream().filter(i -> i.getName().equals("c1")).findFirst(); assertThat(additionalContainer1.isPresent()).isTrue(); Container testAdditionalContainer1 = additionalContainer1.get(); assertThat(testAdditionalContainer1.getName()).as("Unexpected additional container name").isEqualTo("c1"); assertThat(testAdditionalContainer1.getImage()).as("Unexpected additional container image").isEqualTo("busybox:latest"); List commands = testAdditionalContainer1.getCommand(); assertThat(commands).contains("sh", "-c", "echo hello1"); List volumeMounts = testAdditionalContainer1.getVolumeMounts(); assertThat(volumeMounts).hasSize(1); assertThat(volumeMounts.get(0).getName()).isEqualTo("test-volume"); assertThat(volumeMounts.get(0).getMountPath()).isEqualTo("/tmp"); assertThat(volumeMounts.get(0).getReadOnly()).isTrue(); Optional additionalContainer2 = containers.stream().filter(i -> i.getName().equals("c2")).findFirst(); assertThat(additionalContainer2.isPresent()).as("Additional container c2 not found").isTrue(); Container testAdditionalContainer2 = additionalContainer2.get(); assertThat(testAdditionalContainer2.getName()).as("Unexpected additional container name").isEqualTo("c2"); assertThat(testAdditionalContainer2.getImage()).as("Unexpected additional container image").isEqualTo("busybox:1.26.1"); List container2Commands = testAdditionalContainer2.getCommand(); assertThat(container2Commands).contains("sh", "-c", "echo hello2"); logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); } @Test public void testCreateAdditionalContainersOverride() { logger.info("Testing {}...", "CreateAdditionalContainersOverride"); KubernetesDeployerProperties.Container container1 = new KubernetesDeployerProperties.Container(); container1.setName("c1"); container1.setImage("busybox:1.31.0"); container1.setCommand(Arrays.asList("sh", "-c", "echo hello-from-original-properties")); KubernetesDeployerProperties.Container container2 = new KubernetesDeployerProperties.Container(); container2.setName("container2"); container2.setImage("busybox:1.31.0"); container2.setCommand(Arrays.asList("sh", "-c", "echo hello-from-original-properties")); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setAdditionalContainers(Arrays.asList(container1, container2)); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(kubernetesDeployerProperties); Map props = Stream.of(new String[][]{ { "spring.cloud.deployer.kubernetes.volumes", "[{name: 'test-volume', emptyDir: {}}]", }, { "spring.cloud.deployer.kubernetes.volumeMounts", "[{name: 'test-volume', mountPath: '/tmp'}]", }, { "spring.cloud.deployer.kubernetes.additional-containers", "[{name: 'c1', image: 'busybox:latest', command: ['sh', '-c', 'echo hello1'], volumeMounts: [{name: 'test-volume', mountPath: '/tmp', readOnly: true}]}," + "{name: 'c2', image: 'busybox:1.26.1', command: ['sh', '-c', 'echo hello2']}]" }}).collect(Collectors.toMap(data -> data[0], data -> data[1])); AppDefinition definition = new AppDefinition(randomName(), null); AppDeploymentRequest request = new AppDeploymentRequest(definition, testApplication(), props); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); Deployment deployment = kubernetesClient.apps().deployments().withName(request.getDefinition().getName()).get(); List containers = deployment.getSpec().getTemplate().getSpec().getContainers(); assertThat(containers).hasSize(4); // c1 from the deployment properties should have overridden the c1 from the original deployer properties Optional additionalContainer1 = containers.stream().filter(i -> i.getName().equals("c1")).findFirst(); assertThat(additionalContainer1.isPresent()).as("Additional container c1 not found").isTrue(); Container testAdditionalContainer1 = additionalContainer1.get(); assertThat(testAdditionalContainer1.getName()).as("Unexpected additional container name").isEqualTo("c1"); assertThat(testAdditionalContainer1.getImage()).as("Unexpected additional container image").isEqualTo("busybox:latest"); List commands = testAdditionalContainer1.getCommand(); assertThat(commands).contains("sh", "-c", "echo hello1"); List volumeMounts = testAdditionalContainer1.getVolumeMounts(); assertThat(volumeMounts).hasSize(1); assertThat(volumeMounts.get(0).getName()).isEqualTo("test-volume"); assertThat(volumeMounts.get(0).getMountPath()).isEqualTo("/tmp"); assertThat(volumeMounts.get(0).getReadOnly()).isTrue(); Optional additionalContainer2 = containers.stream().filter(i -> i.getName().equals("c2")).findFirst(); assertThat(additionalContainer2.isPresent()).as("Additional container c2 not found").isTrue(); Container testAdditionalContainer2 = additionalContainer2.get(); assertThat(testAdditionalContainer2.getName()).as("Unexpected additional container name").isEqualTo("c2"); assertThat(testAdditionalContainer2.getImage()).as("Unexpected additional container image").isEqualTo("busybox:1.26.1"); List container2Commands = testAdditionalContainer2.getCommand(); assertThat(container2Commands).contains("sh", "-c", "echo hello2"); // Verifying the additional container passed from the root deployer properties Optional additionalContainer3 = containers.stream().filter(i -> i.getName().equals("container2")).findFirst(); assertThat(additionalContainer3.isPresent()).as("Additional container c2 not found").isTrue(); Container testAdditionalContainer3 = additionalContainer3.get(); assertThat(testAdditionalContainer3.getName()).as("Unexpected additional container name").isEqualTo("container2"); assertThat(testAdditionalContainer3.getImage()).as("Unexpected additional container image").isEqualTo("busybox:1.31.0"); List container3Commands = testAdditionalContainer3.getCommand(); assertThat(container3Commands).contains("sh", "-c", "echo hello-from-original-properties"); logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); } @Nested class SecurityContextITs { @Test void podWithInitContainerAndAdditionalContainers() { Map deploymentProps = new HashMap<>(); deploymentProps.put("spring.cloud.deployer.kubernetes.initContainer", "{ containerName: 'init-container-5150', imageName: 'busybox:latest', commands: ['sh', '-c', 'echo hello']}"); deploymentProps.put("spring.cloud.deployer.kubernetes.additional-containers", "[" + "{ name: 'extra-container-5150', image: 'busybox:latest', command: ['sh', '-c'], args: [\"while true; do echo ‘hello 5150’ & sleep 2; done\"]}," + "{ name: 'extra-container-6160', image: 'busybox:latest', command: ['sh', '-c'], args: [\"while true; do echo ‘hello 6160’ & sleep 2; done\"]}" + "]"); deploymentProps.put("spring.cloud.deployer.kubernetes.podSecurityContext", "{ fsGroup: 65534" + ", fsGroupChangePolicy: Always" + ", runAsUser: 65534" + ", runAsGroup: 65534" + ", seLinuxOptions: { level: \"s0:c123,c456\" }" + "}"); deploymentProps.put("spring.cloud.deployer.kubernetes.containerSecurityContext", "{ allowPrivilegeEscalation: true" + ", runAsUser: 65534" + ", runAsGroup: 65534" + ", seLinuxOptions: { level: \"s0:c123,c777\" }" + "}"); AppDefinition definition = new AppDefinition(randomName(), null); AppDeploymentRequest request = new AppDeploymentRequest(definition, testApplication(), deploymentProps); KubernetesDeployerProperties deployProperties = new KubernetesDeployerProperties(); deployProperties.setCreateLoadBalancer(true); deployProperties.setMinutesToWaitForLoadBalancer(1); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(deployProperties); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); PodSecurityContext expectedPodSecurityContext = new PodSecurityContextBuilder() .withFsGroup(65534L) .withFsGroupChangePolicy("Always") .withRunAsUser(65534L) .withRunAsGroup(65534L) .withSeLinuxOptions(new SELinuxOptions("s0:c123,c456", null, null, null)) .build(); SecurityContext expectedContainerSecurityContext = new SecurityContextBuilder() .withAllowPrivilegeEscalation(true) .withRunAsUser(65534L) .withRunAsGroup(65534L) .withSeLinuxOptions(new SELinuxOptions("s0:c123,c777", null, null, null)) .build(); Deployment deployment = kubernetesClient.apps().deployments().withName(request.getDefinition().getName()).get(); assertThat(deployment.getSpec().getTemplate().getSpec().getSecurityContext()) .isEqualTo(expectedPodSecurityContext); assertThatContainerExistsWithSecurityContext(deployment.getSpec().getTemplate().getSpec().getInitContainers(), "init-container-5150", expectedContainerSecurityContext); assertThatContainerExistsWithSecurityContext(deployment.getSpec().getTemplate().getSpec().getContainers(), "extra-container-5150", expectedContainerSecurityContext); assertThatContainerExistsWithSecurityContext(deployment.getSpec().getTemplate().getSpec().getContainers(), "extra-container-6160", expectedContainerSecurityContext); logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); } @Test void statefulSetWithInitContainerAndAdditionalContainers() throws IOException { Map deploymentProps = new HashMap<>(); deploymentProps.put(AppDeployer.COUNT_PROPERTY_KEY, "2"); deploymentProps.put(AppDeployer.INDEXED_PROPERTY_KEY, "true"); deploymentProps.put("spring.cloud.deployer.kubernetes.podSecurityContext", "{ fsGroup: 65534" + ", fsGroupChangePolicy: Always" + ", runAsUser: 65534" + ", runAsGroup: 65534" + ", seLinuxOptions: { level: \"s0:c123,c456\" }" + "}"); deploymentProps.put("spring.cloud.deployer.kubernetes.containerSecurityContext", "{ allowPrivilegeEscalation: true" + ", runAsUser: 65534" + ", runAsGroup: 65534" + ", seLinuxOptions: { level: \"s0:c123,c777\" }" + "}"); AppDefinition definition = new AppDefinition(randomName(), null); AppDeploymentRequest request = new AppDeploymentRequest(definition, testApplication(), deploymentProps); String imageName = testApplication().getURI().getSchemeSpecificPart(); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setStatefulSetInitContainerImageName(imageName); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(kubernetesDeployerProperties); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed)); PodSecurityContext expectedPodSecurityContext = new PodSecurityContextBuilder() .withFsGroup(65534L) .withFsGroupChangePolicy("Always") .withRunAsUser(65534L) .withRunAsGroup(65534L) .withSeLinuxOptions(new SELinuxOptions("s0:c123,c456", null, null, null)) .build(); SecurityContext expectedContainerSecurityContext = new SecurityContextBuilder() .withAllowPrivilegeEscalation(true) .withRunAsUser(65534L) .withRunAsGroup(65534L) .withSeLinuxOptions(new SELinuxOptions("s0:c123,c777", null, null, null)) .build(); Map selector = Collections.singletonMap(AbstractKubernetesDeployer.SPRING_APP_KEY, deploymentId); List statefulSets = kubernetesClient.apps().statefulSets().withLabels(selector).list().getItems(); assertThat(statefulSets).hasSize(1) .element(0, InstanceOfAssertFactories.type(StatefulSet.class)) .extracting("spec.template.spec", InstanceOfAssertFactories.type(PodSpec.class)) .satisfies((podSpec) -> { assertThat(podSpec.getSecurityContext()).isEqualTo(expectedPodSecurityContext); assertThat(podSpec.getInitContainers()) .hasSize(1) .element(0, InstanceOfAssertFactories.type(Container.class)) .extracting(Container::getSecurityContext).isEqualTo(expectedContainerSecurityContext); }); logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown)); } } private void assertThatContainerExistsWithSecurityContext(List containers, String expectedName, SecurityContext expectedSecurityContext) { assertThat(containers .stream().filter(c -> c.getName().equals(expectedName)).findFirst()) .hasValueSatisfying((initContainer) -> assertThat(initContainer.getSecurityContext()).isEqualTo(expectedSecurityContext)); } @Test public void testUnknownStatusOnPendingResources() throws InterruptedException { logger.info("Testing {}...", "UnknownStatusOnPendingResources"); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); Map props = new HashMap<>(); // requests.cpu mirrors limits.cpu when only limits is set avoiding need to set both here props.put("spring.cloud.deployer.kubernetes.limits.cpu", "5000"); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, props); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); while (kubernetesClient.pods().withLabel("spring-deployment-id", deploymentId).list().getItems().isEmpty()) { logger.info("Waiting for deployed pod"); Thread.sleep(500); } Pod pod = kubernetesClient.pods().withLabel("spring-deployment-id", deploymentId).list().getItems().get(0); while (pod.getStatus().getConditions().isEmpty()) { logger.info("Waiting for pod conditions to be set"); Thread.sleep(500); } while (!"Unschedulable".equals(pod.getStatus().getConditions().get(0).getReason())) { logger.info("Waiting for deployed pod to become Unschedulable"); } Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); assertThatThrownBy(() -> { kubernetesAppDeployer.undeploy(deploymentId); }).isInstanceOf(IllegalStateException.class) .hasMessage("App '%s' is not deployed", deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); } @Test public void testSecretRef() throws InterruptedException { logger.info("Testing {}...", "SecretRef"); Secret secret = randomSecret(); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setSecretRefs(Collections.singletonList(secret.getMetadata().getName())); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(kubernetesDeployerProperties); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); Container container = kubernetesClient.apps().deployments().withName(deploymentId).get().getSpec() .getTemplate().getSpec().getContainers().get(0); List envFromSources = container.getEnvFrom(); assertThat(envFromSources).hasSize(1); EnvFromSource envFromSource = envFromSources.get(0); assertThat(envFromSource.getSecretRef().getName()).isEqualTo(secret.getMetadata().getName()); String podEnvironment = getPodEnvironment(deploymentId); assertThat(secret.getData()).hasSize(2); for (Map.Entry secretData : secret.getData().entrySet()) { String decodedValue = new String(Base64.getDecoder().decode(secretData.getValue())); assertThat(podEnvironment).contains(secretData.getKey() + "=" + decodedValue); } logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); kubernetesClient.secrets().delete(secret); } @Test public void testSecretRefFromDeployerProperty() throws InterruptedException { logger.info("Testing {}...", "SecretRefFromDeployerProperty"); Secret secret = randomSecret(); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.secretRefs", secret.getMetadata().getName()); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, props); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); Container container = kubernetesClient.apps().deployments().withName(deploymentId).get().getSpec() .getTemplate().getSpec().getContainers().get(0); List envFromSources = container.getEnvFrom(); assertThat(envFromSources).hasSize(1); EnvFromSource envFromSource = envFromSources.get(0); assertThat(secret.getMetadata().getName()).isEqualTo(envFromSource.getSecretRef().getName()); String podEnvironment = getPodEnvironment(deploymentId); assertThat(secret.getData()).hasSize(2); for (Map.Entry secretData : secret.getData().entrySet()) { String decodedValue = new String(Base64.getDecoder().decode(secretData.getValue())); assertThat(podEnvironment).contains(secretData.getKey() + "=" + decodedValue); } logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); kubernetesClient.secrets().delete(secret); } @Test public void testSecretRefFromDeployerPropertyOverride() throws IOException, InterruptedException { logger.info("Testing {}...", "SecretRefFromDeployerPropertyOverride"); Secret propertySecret = randomSecret(); Secret deployerPropertySecret = randomSecret(); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setSecretRefs(Collections.singletonList(propertySecret.getMetadata().getName())); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(kubernetesDeployerProperties); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.secretRefs", deployerPropertySecret.getMetadata().getName()); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, props); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); Container container = kubernetesClient.apps().deployments().withName(deploymentId).get().getSpec() .getTemplate().getSpec().getContainers().get(0); List envFromSources = container.getEnvFrom(); assertThat(envFromSources).hasSize(1); EnvFromSource envFromSource = envFromSources.get(0); assertThat(envFromSource.getSecretRef().getName()).isEqualTo(deployerPropertySecret.getMetadata().getName()); String podEnvironment = getPodEnvironment(deploymentId); for (Map.Entry deployerPropertySecretData : deployerPropertySecret.getData().entrySet()) { String decodedValue = new String(Base64.getDecoder().decode(deployerPropertySecretData.getValue())); assertThat(podEnvironment).contains(deployerPropertySecretData.getKey() + "=" + decodedValue); } for (Map.Entry propertySecretData : propertySecret.getData().entrySet()) { String decodedValue = new String(Base64.getDecoder().decode(propertySecretData.getValue())); assertThat(podEnvironment).doesNotContain(propertySecretData.getKey() + "=" + decodedValue); } logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); kubernetesClient.secrets().delete(propertySecret); kubernetesClient.secrets().delete(deployerPropertySecret); } @Test public void testSecretRefFromPropertyMultiple() throws InterruptedException { logger.info("Testing {}...", "SecretRefFromPropertyMultiple"); Secret secret1 = randomSecret(); Secret secret2 = randomSecret(); List secrets = new ArrayList<>(); secrets.add(secret1.getMetadata().getName()); secrets.add(secret2.getMetadata().getName()); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setSecretRefs(secrets); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(kubernetesDeployerProperties); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, null); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); Container container = kubernetesClient.apps().deployments().withName(deploymentId).get().getSpec() .getTemplate().getSpec().getContainers().get(0); List envFromSources = container.getEnvFrom(); assertThat(envFromSources).hasSize(2); String podEnvironment = getPodEnvironment(deploymentId); EnvFromSource envFromSource1 = envFromSources.get(0); assertThat(envFromSource1.getSecretRef().getName()).isEqualTo(secret1.getMetadata().getName()); EnvFromSource envFromSource2 = envFromSources.get(1); assertThat(envFromSource2.getSecretRef().getName()).isEqualTo(secret2.getMetadata().getName()); Map mergedSecretData = new HashMap<>(); mergedSecretData.putAll(secret1.getData()); mergedSecretData.putAll(secret2.getData()); assertThat(mergedSecretData).hasSize(4); for (Map.Entry secretData : secret1.getData().entrySet()) { String decodedValue = new String(Base64.getDecoder().decode(secretData.getValue())); assertThat(podEnvironment).contains(secretData.getKey() + "=" + decodedValue); } logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); kubernetesClient.secrets().delete(secret1); kubernetesClient.secrets().delete(secret2); } @Test public void testSecretRefFromDeploymentPropertyMultiple() throws InterruptedException { logger.info("Testing {}...", "SecretRefFromDeploymentPropertyMultiple"); Secret secret1 = randomSecret(); Secret secret2 = randomSecret(); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.secretRefs", "[" + secret1.getMetadata().getName() + "," + secret2.getMetadata().getName() + "]"); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, props); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); Container container = kubernetesClient.apps().deployments().withName(deploymentId).get().getSpec() .getTemplate().getSpec().getContainers().get(0); List envFromSources = container.getEnvFrom(); assertThat(envFromSources).hasSize(2); String podEnvironment = getPodEnvironment(deploymentId); EnvFromSource envFromSource1 = envFromSources.get(0); assertThat(secret1.getMetadata().getName()).isEqualTo(envFromSource1.getSecretRef().getName()); EnvFromSource envFromSource2 = envFromSources.get(1); assertThat(secret2.getMetadata().getName()).isEqualTo(envFromSource2.getSecretRef().getName()); Map mergedSecretData = new HashMap<>(); mergedSecretData.putAll(secret1.getData()); mergedSecretData.putAll(secret2.getData()); assertThat(mergedSecretData).hasSize(4); for (Map.Entry secretData : secret1.getData().entrySet()) { String decodedValue = new String(Base64.getDecoder().decode(secretData.getValue())); assertThat(podEnvironment).contains(secretData.getKey() + "=" + decodedValue); } logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); kubernetesClient.secrets().delete(secret1); kubernetesClient.secrets().delete(secret2); } @Test public void testConfigMapRef() throws InterruptedException { logger.info("Testing {}...", "ConfigMapRef"); ConfigMap configMap = randomConfigMap(); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setConfigMapRefs(Collections.singletonList(configMap.getMetadata().getName())); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(kubernetesDeployerProperties); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); Container container = kubernetesClient.apps().deployments().withName(deploymentId).get().getSpec() .getTemplate().getSpec().getContainers().get(0); List envFromSources = container.getEnvFrom(); assertThat(envFromSources).hasSize(1); EnvFromSource envFromSource = envFromSources.get(0); assertThat(envFromSource.getConfigMapRef().getName()).isEqualTo(configMap.getMetadata().getName()); String podEnvironment = getPodEnvironment(deploymentId); assertThat(configMap.getData()).hasSize(2); for (Map.Entry configMapData : configMap.getData().entrySet()) { assertThat(podEnvironment).contains(configMapData.getKey() + "=" + configMapData.getValue()); } logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); kubernetesClient.configMaps().delete(configMap); } @Test public void testConfigMapRefFromDeployerProperty() throws InterruptedException { logger.info("Testing {}...", "ConfigMapRefFromDeployerProperty"); ConfigMap configMap = randomConfigMap(); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.config-map-refs", configMap.getMetadata().getName()); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, props); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); Container container = kubernetesClient.apps().deployments().withName(deploymentId).get().getSpec() .getTemplate().getSpec().getContainers().get(0); List envFromSources = container.getEnvFrom(); assertThat(envFromSources).hasSize(1); EnvFromSource envFromSource = envFromSources.get(0); assertThat(envFromSource.getConfigMapRef().getName()).isEqualTo(configMap.getMetadata().getName()); String podEnvironment = getPodEnvironment(deploymentId); assertThat(configMap.getData()).hasSize(2); for (Map.Entry configMapData : configMap.getData().entrySet()) { assertThat(podEnvironment).contains(configMapData.getKey() + "=" + configMapData.getValue()); } logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); kubernetesClient.configMaps().delete(configMap); } @Test public void testConfigMapRefFromDeployerPropertyOverride() throws IOException, InterruptedException { logger.info("Testing {}...", "ConfigMapRefFromDeployerPropertyOverride"); ConfigMap propertyConfigMap = randomConfigMap(); ConfigMap deployerPropertyConfigMap = randomConfigMap(); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setConfigMapRefs(Collections.singletonList(propertyConfigMap.getMetadata().getName())); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(kubernetesDeployerProperties); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.configMapRefs", deployerPropertyConfigMap.getMetadata().getName()); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, props); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); Container container = kubernetesClient.apps().deployments().withName(deploymentId).get().getSpec() .getTemplate().getSpec().getContainers().get(0); List envFromSources = container.getEnvFrom(); assertThat(envFromSources).hasSize(1); EnvFromSource envFromSource = envFromSources.get(0); assertThat(deployerPropertyConfigMap.getMetadata().getName()).isEqualTo(envFromSource.getConfigMapRef().getName()); String podEnvironment = getPodEnvironment(deploymentId); for (Map.Entry deployerPropertyConfigMapData : deployerPropertyConfigMap.getData().entrySet()) { assertThat(podEnvironment) .contains(deployerPropertyConfigMapData.getKey() + "=" + deployerPropertyConfigMapData.getValue()); } for (Map.Entry propertyConfigMapData : propertyConfigMap.getData().entrySet()) { assertThat(podEnvironment).doesNotContain(propertyConfigMapData.getKey() + "=" + propertyConfigMapData.getValue()); } logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); kubernetesClient.configMaps().delete(propertyConfigMap); kubernetesClient.configMaps().delete(deployerPropertyConfigMap); } @Test public void testConfigMapRefFromPropertyMultiple() throws InterruptedException { logger.info("Testing {}...", "ConfigMapRefFromPropertyMultiple"); ConfigMap configMap1 = randomConfigMap(); ConfigMap configMap2 = randomConfigMap(); List configMaps = new ArrayList<>(); configMaps.add(configMap1.getMetadata().getName()); configMaps.add(configMap2.getMetadata().getName()); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setConfigMapRefs(configMaps); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(kubernetesDeployerProperties); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, null); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); Container container = kubernetesClient.apps().deployments().withName(deploymentId).get().getSpec() .getTemplate().getSpec().getContainers().get(0); List envFromSources = container.getEnvFrom(); assertThat(envFromSources).hasSize(2); String podEnvironment = getPodEnvironment(deploymentId); EnvFromSource envFromSource1 = envFromSources.get(0); assertThat(configMap1.getMetadata().getName()).isEqualTo(envFromSource1.getConfigMapRef().getName()); EnvFromSource envFromSource2 = envFromSources.get(1); assertThat(configMap2.getMetadata().getName()).isEqualTo(envFromSource2.getConfigMapRef().getName()); Map mergedConfigMapData = new HashMap<>(); mergedConfigMapData.putAll(configMap1.getData()); mergedConfigMapData.putAll(configMap2.getData()); assertThat(mergedConfigMapData).hasSize(4); for (Map.Entry configMapData : configMap1.getData().entrySet()) { assertThat(podEnvironment).contains(configMapData.getKey() + "=" + configMapData.getValue()); } logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); kubernetesClient.configMaps().delete(configMap1); kubernetesClient.configMaps().delete(configMap2); } @Test public void testConfigMapRefFromDeploymentPropertyMultiple() throws InterruptedException { logger.info("Testing {}...", "ConfigMapRefFromDeploymentPropertyMultiple"); ConfigMap configMap1 = randomConfigMap(); ConfigMap configMap2 = randomConfigMap(); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.configMapRefs", "[" + configMap1.getMetadata().getName() + "," + configMap2.getMetadata().getName() + "]"); KubernetesAppDeployer kubernetesAppDeployer = kubernetesAppDeployer(); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, props); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = kubernetesAppDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); Container container = kubernetesClient.apps().deployments().withName(deploymentId).get().getSpec() .getTemplate().getSpec().getContainers().get(0); List envFromSources = container.getEnvFrom(); assertThat(envFromSources).isNotNull(); assertThat(envFromSources).hasSize(2); String podEnvironment = getPodEnvironment(deploymentId); EnvFromSource envFromSource1 = envFromSources.get(0); assertThat(envFromSource1.getConfigMapRef().getName()).isEqualTo(configMap1.getMetadata().getName()); EnvFromSource envFromSource2 = envFromSources.get(1); assertThat(envFromSource2.getConfigMapRef().getName()).isEqualTo(configMap2.getMetadata().getName()); Map mergedConfigMapData = new HashMap<>(); mergedConfigMapData.putAll(configMap1.getData()); mergedConfigMapData.putAll(configMap2.getData()); assertThat(mergedConfigMapData).hasSize(4); for (Map.Entry configMapData : configMap1.getData().entrySet()) { assertThat(podEnvironment).contains(configMapData.getKey() + "=" + configMapData.getValue()); } logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); kubernetesAppDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); kubernetesClient.configMaps().delete(configMap1); kubernetesClient.configMaps().delete(configMap2); } @Override protected String randomName() { // Kubernetes service names must start with a letter and can only be 24 characters long return "app-" + UUID.randomUUID().toString().substring(0, 18); } @Override protected Timeout deploymentTimeout() { return new Timeout(300, 2000); } @Override protected Resource testApplication() { return new DockerResource("springcloud/spring-cloud-deployer-spi-test-app:latest"); } private String getPodEnvironment(String deploymentId) throws InterruptedException { String podName = kubernetesClient.pods().withLabel("spring-deployment-id", deploymentId).list().getItems() .get(0).getMetadata().getName(); final CountDownLatch countDownLatch = new CountDownLatch(1); ByteArrayOutputStream execOutputStream = new ByteArrayOutputStream(); ExecWatch watch = kubernetesClient.pods().withName(podName).inContainer(deploymentId) .writingOutput(execOutputStream) .usingListener(new ExecListener() { @Override public void onFailure(Throwable throwable, Response response) { countDownLatch.countDown(); } @Override public void onClose(int code, String reason) { countDownLatch.countDown(); } }).exec("printenv"); countDownLatch.await(); byte[] bytes = execOutputStream.toByteArray(); watch.close(); return new String(bytes); } // Creates a Secret with a name will be generated prefixed by "secret-" followed by random numbers. // // Two data keys are present, both prefixed by "d-" followed by random numbers. This allows for cases where // multiple Secrets may be read into environment variables avoiding variable name clashes. // // Data values are Base64 encoded strings of value1 and value2 private Secret randomSecret() { Map secretData = new HashMap<>(); secretData.put("d-" + UUID.randomUUID().toString().substring(0, 5), "dmFsdWUx"); secretData.put("d-" + UUID.randomUUID().toString().substring(0, 5), "dmFsdWUy"); Secret secret = new Secret(); secret.setData(secretData); ObjectMeta objectMeta = new ObjectMeta(); objectMeta.setName("secret-" + UUID.randomUUID().toString().substring(0, 5)); secret.setMetadata(objectMeta); return kubernetesClient.secrets().create(secret); } // Creates a ConfigMap with a name will be generated prefixed by "cm-" followed by random numbers. // // Two data keys are present, both prefixed by "d-" followed by random numbers. This allows for cases where // multiple ConfigMaps may be read into environment variables avoiding variable name clashes. // // Data values are strings of value1 and value private ConfigMap randomConfigMap() { Map configMapData = new HashMap<>(); configMapData.put("d-" + UUID.randomUUID().toString().substring(0, 5), "value1"); configMapData.put("d-" + UUID.randomUUID().toString().substring(0, 5), "value2"); ConfigMap configMap = new ConfigMap(); configMap.setData(configMapData); ObjectMeta objectMeta = new ObjectMeta(); objectMeta.setName("cm-" + UUID.randomUUID().toString().substring(0, 5)); configMap.setMetadata(objectMeta); return kubernetesClient.configMaps().create(configMap); } private KubernetesAppDeployer kubernetesAppDeployer() { return kubernetesAppDeployer(new KubernetesDeployerProperties()); } private KubernetesAppDeployer kubernetesAppDeployer(KubernetesDeployerProperties kubernetesDeployerProperties) { return new KubernetesAppDeployer(kubernetesDeployerProperties, this.kubernetesClient, new DefaultContainerFactory(kubernetesDeployerProperties)); } } ================================================ FILE: src/test/java/org/springframework/cloud/deployer/spi/kubernetes/KubernetesAppDeployerTests.java ================================================ /* * Copyright 2015-2023 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.cloud.deployer.spi.kubernetes; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import io.fabric8.kubernetes.api.model.AffinityBuilder; import io.fabric8.kubernetes.api.model.Capabilities; import io.fabric8.kubernetes.api.model.ConfigMapKeySelector; import io.fabric8.kubernetes.api.model.Container; import io.fabric8.kubernetes.api.model.EnvVar; import io.fabric8.kubernetes.api.model.HostPathVolumeSource; import io.fabric8.kubernetes.api.model.HostPathVolumeSourceBuilder; import io.fabric8.kubernetes.api.model.LabelSelector; import io.fabric8.kubernetes.api.model.LabelSelectorRequirementBuilder; import io.fabric8.kubernetes.api.model.NodeAffinity; import io.fabric8.kubernetes.api.model.NodeSelectorRequirementBuilder; import io.fabric8.kubernetes.api.model.NodeSelectorTerm; import io.fabric8.kubernetes.api.model.PodAffinity; import io.fabric8.kubernetes.api.model.PodAffinityTerm; import io.fabric8.kubernetes.api.model.PodAntiAffinity; import io.fabric8.kubernetes.api.model.PodSecurityContext; import io.fabric8.kubernetes.api.model.PodSecurityContextBuilder; import io.fabric8.kubernetes.api.model.PodSpec; import io.fabric8.kubernetes.api.model.PreferredSchedulingTerm; import io.fabric8.kubernetes.api.model.SELinuxOptions; import io.fabric8.kubernetes.api.model.SeccompProfile; import io.fabric8.kubernetes.api.model.SecretKeySelector; import io.fabric8.kubernetes.api.model.SecurityContext; import io.fabric8.kubernetes.api.model.SecurityContextBuilder; import io.fabric8.kubernetes.api.model.Sysctl; import io.fabric8.kubernetes.api.model.Toleration; import io.fabric8.kubernetes.api.model.VolumeBuilder; import io.fabric8.kubernetes.api.model.WeightedPodAffinityTerm; import io.fabric8.kubernetes.api.model.WindowsSecurityContextOptions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.config.YamlPropertiesFactoryBean; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; import org.springframework.cloud.deployer.resource.docker.DockerResource; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.entry; /** * Unit tests for {@link KubernetesAppDeployer} * * @author Donovan Muller * @author David Turanski * @author Ilayaperumal Gopinathan * @author Chris Schaefer * @author Enrique Medina Montenegro * @author Chris Bono */ @DisplayName("KubernetesAppDeployer") public class KubernetesAppDeployerTests { private KubernetesAppDeployer deployer; private DeploymentPropertiesResolver deploymentPropertiesResolver = new DeploymentPropertiesResolver( KubernetesDeployerProperties.KUBERNETES_DEPLOYER_PROPERTIES_PREFIX, new KubernetesDeployerProperties()); @Test public void deployWithVolumesOnly() throws Exception { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), new HashMap<>()); deployer = k8sAppDeployer(bindDeployerProperties()); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getVolumes()).isEmpty(); } @Test public void deployWithVolumesAndVolumeMounts() throws Exception { AppDefinition definition = new AppDefinition("app-test", null); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.volumeMounts", "[" + "{name: 'testpvc', mountPath: '/test/pvc'}, " + "{name: 'testnfs', mountPath: '/test/nfs', readOnly: 'true'}" + "]"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getVolumes()).containsOnly( // volume 'testhostpath' defined in dataflow-server.yml should not be added // as there is no corresponding volume mount new VolumeBuilder().withName("testpvc").withNewPersistentVolumeClaim("testClaim", true).build(), new VolumeBuilder().withName("testnfs").withNewNfs("/test/nfs", null, "10.0.0.1:111").build()); props.clear(); props.put("spring.cloud.deployer.kubernetes.volumes", "[" + "{name: testhostpath, hostPath: { path: '/test/override/hostPath' }}," + "{name: 'testnfs', nfs: { server: '192.168.1.1:111', path: '/test/override/nfs' }} " + "]"); props.put("spring.cloud.deployer.kubernetes.volumeMounts", "[" + "{name: 'testhostpath', mountPath: '/test/hostPath'}, " + "{name: 'testpvc', mountPath: '/test/pvc'}, " + "{name: 'testnfs', mountPath: '/test/nfs', readOnly: 'true'}" + "]"); appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(); podSpec = deployer.createPodSpec(appDeploymentRequest); HostPathVolumeSource hostPathVolumeSource = new HostPathVolumeSourceBuilder() .withPath("/test/override/hostPath").build(); assertThat(podSpec.getVolumes()).containsOnly( new VolumeBuilder().withName("testhostpath").withHostPath(hostPathVolumeSource).build(), new VolumeBuilder().withName("testpvc").withNewPersistentVolumeClaim("testClaim", true).build(), new VolumeBuilder().withName("testnfs").withNewNfs("/test/override/nfs", null, "192.168.1.1:111").build()); } @Test public void deployWithNodeSelectorGlobalProperty() throws Exception { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), null); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setNodeSelector("disktype:ssd, os:qnx"); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getNodeSelector()).containsOnly(entry("disktype", "ssd"), entry("os", "qnx")); } @Test public void deployWithNodeSelectorDeploymentProperty() throws Exception { AppDefinition definition = new AppDefinition("app-test", null); Map props = new HashMap<>(); props.put(KubernetesDeployerProperties.KUBERNETES_DEPLOYMENT_NODE_SELECTOR, "disktype:ssd, os: linux"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getNodeSelector()).containsOnly(entry("disktype", "ssd"), entry("os", "linux")); } @Test public void deployWithNodeSelectorDeploymentPropertyGlobalOverride() throws Exception { AppDefinition definition = new AppDefinition("app-test", null); Map props = new HashMap<>(); props.put(KubernetesDeployerProperties.KUBERNETES_DEPLOYMENT_NODE_SELECTOR, "disktype:ssd, os: openbsd"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setNodeSelector("disktype:ssd, os:qnx"); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getNodeSelector()).containsOnly(entry("disktype", "ssd"), entry("os", "openbsd")); } @Test public void deployWithEnvironmentWithCommaDelimitedValue() throws Exception { AppDefinition definition = new AppDefinition("app-test", null); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.environmentVariables", "JAVA_TOOL_OPTIONS='thing1,thing2',foo='bar,baz',car=caz,boo='zoo,gnu',doo=dar,OPTS='thing1'"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getContainers().get(0).getEnv()) .contains( new EnvVar("foo", "bar,baz", null), new EnvVar("car", "caz", null), new EnvVar("boo", "zoo,gnu", null), new EnvVar("doo", "dar", null), new EnvVar("JAVA_TOOL_OPTIONS", "thing1,thing2", null), new EnvVar("OPTS", "thing1", null)); } @Test public void deployWithEnvironmentWithSingleCommaDelimitedValue() throws Exception { AppDefinition definition = new AppDefinition("app-test", null); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.environmentVariables", "JAVA_TOOL_OPTIONS='thing1,thing2'"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getContainers().get(0).getEnv()) .contains(new EnvVar("JAVA_TOOL_OPTIONS", "thing1,thing2", null)); } @Test public void deployWithEnvironmentWithMultipleCommaDelimitedValue() throws Exception { AppDefinition definition = new AppDefinition("app-test", null); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.environmentVariables", "JAVA_TOOL_OPTIONS='thing1,thing2',OPTS='thing3, thing4'"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getContainers().get(0).getEnv()) .contains( new EnvVar("JAVA_TOOL_OPTIONS", "thing1,thing2", null), new EnvVar("OPTS", "thing3, thing4", null)); } @Test public void deployWithImagePullSecretDeploymentProperty() { AppDefinition definition = new AppDefinition("app-test", null); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.imagePullSecret", "regcred"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(new KubernetesDeployerProperties()); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getImagePullSecrets().size()).isEqualTo(1); assertThat(podSpec.getImagePullSecrets().get(0).getName()).isEqualTo("regcred"); } @Test public void deployWithImagePullSecretDeployerProperty() { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), null); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setImagePullSecret("regcred"); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getImagePullSecrets().size()).isEqualTo(1); assertThat(podSpec.getImagePullSecrets().get(0).getName()).isEqualTo("regcred"); } @Test public void deployWithImagePullSecretsDeploymentProperty() { AppDefinition definition = new AppDefinition("app-test", null); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.imagePullSecrets", "['regcredone','regcredtwo']"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(new KubernetesDeployerProperties()); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getImagePullSecrets().size()).isEqualTo(2); assertThat(podSpec.getImagePullSecrets().get(0).getName()).isEqualTo("regcredone"); assertThat(podSpec.getImagePullSecrets().get(1).getName()).isEqualTo("regcredtwo"); } @Test public void deployWithImagePullSecretsDeployerProperty() { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), null); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setImagePullSecrets(Arrays.asList("regcredone", "regcredtwo")); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getImagePullSecrets().size()).isEqualTo(2); assertThat(podSpec.getImagePullSecrets().get(0).getName()).isEqualTo("regcredone"); assertThat(podSpec.getImagePullSecrets().get(1).getName()).isEqualTo("regcredtwo"); } @Test public void deployWithDeploymentServiceAccountNameDeploymentProperties() { AppDefinition definition = new AppDefinition("app-test", null); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.deploymentServiceAccountName", "myserviceaccount"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(new KubernetesDeployerProperties()); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getServiceAccountName()).isNotNull(); assertThat(podSpec.getServiceAccountName().equals("myserviceaccount")); } @Test public void deployWithDeploymentServiceAccountNameDeployerProperty() { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), null); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setDeploymentServiceAccountName("myserviceaccount"); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getServiceAccountName()).isNotNull(); assertThat(podSpec.getServiceAccountName().equals("myserviceaccount")); } @Test public void deployWithDeploymentServiceAccountNameDeploymentPropertyOverride() { AppDefinition definition = new AppDefinition("app-test", null); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.deploymentServiceAccountName", "overridesan"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setDeploymentServiceAccountName("defaultsan"); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getServiceAccountName()).isNotNull(); assertThat(podSpec.getServiceAccountName().equals("overridesan")); } @Test public void deployWithTolerations() throws Exception { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), new HashMap<>()); deployer = k8sAppDeployer(); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getTolerations()).isNotEmpty(); } @Test public void deployWithGlobalTolerations() { AppDefinition definition = new AppDefinition("app-test", null); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.tolerations", "[{key: 'test', value: 'true', operator: 'Equal', effect: 'NoSchedule', tolerationSeconds: 5}, " + "{key: 'test2', value: 'false', operator: 'Equal', effect: 'NoSchedule', tolerationSeconds: 5}]"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(new KubernetesDeployerProperties()); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getTolerations()).isNotNull(); assertThat(podSpec.getTolerations().size() == 2); assertThat(podSpec.getTolerations().contains(new Toleration("NoSchedule", "test", "Equal", 5L, "true"))); assertThat(podSpec.getTolerations().contains(new Toleration("NoSchedule", "test2", "Equal", 5L, "false"))); } @Test public void deployWithTolerationPropertyOverride() { AppDefinition definition = new AppDefinition("app-test", null); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.tolerations", "[{key: 'test', value: 'true', operator: 'Equal', effect: 'NoSchedule', tolerationSeconds: 5}, " + "{key: 'test2', value: 'false', operator: 'Equal', effect: 'NoSchedule', tolerationSeconds: 5}]"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); KubernetesDeployerProperties.Toleration toleration = new KubernetesDeployerProperties.Toleration(); toleration.setEffect("NoSchedule"); toleration.setKey("test"); toleration.setOperator("Equal"); toleration.setTolerationSeconds(5L); toleration.setValue("false"); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.getTolerations().add(toleration); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getTolerations()).isNotNull(); assertThat(podSpec.getTolerations().size() == 2); assertThat(podSpec.getTolerations().contains(new Toleration("NoSchedule", "test", "Equal", 5L, "true"))); assertThat(podSpec.getTolerations().contains(new Toleration("NoSchedule", "test2", "Equal", 5L, "false"))); } @Test public void deployWithDuplicateTolerationKeyPropertyOverride() { AppDefinition definition = new AppDefinition("app-test", null); Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.tolerations", "[{key: 'test', value: 'true', operator: 'Equal', effect: 'NoSchedule', tolerationSeconds: 5}]"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); KubernetesDeployerProperties.Toleration toleration = new KubernetesDeployerProperties.Toleration(); toleration.setEffect("NoSchedule"); toleration.setKey("test"); toleration.setOperator("Equal"); toleration.setTolerationSeconds(5L); toleration.setValue("false"); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.getTolerations().add(toleration); kubernetesDeployerProperties.setStartupHttpProbePort(kubernetesDeployerProperties.getLivenessHttpProbePort()); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getTolerations()).isNotNull(); assertThat(podSpec.getTolerations().size() == 1); assertThat(podSpec.getTolerations().contains(new Toleration("NoSchedule", "test2", "Equal", 5L, "false"))); } @Test public void deployWithDuplicateGlobalToleration() { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), null); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); KubernetesDeployerProperties.Toleration toleration1 = new KubernetesDeployerProperties.Toleration(); toleration1.setEffect("NoSchedule"); toleration1.setKey("test"); toleration1.setOperator("Equal"); toleration1.setTolerationSeconds(5L); toleration1.setValue("false"); kubernetesDeployerProperties.getTolerations().add(toleration1); KubernetesDeployerProperties.Toleration toleration2 = new KubernetesDeployerProperties.Toleration(); toleration2.setEffect("NoSchedule"); toleration2.setKey("test"); toleration2.setOperator("Equal"); toleration2.setTolerationSeconds(5L); toleration2.setValue("true"); kubernetesDeployerProperties.getTolerations().add(toleration2); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getTolerations()).isNotNull(); assertThat(podSpec.getTolerations().size() == 1); assertThat(podSpec.getTolerations().contains(new Toleration("NoSchedule", "test2", "Equal", 5L, "true"))); } @Test public void testInvalidDeploymentLabelDelimiter() { Map props = Collections.singletonMap("spring.cloud.deployer.kubernetes.deploymentLabels", "label1|value1"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); assertThatThrownBy(() -> { this.deploymentPropertiesResolver.getDeploymentLabels(appDeploymentRequest.getDeploymentProperties()); }).isInstanceOf(IllegalArgumentException.class); } @Test public void testInvalidMultipleDeploymentLabelDelimiter() { Map props = Collections.singletonMap("spring.cloud.deployer.kubernetes.deploymentLabels", "label1:value1 label2:value2"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); assertThatThrownBy(() -> { this.deploymentPropertiesResolver.getDeploymentLabels(appDeploymentRequest.getDeploymentProperties()); }).isInstanceOf(IllegalArgumentException.class); } @Test public void testDeploymentLabels() { Map props = Collections.singletonMap("spring.cloud.deployer.kubernetes.deploymentLabels", "label1:value1,label2:value2"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); Map deploymentLabels = this.deploymentPropertiesResolver.getDeploymentLabels(appDeploymentRequest.getDeploymentProperties()); assertThat(deploymentLabels).isNotEmpty(); assertThat(deploymentLabels.size()).as("Invalid number of labels").isEqualTo(2); assertThat(deploymentLabels).containsKey("label1"); assertThat(deploymentLabels.get("label1")).as("Invalid value for 'label1'").isEqualTo("value1"); assertThat(deploymentLabels).containsKey("label2"); assertThat(deploymentLabels.get("label2")).as("Invalid value for 'label2'").isEqualTo("value2"); } @Test public void testSecretKeyRef() { Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.secretKeyRefs", "[{envVarName: 'SECRET_PASSWORD', secretName: 'mySecret', dataKey: 'password'}]"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(new KubernetesDeployerProperties()); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); List envVars = podSpec.getContainers().get(0).getEnv(); assertThat(envVars.size()).as("Invalid number of env vars").isEqualTo(2); EnvVar secretKeyRefEnvVar = envVars.get(0); assertThat(secretKeyRefEnvVar.getName()).as("Unexpected env var name").isEqualTo("SECRET_PASSWORD"); SecretKeySelector secretKeySelector = secretKeyRefEnvVar.getValueFrom().getSecretKeyRef(); assertThat(secretKeySelector.getName()).as("Unexpected secret name").isEqualTo("mySecret"); assertThat(secretKeySelector.getKey()).as("Unexpected secret data key").isEqualTo("password"); } @Test public void testSecretKeyRefMultiple() { Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.secretKeyRefs", "[{envVarName: 'SECRET_PASSWORD', secretName: 'mySecret', dataKey: 'password'}," + "{envVarName: 'SECRET_USERNAME', secretName: 'mySecret2', dataKey: 'username'}]"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(new KubernetesDeployerProperties()); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); List envVars = podSpec.getContainers().get(0).getEnv(); assertThat(envVars.size()).as("Invalid number of env vars").isEqualTo(3); EnvVar secretKeyRefEnvVar = envVars.get(0); assertThat(secretKeyRefEnvVar.getName()).as("Unexpected env var name").isEqualTo("SECRET_PASSWORD"); SecretKeySelector secretKeySelector = secretKeyRefEnvVar.getValueFrom().getSecretKeyRef(); assertThat(secretKeySelector.getName()).as("Unexpected secret name").isEqualTo("mySecret"); assertThat(secretKeySelector.getKey()).as("Unexpected secret data key").isEqualTo("password"); secretKeyRefEnvVar = envVars.get(1); assertThat(secretKeyRefEnvVar.getName()).as("Unexpected env var name").isEqualTo("SECRET_USERNAME"); secretKeySelector = secretKeyRefEnvVar.getValueFrom().getSecretKeyRef(); assertThat(secretKeySelector.getName()).as("Unexpected secret name").isEqualTo("mySecret2"); assertThat(secretKeySelector.getKey()).as("Unexpected secret data key").isEqualTo("username"); } @Test public void testSecretKeyRefGlobal() { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), null); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); KubernetesDeployerProperties.SecretKeyRef secretKeyRef = new KubernetesDeployerProperties.SecretKeyRef(); secretKeyRef.setEnvVarName("SECRET_PASSWORD_GLOBAL"); secretKeyRef.setSecretName("mySecretGlobal"); secretKeyRef.setDataKey("passwordGlobal"); kubernetesDeployerProperties.setSecretKeyRefs(Collections.singletonList(secretKeyRef)); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); List envVars = podSpec.getContainers().get(0).getEnv(); assertThat(envVars.size()).as("Invalid number of env vars").isEqualTo(2); EnvVar secretKeyRefEnvVar = envVars.get(0); assertThat(secretKeyRefEnvVar.getName()).as("Unexpected env var name").isEqualTo("SECRET_PASSWORD_GLOBAL"); SecretKeySelector secretKeySelector = secretKeyRefEnvVar.getValueFrom().getSecretKeyRef(); assertThat(secretKeySelector.getName()).as("Unexpected secret name").isEqualTo("mySecretGlobal"); assertThat(secretKeySelector.getKey()).as("Unexpected secret data key").isEqualTo("passwordGlobal"); } @Test public void testSecretKeyRefPropertyOverride() { Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.secretKeyRefs", "[{envVarName: 'SECRET_PASSWORD_GLOBAL', secretName: 'mySecret', dataKey: 'password'}," + "{envVarName: 'SECRET_USERNAME', secretName: 'mySecret2', dataKey: 'username'}]"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); List globalSecretKeyRefs = new ArrayList<>(); KubernetesDeployerProperties.SecretKeyRef globalSecretKeyRef1 = new KubernetesDeployerProperties.SecretKeyRef(); globalSecretKeyRef1.setEnvVarName("SECRET_PASSWORD_GLOBAL"); globalSecretKeyRef1.setSecretName("mySecretGlobal"); globalSecretKeyRef1.setDataKey("passwordGlobal"); KubernetesDeployerProperties.SecretKeyRef globalSecretKeyRef2 = new KubernetesDeployerProperties.SecretKeyRef(); globalSecretKeyRef2.setEnvVarName("SECRET_USERNAME_GLOBAL"); globalSecretKeyRef2.setSecretName("mySecretGlobal"); globalSecretKeyRef2.setDataKey("usernameGlobal"); globalSecretKeyRefs.add(globalSecretKeyRef1); globalSecretKeyRefs.add(globalSecretKeyRef2); kubernetesDeployerProperties.setSecretKeyRefs(globalSecretKeyRefs); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); List envVars = podSpec.getContainers().get(0).getEnv(); assertThat(envVars.size()).as("Invalid number of env vars").isEqualTo(4); // deploy prop overrides global EnvVar secretKeyRefEnvVar = envVars.get(0); assertThat(secretKeyRefEnvVar.getName()).as("Unexpected env var name").isEqualTo("SECRET_PASSWORD_GLOBAL"); SecretKeySelector secretKeySelector = secretKeyRefEnvVar.getValueFrom().getSecretKeyRef(); assertThat(secretKeySelector.getName()).as("Unexpected secret name").isEqualTo("mySecret"); assertThat(secretKeySelector.getKey()).as("Unexpected secret data key").isEqualTo("password"); // unique deploy prop secretKeyRefEnvVar = envVars.get(1); assertThat(secretKeyRefEnvVar.getName()).as("Unexpected env var name").isEqualTo("SECRET_USERNAME"); secretKeySelector = secretKeyRefEnvVar.getValueFrom().getSecretKeyRef(); assertThat(secretKeySelector.getName()).as("Unexpected secret name").isEqualTo("mySecret2"); assertThat(secretKeySelector.getKey()).as("Unexpected secret data key").isEqualTo("username"); // unique, non-overridden global prop secretKeyRefEnvVar = envVars.get(2); assertThat(secretKeyRefEnvVar.getName()).as("Unexpected env var name").isEqualTo("SECRET_USERNAME_GLOBAL"); secretKeySelector = secretKeyRefEnvVar.getValueFrom().getSecretKeyRef(); assertThat(secretKeySelector.getName()).as("Unexpected secret name").isEqualTo("mySecretGlobal"); assertThat(secretKeySelector.getKey()).as("Unexpected secret data key").isEqualTo("usernameGlobal"); } @Test public void testSecretKeyRefGlobalFromYaml() throws Exception { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), null); deployer = k8sAppDeployer(); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); List envVars = podSpec.getContainers().get(0).getEnv(); assertThat(envVars.size()).as("Invalid number of env vars").isEqualTo(3); EnvVar secretKeyRefEnvVar = envVars.get(0); assertThat(secretKeyRefEnvVar.getName()).as("Unexpected env var name").isEqualTo("SECRET_PASSWORD"); SecretKeySelector secretKeySelector = secretKeyRefEnvVar.getValueFrom().getSecretKeyRef(); assertThat(secretKeySelector.getName()).as("Unexpected secret name").isEqualTo("mySecret"); assertThat(secretKeySelector.getKey()).as("Unexpected secret data key").isEqualTo("myPassword"); } @Test public void testConfigMapKeyRef() { Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.configMapKeyRefs", "[{envVarName: 'MY_ENV', configMapName: 'myConfigMap', dataKey: 'envName'}]"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(new KubernetesDeployerProperties()); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); List envVars = podSpec.getContainers().get(0).getEnv(); assertThat(envVars.size()).as("Invalid number of env vars").isEqualTo(2); EnvVar configMapKeyRefEnvVar = envVars.get(0); assertThat(configMapKeyRefEnvVar.getName()).as("Unexpected env var name").isEqualTo("MY_ENV"); ConfigMapKeySelector configMapKeySelector = configMapKeyRefEnvVar.getValueFrom().getConfigMapKeyRef(); assertThat(configMapKeySelector.getName()).as("Unexpected config map name").isEqualTo("myConfigMap"); assertThat(configMapKeySelector.getKey()).as("Unexpected config map data key").isEqualTo("envName"); } @Test public void testConfigMapKeyRefMultiple() { Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.configMapKeyRefs", "[{envVarName: 'MY_ENV', configMapName: 'myConfigMap', dataKey: 'envName'}," + "{envVarName: 'ENV_VALUES', configMapName: 'myOtherConfigMap', dataKey: 'diskType'}]"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(new KubernetesDeployerProperties()); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); List envVars = podSpec.getContainers().get(0).getEnv(); assertThat(envVars.size()).as("Invalid number of env vars").isEqualTo(3); EnvVar configMapKeyRefEnvVar = envVars.get(0); assertThat(configMapKeyRefEnvVar.getName()).as("Unexpected env var name").isEqualTo("MY_ENV"); ConfigMapKeySelector configMapKeySelector = configMapKeyRefEnvVar.getValueFrom().getConfigMapKeyRef(); assertThat(configMapKeySelector.getName()).as("Unexpected config map name").isEqualTo("myConfigMap"); assertThat(configMapKeySelector.getKey()).as("Unexpected config map data key").isEqualTo("envName"); configMapKeyRefEnvVar = envVars.get(1); assertThat(configMapKeyRefEnvVar.getName()).as("Unexpected env var name").isEqualTo("ENV_VALUES"); configMapKeySelector = configMapKeyRefEnvVar.getValueFrom().getConfigMapKeyRef(); assertThat(configMapKeySelector.getName()).as("Unexpected config map name").isEqualTo("myOtherConfigMap"); assertThat(configMapKeySelector.getKey()).as("Unexpected config map data key").isEqualTo("diskType"); } @Test public void testConfigMapKeyRefGlobal() { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), null); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); KubernetesDeployerProperties.ConfigMapKeyRef configMapKeyRef = new KubernetesDeployerProperties.ConfigMapKeyRef(); configMapKeyRef.setEnvVarName("MY_ENV_GLOBAL"); configMapKeyRef.setConfigMapName("myConfigMapGlobal"); configMapKeyRef.setDataKey("envGlobal"); kubernetesDeployerProperties.setConfigMapKeyRefs(Collections.singletonList(configMapKeyRef)); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); List envVars = podSpec.getContainers().get(0).getEnv(); assertThat(envVars.size()).as("Invalid number of env vars").isEqualTo(2); EnvVar configMapKeyRefEnvVar = envVars.get(0); assertThat(configMapKeyRefEnvVar.getName()).as("Unexpected env var name").isEqualTo("MY_ENV_GLOBAL"); ConfigMapKeySelector configMapKeySelector = configMapKeyRefEnvVar.getValueFrom().getConfigMapKeyRef(); assertThat(configMapKeySelector.getName()).as("Unexpected config map name").isEqualTo("myConfigMapGlobal"); assertThat(configMapKeySelector.getKey()).as("Unexpected config data key").isEqualTo("envGlobal"); } @Test public void testConfigMapKeyRefPropertyOverride() { Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.configMapKeyRefs", "[{envVarName: 'MY_ENV', configMapName: 'myConfigMap', dataKey: 'envName'}," + "{envVarName: 'ENV_VALUES', configMapName: 'myOtherConfigMap', dataKey: 'diskType'}]"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); List globalConfigMapKeyRefs = new ArrayList<>(); KubernetesDeployerProperties.ConfigMapKeyRef globalConfigMapKeyRef1 = new KubernetesDeployerProperties.ConfigMapKeyRef(); globalConfigMapKeyRef1.setEnvVarName("MY_ENV"); globalConfigMapKeyRef1.setConfigMapName("myEnvGlobal"); globalConfigMapKeyRef1.setDataKey("envGlobal"); KubernetesDeployerProperties.ConfigMapKeyRef globalConfigMapKeyRef2 = new KubernetesDeployerProperties.ConfigMapKeyRef(); globalConfigMapKeyRef2.setEnvVarName("MY_VALS_GLOBAL"); globalConfigMapKeyRef2.setConfigMapName("myValsGlobal"); globalConfigMapKeyRef2.setDataKey("valsGlobal"); globalConfigMapKeyRefs.add(globalConfigMapKeyRef1); globalConfigMapKeyRefs.add(globalConfigMapKeyRef2); kubernetesDeployerProperties.setConfigMapKeyRefs(globalConfigMapKeyRefs); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); List envVars = podSpec.getContainers().get(0).getEnv(); assertThat(envVars.size()).as("Invalid number of env vars").isEqualTo(4); // deploy prop overrides global EnvVar configMapKeyRefEnvVar = envVars.get(0); assertThat(configMapKeyRefEnvVar.getName()).as("Unexpected env var name").isEqualTo("MY_ENV"); ConfigMapKeySelector configMapKeySelector = configMapKeyRefEnvVar.getValueFrom().getConfigMapKeyRef(); assertThat(configMapKeySelector.getName()).as("Unexpected config map name").isEqualTo("myConfigMap"); assertThat(configMapKeySelector.getKey()).as("Unexpected config map data key").isEqualTo("envName"); // unique deploy prop configMapKeyRefEnvVar = envVars.get(1); assertThat(configMapKeyRefEnvVar.getName()).as("Unexpected env var name").isEqualTo("ENV_VALUES"); configMapKeySelector = configMapKeyRefEnvVar.getValueFrom().getConfigMapKeyRef(); assertThat(configMapKeySelector.getName()).as("Unexpected config map name").isEqualTo("myOtherConfigMap"); assertThat(configMapKeySelector.getKey()).as("Unexpected config map data key").isEqualTo("diskType"); // unique, non-overridden global prop configMapKeyRefEnvVar = envVars.get(2); assertThat(configMapKeyRefEnvVar.getName()).as("Unexpected env var name").isEqualTo("MY_VALS_GLOBAL"); configMapKeySelector = configMapKeyRefEnvVar.getValueFrom().getConfigMapKeyRef(); assertThat(configMapKeySelector.getName()).as("Unexpected config map name").isEqualTo("myValsGlobal"); assertThat(configMapKeySelector.getKey()).as("Unexpected config map data key").isEqualTo("valsGlobal"); } @Test public void testConfigMapKeyRefGlobalFromYaml() throws Exception { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), null); deployer = k8sAppDeployer(); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); List envVars = podSpec.getContainers().get(0).getEnv(); assertThat(envVars.size()).as("Invalid number of env vars").isEqualTo(3); EnvVar configMapKeyRefEnvVar = envVars.get(1); assertThat(configMapKeyRefEnvVar.getName()).as("Unexpected env var name").isEqualTo("MY_ENV"); ConfigMapKeySelector configMapKeySelector = configMapKeyRefEnvVar.getValueFrom().getConfigMapKeyRef(); assertThat(configMapKeySelector.getName()).as("Unexpected config map name").isEqualTo("myConfigMap"); assertThat(configMapKeySelector.getKey()).as("Unexpected config map data key").isEqualTo("envName"); } @Test public void testNodeAffinityProperty() { Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.affinity.nodeAffinity", "{ requiredDuringSchedulingIgnoredDuringExecution:" + " { nodeSelectorTerms:" + " [ { matchExpressions:" + " [ { key: 'kubernetes.io/e2e-az-name', " + " operator: 'In'," + " values:" + " [ 'e2e-az1', 'e2e-az2']}]}]}, " + " preferredDuringSchedulingIgnoredDuringExecution:" + " [ { weight: 1," + " preference:" + " { matchExpressions:" + " [ { key: 'another-node-label-key'," + " operator: 'In'," + " values:" + " [ 'another-node-label-value' ]}]}}]}"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(new KubernetesDeployerProperties()); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); NodeAffinity nodeAffinity = podSpec.getAffinity().getNodeAffinity(); assertThat(nodeAffinity).as("Node affinity should not be null").isNotNull(); assertThat(nodeAffinity.getRequiredDuringSchedulingIgnoredDuringExecution()).as("RequiredDuringSchedulingIgnoredDuringExecution should not be null").isNotNull(); assertThat(nodeAffinity.getPreferredDuringSchedulingIgnoredDuringExecution().size()).as("PreferredDuringSchedulingIgnoredDuringExecution should have one element").isEqualTo(1); } @Test public void testPodAffinityProperty() { Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.affinity.podAffinity", "{ requiredDuringSchedulingIgnoredDuringExecution:" + " { labelSelector:" + " [ { matchExpressions:" + " [ { key: 'app', " + " operator: 'In'," + " values:" + " [ 'store']}]}], " + " topologyKey: 'kubernetes.io/hostname'}, " + " preferredDuringSchedulingIgnoredDuringExecution:" + " [ { weight: 1," + " podAffinityTerm:" + " { labelSelector:" + " { matchExpressions:" + " [ { key: 'security'," + " operator: 'In'," + " values:" + " [ 'S2' ]}]}, " + " topologyKey: 'failure-domain.beta.kubernetes.io/zone'}}]}"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(new KubernetesDeployerProperties()); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); PodAffinity podAffinity = podSpec.getAffinity().getPodAffinity(); assertThat(podAffinity).as("Pod affinity should not be null").isNotNull(); assertThat(podAffinity.getRequiredDuringSchedulingIgnoredDuringExecution()).as("RequiredDuringSchedulingIgnoredDuringExecution should not be null").isNotNull(); assertThat(podAffinity.getPreferredDuringSchedulingIgnoredDuringExecution().size()).as("PreferredDuringSchedulingIgnoredDuringExecution should have one element").isEqualTo(1); } @Test public void testPodAntiAffinityProperty() { Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.affinity.podAntiAffinity", "{ requiredDuringSchedulingIgnoredDuringExecution:" + " { labelSelector:" + " [ { matchExpressions:" + " [ { key: 'app', " + " operator: 'In'," + " values:" + " [ 'store']}]}], " + " topologyKey: 'kubernetes.io/hostname'}, " + " preferredDuringSchedulingIgnoredDuringExecution:" + " [ { weight: 1," + " podAffinityTerm:" + " { labelSelector:" + " { matchExpressions:" + " [ { key: 'security'," + " operator: 'In'," + " values:" + " [ 'S2' ]}]}, " + " topologyKey: 'failure-domain.beta.kubernetes.io/zone'}}]}"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); deployer = k8sAppDeployer(new KubernetesDeployerProperties()); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); PodAntiAffinity podAntiAffinity = podSpec.getAffinity().getPodAntiAffinity(); assertThat(podAntiAffinity).as("Pod anti-affinity should not be null").isNotNull(); assertThat(podAntiAffinity.getRequiredDuringSchedulingIgnoredDuringExecution()).as("RequiredDuringSchedulingIgnoredDuringExecution should not be null").isNotNull(); assertThat(podAntiAffinity.getPreferredDuringSchedulingIgnoredDuringExecution().size()).as("PreferredDuringSchedulingIgnoredDuringExecution should have one element").isEqualTo(1); } @Test public void testNodeAffinityGlobalProperty() { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), null); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); NodeSelectorTerm nodeSelectorTerm = new NodeSelectorTerm(); nodeSelectorTerm.setMatchExpressions(Arrays.asList(new NodeSelectorRequirementBuilder() .withKey("kubernetes.io/e2e-az-name") .withOperator("In") .withValues("e2e-az1", "e2e-az2") .build())); NodeSelectorTerm nodeSelectorTerm2 = new NodeSelectorTerm(); nodeSelectorTerm2.setMatchExpressions(Arrays.asList(new NodeSelectorRequirementBuilder() .withKey("another-node-label-key") .withOperator("In") .withValues("another-node-label-value2") .build())); PreferredSchedulingTerm preferredSchedulingTerm = new PreferredSchedulingTerm(nodeSelectorTerm2, 1); NodeAffinity nodeAffinity = new AffinityBuilder() .withNewNodeAffinity() .withNewRequiredDuringSchedulingIgnoredDuringExecution() .withNodeSelectorTerms(nodeSelectorTerm) .endRequiredDuringSchedulingIgnoredDuringExecution() .withPreferredDuringSchedulingIgnoredDuringExecution(preferredSchedulingTerm) .endNodeAffinity() .buildNodeAffinity(); kubernetesDeployerProperties.setNodeAffinity(nodeAffinity); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); NodeAffinity nodeAffinityTest = podSpec.getAffinity().getNodeAffinity(); assertThat(nodeAffinityTest).as("Node affinity should not be null").isNotNull(); assertThat(nodeAffinityTest.getRequiredDuringSchedulingIgnoredDuringExecution()).as("RequiredDuringSchedulingIgnoredDuringExecution should not be null").isNotNull(); assertThat(nodeAffinityTest.getPreferredDuringSchedulingIgnoredDuringExecution().size()).as("PreferredDuringSchedulingIgnoredDuringExecution should have one element").isEqualTo(1); } @Test public void testPodAffinityGlobalProperty() { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), null); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); LabelSelector labelSelector = new LabelSelector(); labelSelector.setMatchExpressions(Arrays.asList(new LabelSelectorRequirementBuilder() .withKey("security") .withOperator("In") .withValues("S1") .build())); PodAffinityTerm podAffinityTerm = new PodAffinityTerm(labelSelector, null, null, "failure-domain.beta.kubernetes.io/zone"); LabelSelector labelSelector2 = new LabelSelector(); labelSelector2.setMatchExpressions(Arrays.asList(new LabelSelectorRequirementBuilder() .withKey("security") .withOperator("In") .withValues("s2") .build())); PodAffinityTerm podAffinityTerm2 = new PodAffinityTerm(labelSelector2, null, null, "failure-domain.beta.kubernetes.io/zone"); WeightedPodAffinityTerm weightedPodAffinityTerm = new WeightedPodAffinityTerm(podAffinityTerm2, 100); PodAffinity podAffinity = new AffinityBuilder() .withNewPodAffinity() .withRequiredDuringSchedulingIgnoredDuringExecution(podAffinityTerm) .withPreferredDuringSchedulingIgnoredDuringExecution(weightedPodAffinityTerm) .endPodAffinity() .buildPodAffinity(); kubernetesDeployerProperties.setPodAffinity(podAffinity); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); PodAffinity podAffinityTest = podSpec.getAffinity().getPodAffinity(); assertThat(podAffinityTest).as("Pod affinity should not be null").isNotNull(); assertThat(podAffinityTest.getRequiredDuringSchedulingIgnoredDuringExecution()).as("RequiredDuringSchedulingIgnoredDuringExecution should not be null").isNotNull(); assertThat(podAffinityTest.getPreferredDuringSchedulingIgnoredDuringExecution().size()).as("PreferredDuringSchedulingIgnoredDuringExecution should have one element").isEqualTo(1); } @Test public void testPodAntiAffinityGlobalProperty() { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), null); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); kubernetesDeployerProperties.setStartupHttpProbePort(kubernetesDeployerProperties.getLivenessHttpProbePort()); LabelSelector labelSelector = new LabelSelector(); labelSelector.setMatchExpressions(Arrays.asList(new LabelSelectorRequirementBuilder() .withKey("app") .withOperator("In") .withValues("store") .build())); PodAffinityTerm podAffinityTerm = new PodAffinityTerm(labelSelector, null, null, "kubernetes.io/hostname"); LabelSelector labelSelector2 = new LabelSelector(); labelSelector2.setMatchExpressions(Arrays.asList(new LabelSelectorRequirementBuilder() .withKey("security") .withOperator("In") .withValues("s2") .build())); PodAffinityTerm podAffinityTerm2 = new PodAffinityTerm(labelSelector2, null, null, "failure-domain.beta.kubernetes.io/zone"); WeightedPodAffinityTerm weightedPodAffinityTerm = new WeightedPodAffinityTerm(podAffinityTerm2, 100); PodAntiAffinity podAntiAffinity = new AffinityBuilder() .withNewPodAntiAffinity() .withRequiredDuringSchedulingIgnoredDuringExecution(podAffinityTerm) .withPreferredDuringSchedulingIgnoredDuringExecution(weightedPodAffinityTerm) .endPodAntiAffinity() .buildPodAntiAffinity(); kubernetesDeployerProperties.setPodAntiAffinity(podAntiAffinity); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); PodAntiAffinity podAntiAffinityTest = podSpec.getAffinity().getPodAntiAffinity(); assertThat(podAntiAffinityTest).as("Pod anti-affinity should not be null").isNotNull(); assertThat(podAntiAffinityTest.getRequiredDuringSchedulingIgnoredDuringExecution()).as("RequiredDuringSchedulingIgnoredDuringExecution should not be null").isNotNull(); assertThat(podAntiAffinityTest.getPreferredDuringSchedulingIgnoredDuringExecution().size()).as("PreferredDuringSchedulingIgnoredDuringExecution should have one element").isEqualTo(1); } @Test public void testNodeAffinityFromYaml() throws Exception { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), null); deployer = k8sAppDeployer(); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); NodeAffinity nodeAffinity = podSpec.getAffinity().getNodeAffinity(); assertThat(nodeAffinity).as("Node affinity should not be null").isNotNull(); assertThat(nodeAffinity.getRequiredDuringSchedulingIgnoredDuringExecution()).as("RequiredDuringSchedulingIgnoredDuringExecution should not be null").isNotNull(); assertThat(nodeAffinity.getPreferredDuringSchedulingIgnoredDuringExecution().size()).as("PreferredDuringSchedulingIgnoredDuringExecution should have one element").isEqualTo(1); } @Test public void testPodAffinityFromYaml() throws Exception { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), null); deployer = k8sAppDeployer(); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); PodAffinity podAffinity = podSpec.getAffinity().getPodAffinity(); assertThat(podAffinity).as("Pod affinity should not be null").isNotNull(); assertThat(podAffinity.getRequiredDuringSchedulingIgnoredDuringExecution()).as("RequiredDuringSchedulingIgnoredDuringExecution should not be null").isNotNull(); assertThat(podAffinity.getPreferredDuringSchedulingIgnoredDuringExecution().size()).as("PreferredDuringSchedulingIgnoredDuringExecution should have one element").isEqualTo(1); } @Test public void testPodAntiAffinityFromYaml() throws Exception { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), null); deployer = k8sAppDeployer(); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); PodAntiAffinity podAntiAffinity = podSpec.getAffinity().getPodAntiAffinity(); assertThat(podAntiAffinity).as("Pod anti-affinity should not be null").isNotNull(); assertThat(podAntiAffinity.getRequiredDuringSchedulingIgnoredDuringExecution()).as("RequiredDuringSchedulingIgnoredDuringExecution should not be null").isNotNull(); assertThat(podAntiAffinity.getPreferredDuringSchedulingIgnoredDuringExecution().size()).as("PreferredDuringSchedulingIgnoredDuringExecution should have one element").isEqualTo(1); } @Test public void testNodeAffinityPropertyOverrideGlobal() { Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.affinity.nodeAffinity", "{ requiredDuringSchedulingIgnoredDuringExecution:" + " { nodeSelectorTerms:" + " [ { matchExpressions:" + " [ { key: 'kubernetes.io/e2e-az-name', " + " operator: 'In'," + " values:" + " [ 'e2e-az1', 'e2e-az2']}]}]}, " + " preferredDuringSchedulingIgnoredDuringExecution:" + " [ { weight: 1," + " preference:" + " { matchExpressions:" + " [ { key: 'another-node-label-key'," + " operator: 'In'," + " values:" + " [ 'another-node-label-value' ]}]}}]}"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); NodeSelectorTerm nodeSelectorTerm = new NodeSelectorTerm(); nodeSelectorTerm.setMatchExpressions(Arrays.asList(new NodeSelectorRequirementBuilder() .withKey("kubernetes.io/e2e-az-name") .withOperator("In") .withValues("e2e-az1", "e2e-az2") .build())); NodeAffinity nodeAffinity = new AffinityBuilder() .withNewNodeAffinity() .withNewRequiredDuringSchedulingIgnoredDuringExecution() .withNodeSelectorTerms(nodeSelectorTerm) .endRequiredDuringSchedulingIgnoredDuringExecution() .endNodeAffinity() .buildNodeAffinity(); kubernetesDeployerProperties.setNodeAffinity(nodeAffinity); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); NodeAffinity nodeAffinityTest = podSpec.getAffinity().getNodeAffinity(); assertThat(nodeAffinityTest).as("Node affinity should not be null").isNotNull(); assertThat(nodeAffinityTest.getRequiredDuringSchedulingIgnoredDuringExecution()).as("RequiredDuringSchedulingIgnoredDuringExecution should not be null").isNotNull(); assertThat(nodeAffinityTest.getPreferredDuringSchedulingIgnoredDuringExecution().size()).as("PreferredDuringSchedulingIgnoredDuringExecution should have one element").isEqualTo(1); } @Test public void testPodAffinityPropertyOverrideGlobal() { Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.affinity.podAffinity", "{ requiredDuringSchedulingIgnoredDuringExecution:" + " { labelSelector:" + " [ { matchExpressions:" + " [ { key: 'security', " + " operator: 'In'," + " values:" + " [ 'S1']}]}], " + " topologyKey: 'failure-domain.beta.kubernetes.io/zone'}, " + " preferredDuringSchedulingIgnoredDuringExecution:" + " [ { weight: 1," + " podAffinityTerm:" + " { labelSelector:" + " { matchExpressions:" + " [ { key: 'security'," + " operator: 'In'," + " values:" + " [ 'S2' ]}]}, " + " topologyKey: 'failure-domain.beta.kubernetes.io/zone'}}]}"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); LabelSelector labelSelector = new LabelSelector(); labelSelector.setMatchExpressions(Arrays.asList(new LabelSelectorRequirementBuilder() .withKey("tolerance") .withOperator("In") .withValues("Reliable") .build())); PodAffinityTerm podAffinityTerm = new PodAffinityTerm(labelSelector, null, null, "failure-domain.beta.kubernetes.io/zone"); PodAffinity podAffinity = new AffinityBuilder() .withNewPodAffinity() .withRequiredDuringSchedulingIgnoredDuringExecution(podAffinityTerm) .endPodAffinity() .buildPodAffinity(); kubernetesDeployerProperties.setPodAffinity(podAffinity); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); PodAffinity podAffinityTest = podSpec.getAffinity().getPodAffinity(); assertThat(podAffinityTest).as("Pod affinity should not be null").isNotNull(); assertThat(podAffinityTest.getRequiredDuringSchedulingIgnoredDuringExecution()).as("RequiredDuringSchedulingIgnoredDuringExecution should not be null").isNotNull(); assertThat(podAffinityTest.getPreferredDuringSchedulingIgnoredDuringExecution().size()).as("PreferredDuringSchedulingIgnoredDuringExecution should have one element").isEqualTo(1); } @Test public void testPodAntiAffinityPropertyOverrideGlobal() { Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.affinity.podAntiAffinity", "{ requiredDuringSchedulingIgnoredDuringExecution:" + " { labelSelector:" + " [ { matchExpressions:" + " [ { key: 'app', " + " operator: 'In'," + " values:" + " [ 'store']}]}], " + " topologyKey: 'kubernetes.io/hostnam'}, " + " preferredDuringSchedulingIgnoredDuringExecution:" + " [ { weight: 1," + " podAffinityTerm:" + " { labelSelector:" + " { matchExpressions:" + " [ { key: 'security'," + " operator: 'In'," + " values:" + " [ 'S2' ]}]}, " + " topologyKey: 'failure-domain.beta.kubernetes.io/zone'}}]}"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); LabelSelector labelSelector = new LabelSelector(); labelSelector.setMatchExpressions(Arrays.asList(new LabelSelectorRequirementBuilder() .withKey("version") .withOperator("Equals") .withValues("v1") .build())); PodAffinityTerm podAffinityTerm = new PodAffinityTerm(labelSelector, null, null, "kubernetes.io/hostnam"); PodAntiAffinity podAntiAffinity = new AffinityBuilder() .withNewPodAntiAffinity() .withRequiredDuringSchedulingIgnoredDuringExecution(podAffinityTerm) .endPodAntiAffinity() .buildPodAntiAffinity(); kubernetesDeployerProperties.setPodAntiAffinity(podAntiAffinity); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); PodAntiAffinity podAntiAffinityTest = podSpec.getAffinity().getPodAntiAffinity(); assertThat(podAntiAffinityTest).as("Pod anti-affinity should not be null").isNotNull(); assertThat(podAntiAffinityTest.getRequiredDuringSchedulingIgnoredDuringExecution()).as("RequiredDuringSchedulingIgnoredDuringExecution should not be null").isNotNull(); assertThat(podAntiAffinityTest.getPreferredDuringSchedulingIgnoredDuringExecution().size()).as("PreferredDuringSchedulingIgnoredDuringExecution should have one element").isEqualTo(1); } @Nested @DisplayName("creates pod spec with pod security context") class CreatePodSpecWithPodSecurityContext { @Test @DisplayName("created from deployment property with all fields") void createdFromDeploymentPropertyWithAllFields() { KubernetesDeployerProperties globalDeployerProps = new KubernetesDeployerProperties(); Map deploymentProps = new HashMap<>(); deploymentProps.put("spring.cloud.deployer.kubernetes.podSecurityContext", "{" + " fsGroup: 65534" + ", fsGroupChangePolicy: Always" + ", runAsUser: 65534" + ", runAsGroup: 65534" + ", runAsNonRoot: true" + ", seLinuxOptions: { level: \"s0:c123,c456\" }" + ", seccompProfile: { type: Localhost, localhostProfile: my-profiles/profile-allow.json }" + ", supplementalGroups: [65534, 65535]" + ", sysctls: [{name: \"kernel.shm_rmid_forced\", value: 0}, {name: \"net.core.somaxconn\", value: 1024}]" + ", windowsOptions: { gmsaCredentialSpec: \"specA\", gmsaCredentialSpecName: \"specA-name\", hostProcess: true, runAsUserName: \"userA\" }" + "}"); PodSecurityContext expectedPodSecurityContext = new PodSecurityContextBuilder() .withFsGroup(65534L) .withFsGroupChangePolicy("Always") .withRunAsUser(65534L) .withRunAsGroup(65534L) .withRunAsNonRoot(true) .withSeLinuxOptions(new SELinuxOptions("s0:c123,c456", null, null, null)) .withSeccompProfile(new SeccompProfile("my-profiles/profile-allow.json", "Localhost")) .withSupplementalGroups(65534L, 65535L) .withSysctls(new Sysctl("kernel.shm_rmid_forced", "0"), new Sysctl("net.core.somaxconn", "1024")) .withWindowsOptions(new WindowsSecurityContextOptions("specA", "specA-name", true, "userA")) .build(); assertThatDeployerCreatesPodSpecWithPodSecurityContext(globalDeployerProps, deploymentProps, expectedPodSecurityContext); } @Test @DisplayName("created from deployment property with runAsUser only") void createdFromDeploymentPropertyWithRunAsUserOnly() { KubernetesDeployerProperties globalDeployerProps = new KubernetesDeployerProperties(); Map deploymentProps = new HashMap<>(); deploymentProps.put("spring.cloud.deployer.kubernetes.podSecurityContext", "{runAsUser: 65534}"); PodSecurityContext expectedPodSecurityContext = new PodSecurityContextBuilder() .withRunAsUser(65534L) .build(); assertThatDeployerCreatesPodSpecWithPodSecurityContext(globalDeployerProps, deploymentProps, expectedPodSecurityContext); } @Test @DisplayName("created from deployment property with fsGroup only") void createdFromDeploymentPropertyWithFsGroupOnly() { KubernetesDeployerProperties globalDeployerProps = new KubernetesDeployerProperties(); Map deploymentProps = new HashMap<>(); deploymentProps.put("spring.cloud.deployer.kubernetes.podSecurityContext", "{fsGroup: 65534}"); PodSecurityContext expectedPodSecurityContext = new PodSecurityContextBuilder() .withFsGroup(65534L) .build(); assertThatDeployerCreatesPodSpecWithPodSecurityContext(globalDeployerProps, deploymentProps, expectedPodSecurityContext); } @Test @DisplayName("created from deployment property with supplementalGroups only") void createdFromDeploymentPropertyWithSupplementalGroupsOnly() { KubernetesDeployerProperties globalDeployerProps = new KubernetesDeployerProperties(); Map deploymentProps = new HashMap<>(); deploymentProps.put("spring.cloud.deployer.kubernetes.podSecurityContext", "{supplementalGroups: [65534,65535]}"); PodSecurityContext expectedPodSecurityContext = new PodSecurityContextBuilder() .withSupplementalGroups(65534L, 65535L) .build(); assertThatDeployerCreatesPodSpecWithPodSecurityContext(globalDeployerProps, deploymentProps, expectedPodSecurityContext); } @Test @DisplayName("created from deployment property with seccompProfile only") void createdFromDeploymentPropertyWithSeccompProfileOnly() { KubernetesDeployerProperties globalDeployerProps = new KubernetesDeployerProperties(); Map deploymentProps = new HashMap<>(); deploymentProps.put("spring.cloud.deployer.kubernetes.podSecurityContext", "{seccompProfile: { type: RuntimeDefault}}"); PodSecurityContext expectedPodSecurityContext = new PodSecurityContextBuilder() .withSeccompProfile(new SeccompProfile(null, "RuntimeDefault")) .build(); assertThatDeployerCreatesPodSpecWithPodSecurityContext(globalDeployerProps, deploymentProps, expectedPodSecurityContext); } @Test @DisplayName("created from global deployer property sourced from yaml") void createdFromGlobalDeployerPropertySourcedFromYaml() throws Exception { KubernetesDeployerProperties globalDeployerProps = bindDeployerProperties(); Map deploymentProps = new HashMap<>(); PodSecurityContext expectedPodSecurityContext = new PodSecurityContextBuilder() .withFsGroup(65534L) .withFsGroupChangePolicy("Always") .withRunAsUser(65534L) .withRunAsGroup(65534L) .withRunAsNonRoot(true) .withSeLinuxOptions(new SELinuxOptions("s0:c123,c456", null, null, null)) .withSeccompProfile(new SeccompProfile("my-profiles/profile-allow.json", "Localhost")) .withSupplementalGroups(65534L, 65535L) .withSysctls(new Sysctl("kernel.shm_rmid_forced", "0"), new Sysctl("net.core.somaxconn", "1024")) .withWindowsOptions(new WindowsSecurityContextOptions("specA", "specA-name", true, "userA")) .build(); assertThatDeployerCreatesPodSpecWithPodSecurityContext(globalDeployerProps, deploymentProps, expectedPodSecurityContext); } @Test @DisplayName("created from global deployer property") void createdFromGlobalDeployerProperty() { KubernetesDeployerProperties globalDeployerProps = new KubernetesDeployerProperties(); KubernetesDeployerProperties.PodSecurityContext securityContext = new KubernetesDeployerProperties.PodSecurityContext(); securityContext.setFsGroup(65534L); securityContext.setRunAsUser(65534L); securityContext.setSupplementalGroups(new Long[]{65534L}); KubernetesDeployerProperties.SeccompProfile seccompProfile = new KubernetesDeployerProperties.SeccompProfile(); seccompProfile.setType("Localhost"); seccompProfile.setLocalhostProfile("profile.json"); securityContext.setSeccompProfile(seccompProfile); globalDeployerProps.setPodSecurityContext(securityContext); Map deploymentProps = Collections.emptyMap(); PodSecurityContext expectedPodSecurityContext = new PodSecurityContextBuilder() .withRunAsUser(65534L) .withFsGroup(65534L) .withSupplementalGroups(65534L) .withSeccompProfile(new SeccompProfile("profile.json", "Localhost")) .build(); assertThatDeployerCreatesPodSpecWithPodSecurityContext(globalDeployerProps, deploymentProps, expectedPodSecurityContext); } @Test @DisplayName("created from deployment property overrriding global deployer property") void createdFromDeploymentPropertyOverridingGlobalDeployerProperty() { KubernetesDeployerProperties globalDeployerProps = new KubernetesDeployerProperties(); KubernetesDeployerProperties.PodSecurityContext securityContext = new KubernetesDeployerProperties.PodSecurityContext(); securityContext.setFsGroup(1000L); securityContext.setRunAsUser(1000L); securityContext.setSupplementalGroups(new Long[]{1000L}); KubernetesDeployerProperties.SeccompProfile seccompProfile = new KubernetesDeployerProperties.SeccompProfile(); seccompProfile.setType("Localhost"); seccompProfile.setLocalhostProfile("sec/default-allow.json"); securityContext.setSeccompProfile(seccompProfile); globalDeployerProps.setPodSecurityContext(securityContext); Map deploymentProps = new HashMap<>(); deploymentProps.put("spring.cloud.deployer.kubernetes.podSecurityContext", "{runAsUser: 65534, fsGroup: 65534, supplementalGroups: [65534,65535], seccompProfile: { type: Localhost, localhostProfile: sec/custom-allow.json } }"); PodSecurityContext expectedPodSecurityContext = new PodSecurityContextBuilder() .withRunAsUser(65534L) .withFsGroup(65534L) .withSupplementalGroups(65534L, 65535L) .withSeccompProfile(new SeccompProfile("sec/custom-allow.json", "Localhost")) .build(); assertThatDeployerCreatesPodSpecWithPodSecurityContext(globalDeployerProps, deploymentProps, expectedPodSecurityContext); } private void assertThatDeployerCreatesPodSpecWithPodSecurityContext( KubernetesDeployerProperties globalDeployerProps, Map deploymentProps, PodSecurityContext expectedPodSecurityContext ) { PodSpec podSpec = deployerCreatesPodSpec(globalDeployerProps, deploymentProps); PodSecurityContext actualPodSecurityContext = podSpec.getSecurityContext(); assertThat(actualPodSecurityContext) .isNotNull() .isEqualTo(expectedPodSecurityContext); } } @Nested @DisplayName("creates pod spec with container security context") class CreatePodSpecWithContainerSecurityContext { @Test @DisplayName("created from deployment property with all fields") void createdFromDeploymentPropertyWithAllFields() { KubernetesDeployerProperties globalDeployerProps = new KubernetesDeployerProperties(); Map deploymentProps = new HashMap<>(); deploymentProps.put("spring.cloud.deployer.kubernetes.containerSecurityContext", "{" + " allowPrivilegeEscalation: true" + ", capabilities: { add: [ \"a\", \"b\" ], drop: [ \"c\" ] }" + ", privileged: true" + ", procMount: DefaultProcMount" + ", readOnlyRootFilesystem: true" + ", runAsUser: 65534" + ", runAsGroup: 65534" + ", runAsNonRoot: true" + ", seLinuxOptions: { level: \"s0:c123,c456\" }" + ", seccompProfile: { type: Localhost, localhostProfile: my-profiles/profile-allow.json }" + ", windowsOptions: { gmsaCredentialSpec: \"specA\", gmsaCredentialSpecName: \"specA-name\", hostProcess: true, runAsUserName: \"userA\" }" + "}"); SecurityContext expectedContainerSecurityContext = new SecurityContextBuilder() .withAllowPrivilegeEscalation(true) .withCapabilities(new Capabilities(Arrays.asList("a", "b"), Arrays.asList("c"))) .withPrivileged(true) .withProcMount("DefaultProcMount") .withReadOnlyRootFilesystem(true) .withRunAsUser(65534L) .withRunAsGroup(65534L) .withRunAsNonRoot(true) .withSeLinuxOptions(new SELinuxOptions("s0:c123,c456", null, null, null)) .withSeccompProfile(new SeccompProfile("my-profiles/profile-allow.json", "Localhost")) .withWindowsOptions(new WindowsSecurityContextOptions("specA", "specA-name", true, "userA")) .build(); assertThatDeployerCreatesPodSpecWithContainerSecurityContext(globalDeployerProps, deploymentProps, expectedContainerSecurityContext); } @Test @DisplayName("created from deployment property with allowPrivilegeEscalation only") void createdFromDeploymentPropertyWithAllowPrivilegeEscalationOnly() { KubernetesDeployerProperties globalDeployerProps = new KubernetesDeployerProperties(); Map deploymentProps = Collections.singletonMap("spring.cloud.deployer.kubernetes.containerSecurityContext", "{allowPrivilegeEscalation: true}"); SecurityContext expectedContainerSecurityContext = new SecurityContextBuilder() .withAllowPrivilegeEscalation(true) .build(); assertThatDeployerCreatesPodSpecWithContainerSecurityContext(globalDeployerProps, deploymentProps, expectedContainerSecurityContext); } @Test @DisplayName("created from deployment property with readOnlyRootFilesystem only") void createdFromDeploymentPropertyWithReadOnlyRootFilesystemOnly() { KubernetesDeployerProperties globalDeployerProps = new KubernetesDeployerProperties(); Map deploymentProps = Collections.singletonMap("spring.cloud.deployer.kubernetes.containerSecurityContext", "{readOnlyRootFilesystem: true}"); SecurityContext expectedContainerSecurityContext = new SecurityContextBuilder() .withReadOnlyRootFilesystem(true) .build(); assertThatDeployerCreatesPodSpecWithContainerSecurityContext(globalDeployerProps, deploymentProps, expectedContainerSecurityContext); } @Test @DisplayName("created from global deployer property sourced from yaml") void createdFromGlobalDeployerPropertySourcedFromYaml() throws Exception { KubernetesDeployerProperties globalDeployerProps = bindDeployerProperties(); Map deploymentProps = Collections.emptyMap(); SecurityContext expectedContainerSecurityContext = new SecurityContextBuilder() .withAllowPrivilegeEscalation(true) .withCapabilities(new Capabilities(Arrays.asList("a", "b"), Arrays.asList("c"))) .withPrivileged(true) .withProcMount("DefaultProcMount") .withReadOnlyRootFilesystem(true) .withRunAsUser(65534L) .withRunAsGroup(65534L) .withRunAsNonRoot(true) .withSeLinuxOptions(new SELinuxOptions("s0:c123,c456", null, null, null)) .withSeccompProfile(new SeccompProfile("my-profiles/profile-allow.json", "Localhost")) .withWindowsOptions(new WindowsSecurityContextOptions("specA", "specA-name", true, "userA")) .build(); assertThatDeployerCreatesPodSpecWithContainerSecurityContext(globalDeployerProps, deploymentProps, expectedContainerSecurityContext); } @Test @DisplayName("created from global deployer property") void createdFromGlobalDeployerProperty() { KubernetesDeployerProperties globalDeployerProps = new KubernetesDeployerProperties(); KubernetesDeployerProperties.ContainerSecurityContext securityContext = new KubernetesDeployerProperties.ContainerSecurityContext(); securityContext.setAllowPrivilegeEscalation(false); securityContext.setReadOnlyRootFilesystem(true); globalDeployerProps.setContainerSecurityContext(securityContext); Map deploymentProps = Collections.emptyMap(); SecurityContext expectedContainerSecurityContext = new SecurityContextBuilder() .withAllowPrivilegeEscalation(false) .withReadOnlyRootFilesystem(true) .build(); assertThatDeployerCreatesPodSpecWithContainerSecurityContext(globalDeployerProps, deploymentProps, expectedContainerSecurityContext); } @Test @DisplayName("created from deployment property overrriding global deployer property") void createdFromDeploymentPropertyOverridingGlobalDeployerProperty() { KubernetesDeployerProperties globalDeployerProps = new KubernetesDeployerProperties(); KubernetesDeployerProperties.ContainerSecurityContext securityContext = new KubernetesDeployerProperties.ContainerSecurityContext(); securityContext.setAllowPrivilegeEscalation(true); securityContext.setReadOnlyRootFilesystem(false); globalDeployerProps.setContainerSecurityContext(securityContext); Map deploymentProps = Collections.singletonMap("spring.cloud.deployer.kubernetes.containerSecurityContext", "{allowPrivilegeEscalation: false, readOnlyRootFilesystem: true}"); SecurityContext expectedContainerSecurityContext = new SecurityContextBuilder() .withAllowPrivilegeEscalation(false) .withReadOnlyRootFilesystem(true) .build(); assertThatDeployerCreatesPodSpecWithContainerSecurityContext(globalDeployerProps, deploymentProps, expectedContainerSecurityContext); } private void assertThatDeployerCreatesPodSpecWithContainerSecurityContext( KubernetesDeployerProperties globalDeployerProps, Map deploymentProps, SecurityContext expectedContainerSecurityContext ) { PodSpec podSpec = deployerCreatesPodSpec(globalDeployerProps, deploymentProps); assertThat(podSpec.getContainers()) .singleElement() .extracting(Container::getSecurityContext) .isEqualTo(expectedContainerSecurityContext); } } private PodSpec deployerCreatesPodSpec(KubernetesDeployerProperties globalDeployerProperties, Map deploymentProperties) { AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), deploymentProperties); KubernetesAppDeployer deployer = k8sAppDeployer(globalDeployerProperties); return deployer.createPodSpec(appDeploymentRequest); } @Test public void testWithLifecyclePostStart() { Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.lifecycle.postStart.exec.command", "/bin/sh,-c,echo Hello from the postStart handler > /usr/share/message"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getContainers().get(0).getLifecycle().getPostStart().getExec().getCommand()) .containsExactlyInAnyOrder("/bin/sh", "-c", "echo Hello from the postStart handler > /usr/share/message"); } @Test public void testWithLifecyclePreStop() { Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.lifecycle.preStop.exec.command", "/bin/sh,-c,nginx -s quit; while killall -0 nginx; do sleep 1; done"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getContainers().get(0).getLifecycle().getPreStop().getExec().getCommand()) .containsExactlyInAnyOrder( "/bin/sh", "-c", "nginx -s quit; while killall -0 nginx; do sleep 1; done"); } @Test public void testLifecyclePostStartOverridesGlobalPostStart() { Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.lifecycle.postStart.exec.command", "/bin/sh,-c,nginx -s quit; while killall -0 nginx; do sleep 1; done"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); KubernetesDeployerProperties.Lifecycle lifecycle = new KubernetesDeployerProperties.Lifecycle(); lifecycle.setPostStart(new KubernetesDeployerProperties.Lifecycle.Hook() { @Override KubernetesDeployerProperties.Lifecycle.Exec getExec() { return new KubernetesDeployerProperties.Lifecycle.Exec() { @Override List getCommand() { return Arrays.asList("echo", "postStart"); } }; } }); lifecycle.setPreStop(new KubernetesDeployerProperties.Lifecycle.Hook() { @Override KubernetesDeployerProperties.Lifecycle.Exec getExec() { return new KubernetesDeployerProperties.Lifecycle.Exec() { @Override List getCommand() { return Arrays.asList("echo", "preStop"); } }; } }); kubernetesDeployerProperties.setLifecycle(lifecycle); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getContainers().get(0).getLifecycle().getPostStart().getExec().getCommand()) .containsExactlyInAnyOrder( "/bin/sh", "-c", "nginx -s quit; while killall -0 nginx; do sleep 1; done"); assertThat(podSpec.getContainers().get(0).getLifecycle().getPreStop().getExec().getCommand()) .containsExactlyInAnyOrder("echo", "preStop"); } @Test public void testLifecyclePrestopOverridesGlobalPrestop() { Map props = new HashMap<>(); props.put("spring.cloud.deployer.kubernetes.lifecycle.preStop.exec.command", "/bin/sh,-c,nginx -s quit; while killall -0 nginx; do sleep 1; done"); AppDefinition definition = new AppDefinition("app-test", null); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, getResource(), props); KubernetesDeployerProperties kubernetesDeployerProperties = new KubernetesDeployerProperties(); KubernetesDeployerProperties.Lifecycle lifecycle = new KubernetesDeployerProperties.Lifecycle(); lifecycle.setPostStart(new KubernetesDeployerProperties.Lifecycle.Hook() { @Override KubernetesDeployerProperties.Lifecycle.Exec getExec() { return new KubernetesDeployerProperties.Lifecycle.Exec() { @Override List getCommand() { return Arrays.asList("echo", "postStart"); } }; } }); lifecycle.setPreStop(new KubernetesDeployerProperties.Lifecycle.Hook() { @Override KubernetesDeployerProperties.Lifecycle.Exec getExec() { return new KubernetesDeployerProperties.Lifecycle.Exec() { @Override List getCommand() { return Arrays.asList("echo", "preStop"); } }; } }); kubernetesDeployerProperties.setLifecycle(lifecycle); deployer = k8sAppDeployer(kubernetesDeployerProperties); PodSpec podSpec = deployer.createPodSpec(appDeploymentRequest); assertThat(podSpec.getContainers().get(0).getLifecycle().getPreStop().getExec().getCommand()) .containsExactlyInAnyOrder( "/bin/sh", "-c", "nginx -s quit; while killall -0 nginx; do sleep 1; done"); assertThat(podSpec.getContainers().get(0).getLifecycle().getPostStart().getExec().getCommand()) .containsExactlyInAnyOrder("echo", "postStart"); } private Resource getResource() { return new DockerResource("springcloud/spring-cloud-deployer-spi-test-app:latest"); } private KubernetesDeployerProperties bindDeployerProperties() throws Exception { YamlPropertiesFactoryBean properties = new YamlPropertiesFactoryBean(); properties.setResources(new ClassPathResource("dataflow-server.yml"), new ClassPathResource("dataflow-server-tolerations.yml"), new ClassPathResource("dataflow-server-secretKeyRef.yml"), new ClassPathResource("dataflow-server-configMapKeyRef.yml"), new ClassPathResource("dataflow-server-podsecuritycontext.yml"), new ClassPathResource("dataflow-server-containerSecurityContext.yml"), new ClassPathResource("dataflow-server-nodeAffinity.yml"), new ClassPathResource("dataflow-server-podAffinity.yml"), new ClassPathResource("dataflow-server-podAntiAffinity.yml")); Properties yaml = properties.getObject(); MapConfigurationPropertySource source = new MapConfigurationPropertySource(yaml); return new Binder(source).bind("", Bindable.of(KubernetesDeployerProperties.class)).get(); } protected KubernetesAppDeployer k8sAppDeployer() throws Exception { return k8sAppDeployer(bindDeployerProperties()); } protected KubernetesAppDeployer k8sAppDeployer(KubernetesDeployerProperties kubernetesDeployerProperties) { return new KubernetesAppDeployer(kubernetesDeployerProperties, null); } } ================================================ FILE: src/test/java/org/springframework/cloud/deployer/spi/kubernetes/KubernetesTaskLauncherIntegrationIT.java ================================================ /* * Copyright 2016-2021 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.cloud.deployer.spi.kubernetes; import java.time.Duration; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; import io.fabric8.kubernetes.api.model.Container; import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.client.KubernetesClient; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.deployer.resource.docker.DockerResource; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.task.LaunchState; import org.springframework.cloud.deployer.spi.task.TaskLauncher; import org.springframework.cloud.deployer.spi.test.AbstractTaskLauncherIntegrationJUnit5Tests; import org.springframework.cloud.deployer.spi.test.Timeout; import org.springframework.core.io.Resource; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; import static org.awaitility.Awaitility.await; /** * Integration tests for {@link KubernetesTaskLauncher}. * * @author Thomas Risberg * @author Chris Schaefer */ @SpringBootTest(classes = {KubernetesAutoConfiguration.class}, properties = { "spring.cloud.deployer.kubernetes.namespace=default" }) public class KubernetesTaskLauncherIntegrationIT extends AbstractTaskLauncherIntegrationJUnit5Tests { @Autowired private TaskLauncher taskLauncher; @Autowired private KubernetesClient kubernetesClient; @Override protected TaskLauncher provideTaskLauncher() { return taskLauncher; } @Test @Override public void testSimpleCancel() throws InterruptedException { super.testSimpleCancel(); } @Override protected String randomName() { return "task-" + UUID.randomUUID().toString().substring(0, 18); } @Override protected Resource testApplication() { return new DockerResource("springcloud/spring-cloud-deployer-spi-test-app:latest"); } @Override protected Timeout deploymentTimeout() { return new Timeout(20, 5000); } @Test public void testJobPodAnnotation() { logger.info("Testing {}...", "JobPodAnnotation"); KubernetesTaskLauncher kubernetesTaskLauncher = new KubernetesTaskLauncher(new KubernetesDeployerProperties(), new KubernetesTaskLauncherProperties(), kubernetesClient); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, Collections.singletonMap("spring.cloud.deployer.kubernetes.jobAnnotations", "key1:val1,key2:val2,key3:val31:val32")); logger.info("Launching {}...", request.getDefinition().getName()); String launchId = kubernetesTaskLauncher.launch(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(taskLauncher().status(launchId).getState()).isEqualTo(LaunchState.running); }); String taskName = request.getDefinition().getName(); logger.info("Checking job pod spec annotations of {}...", taskName); List pods = kubernetesClient.pods().withLabel("task-name", taskName).list().getItems(); assertThat(pods).hasSize(1); Pod pod = pods.get(0); assertThat(pod.getSpec().getContainers().get(0).getPorts()).isEmpty(); Map annotations = pod.getMetadata().getAnnotations(); assertThat(annotations).contains(entry("key1", "val1"), entry("key2", "val2"), entry("key3", "val31:val32")); logger.info("Destroying {}...", taskName); timeout = undeploymentTimeout(); kubernetesTaskLauncher.destroy(taskName); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(taskLauncher().status(launchId).getState()).isEqualTo(LaunchState.unknown); }); } @Test public void testDeploymentLabels() { logger.info("Testing {}...", "deploymentLabels"); KubernetesTaskLauncher kubernetesTaskLauncher = new KubernetesTaskLauncher(new KubernetesDeployerProperties(), new KubernetesTaskLauncherProperties(), kubernetesClient); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, Collections.singletonMap("spring.cloud.deployer.kubernetes.deploymentLabels", "label1:value1,label2:value2")); logger.info("Launching {}...", request.getDefinition().getName()); String launchId = kubernetesTaskLauncher.launch(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(taskLauncher().status(launchId).getState()).isEqualTo(LaunchState.running); }); String taskName = request.getDefinition().getName(); logger.info("Checking job pod spec labels of {}...", taskName); List pods = kubernetesClient.pods().withLabel("task-name", taskName).list().getItems(); assertThat(pods).hasSize(1); Pod pod = pods.get(0); assertThat(pod.getSpec().getContainers().get(0).getPorts()).isEmpty(); Map labels = pod.getMetadata().getLabels(); assertThat(labels).contains(entry("label1", "value1"), entry("label2", "value2")); logger.info("Destroying {}...", taskName); timeout = undeploymentTimeout(); kubernetesTaskLauncher.destroy(taskName); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(taskLauncher().status(launchId).getState()).isEqualTo(LaunchState.unknown); }); } @Test public void testTaskAdditionalContainer() { logger.info("Testing {}...", "TaskAdditionalContainer"); KubernetesTaskLauncher kubernetesTaskLauncher = new KubernetesTaskLauncher(new KubernetesDeployerProperties(), new KubernetesTaskLauncherProperties(), kubernetesClient); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); Map props = Collections.singletonMap("spring.cloud.deployer.kubernetes.additionalContainers", "[{name: 'test', image: 'busybox:latest', command: ['sh', '-c', 'echo hello']}]"); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, props); logger.info("Launching {}...", request.getDefinition().getName()); String launchId = kubernetesTaskLauncher.launch(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(taskLauncher().status(launchId).getState()).isEqualTo(LaunchState.running); }); String taskName = request.getDefinition().getName(); List pods = kubernetesClient.pods().withLabel("task-name", taskName).list().getItems(); assertThat(pods).hasSize(1); Pod pod = pods.get(0); assertThat(pod.getSpec().getContainers()).hasSize(2); Optional additionalContainer = pod.getSpec().getContainers().stream().filter(i -> i.getName().equals("test")).findFirst(); assertThat(additionalContainer.isPresent()).as("Additional container not found").isTrue(); Container testAdditionalContainer = additionalContainer.get(); assertThat(testAdditionalContainer.getName()).as("Unexpected additional container name").isEqualTo("test"); assertThat(testAdditionalContainer.getImage()).as("Unexpected additional container image").isEqualTo("busybox:latest"); List commands = testAdditionalContainer.getCommand(); assertThat(commands).contains("sh", "-c", "echo hello"); logger.info("Destroying {}...", taskName); timeout = undeploymentTimeout(); kubernetesTaskLauncher.destroy(taskName); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(taskLauncher().status(launchId).getState()).isEqualTo(LaunchState.unknown); }); } } ================================================ FILE: src/test/java/org/springframework/cloud/deployer/spi/kubernetes/KubernetesTaskLauncherWithJobIntegrationIT.java ================================================ /* * Copyright 2016-2022 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.cloud.deployer.spi.kubernetes; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Consumer; import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.api.model.batch.v1.Job; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.task.LaunchState; import org.springframework.core.io.Resource; import org.springframework.test.context.TestPropertySource; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.entry; /** * Integration tests for {@link KubernetesTaskLauncher} using jobs instead of bare pods. * *

NOTE: The tests do not call {@code TaskLauncher.destroy/cleanup} in a finally block but instead rely on the * {@link AbstractKubernetesTaskLauncherIntegrationTests#cleanupLingeringApps() AfterEach method} to clean any stray apps. * * @author Leonardo Diniz * @author Chris Schaefer * @author Ilayaperumal Gopinathan * @author Chris Bono * @author Glenn Renfro */ @SpringBootTest(classes = {KubernetesAutoConfiguration.class}) @TestPropertySource(properties = "spring.cloud.deployer.kubernetes.create-job=true") @ExtendWith(OutputCaptureExtension.class) public class KubernetesTaskLauncherWithJobIntegrationIT extends AbstractKubernetesTaskLauncherIntegrationTests { @BeforeEach public void setup() { if (kubernetesClient.getNamespace() == null) { kubernetesClient.getConfiguration().setNamespace("default"); } } @Test void taskLaunchedWithJobAnnotations(TestInfo testInfo) { logTestInfo(testInfo); launchTaskJobAndValidateCreatedJobAndPodWithCleanup( Collections.singletonMap("spring.cloud.deployer.kubernetes.jobAnnotations", "key1:val1,key2:val2,key3:val31:val32"), (job) -> assertThat(job.getMetadata().getAnnotations()).isNotEmpty() .contains(entry("key1", "val1"), entry("key2", "val2"), entry("key3", "val31:val32")), (pod) -> assertThat(pod.getMetadata().getAnnotations()).isNotEmpty() .contains(entry("key1", "val1"), entry("key2", "val2"), entry("key3", "val31:val32"))); } @Test void taskLaunchedWithJobSpecProperties(TestInfo testInfo) { logTestInfo(testInfo); Map deploymentProps = new HashMap<>(); deploymentProps.put("spring.cloud.deployer.kubernetes.restartPolicy", "OnFailure"); deploymentProps.put("spring.cloud.deployer.kubernetes.backoffLimit", "5"); launchTaskJobAndValidateCreatedJobAndPodWithCleanup( deploymentProps, (job) -> assertThat(job.getSpec().getBackoffLimit()).isEqualTo(5), (pod) -> {}); } private void launchTaskJobAndValidateCreatedJobAndPodWithCleanup(Map deploymentProps, Consumer assertingJobConsumer, Consumer assertingPodConsumer) { String taskName = randomName(); AppDefinition definition = new AppDefinition(taskName, null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, deploymentProps); logger.info("Launching {}...", taskName); String launchId = taskLauncher().launch(request); awaitWithPollAndTimeout(deploymentTimeout()) .untilAsserted(() -> assertThat(taskLauncher().status(launchId).getState()).isEqualTo(LaunchState.launching)); logger.info("Checking task Job for {}...", taskName); List jobs = getJobsForTask(taskName); assertThat(jobs).hasSize(1); assertThat(jobs).singleElement().satisfies(assertingJobConsumer); logger.info("Checking task Pod for {}...", taskName); List pods = getPodsForTask(taskName); assertThat(pods).hasSize(1); assertThat(pods).singleElement().satisfies(assertingPodConsumer); logger.info("Destroying {}...", taskName); taskLauncher().destroy(taskName); awaitWithPollAndTimeout(undeploymentTimeout()) .untilAsserted(() -> assertThat(taskLauncher().status(launchId).getState()).isEqualTo(LaunchState.unknown)); } @Test void taskLaunchedWithInvalidRestartPolicyThrowsException(TestInfo testInfo) { logTestInfo(testInfo); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); Map deploymentProps = new HashMap<>(); deploymentProps.put("spring.cloud.deployer.kubernetes.restartPolicy", "Always"); deploymentProps.put("spring.cloud.deployer.kubernetes.backoffLimit", "5"); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, deploymentProps); logger.info("Launching {}...", request.getDefinition().getName()); assertThatThrownBy(() -> taskLauncher.launch(request)) .isInstanceOf(Exception.class) .hasMessage("RestartPolicy should not be 'Always' when the JobSpec is used."); } @Test void cleanupDeletesTaskJob(TestInfo testInfo) { logTestInfo(testInfo); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, null); String taskName = request.getDefinition().getName(); logger.info("Launching {}...", taskName); String launchId = taskLauncher().launch(request); awaitWithPollAndTimeout(deploymentTimeout()) .untilAsserted(() -> assertThat(taskLauncher().status(launchId).getState()).isEqualTo(LaunchState.launching)); List jobs = getJobsForTask(taskName); assertThat(jobs).hasSize(1); logger.info("Cleaning up {}...", taskName); taskLauncher().cleanup(launchId); awaitWithPollAndTimeout(undeploymentTimeout()) .untilAsserted(() -> assertThat(taskLauncher().status(launchId).getState()).isEqualTo(LaunchState.unknown)); jobs = getJobsForTask(taskName); assertThat(jobs).isEmpty(); } @Test void cleanupForNonExistentTaskThrowsException(TestInfo testInfo, CapturedOutput taskOutput) { logTestInfo(testInfo); taskLauncher().cleanup("foo"); assertThat(taskOutput.getAll()).contains("Cannot delete job for task \"foo\" (reason: job does not exist)"); } @Test void deleteJobAfterTtlSecondsOnAfterFinishedExpire(TestInfo testInfo) { logTestInfo(testInfo); Map applicationProps = new HashMap<>(); applicationProps.put("killDelay", "1"); AppDefinition definition = new AppDefinition(randomName(), applicationProps); Resource resource = testApplication(); Map deploymentProps = new HashMap<>(); deploymentProps.put("spring.cloud.deployer.kubernetes.restartPolicy", "Never"); deploymentProps.put("spring.cloud.deployer.kubernetes.backoffLimit", "0"); deploymentProps.put("spring.cloud.deployer.kubernetes.ttlSecondsAfterFinished", "3"); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, deploymentProps); String taskName = request.getDefinition().getName(); logger.info("Launching {}...", taskName); String launchId = taskLauncher().launch(request); awaitWithPollAndTimeout(deploymentTimeout()) .untilAsserted(() -> assertThat(taskLauncher().status(launchId).getState()).isEqualTo(LaunchState.launching)); List jobs = getJobsForTask(taskName); assertThat(jobs).hasSize(1); logger.info("Waiting for deleting the job {}...", taskName); awaitWithPollAndTimeout(undeploymentTimeout()) .untilAsserted(() -> assertThat(taskLauncher().status(launchId).getState()).isEqualTo(LaunchState.unknown)); jobs = getJobsForTask(taskName); assertThat(jobs).isEmpty(); } } ================================================ FILE: src/test/java/org/springframework/cloud/deployer/spi/local/DebugAddressTests.java ================================================ /* * Copyright 2020-2021 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.cloud.deployer.spi.local; import java.util.Optional; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** * @author Christian Tzolov */ public class DebugAddressTests { @Test public void testDebugEmptyConfiguration() { Optional debugAddress = DebugAddress.from(new LocalDeployerProperties(), 0); assertThat(debugAddress.isPresent()).isFalse(); } @Test public void testDebugPort() { LocalDeployerProperties properties = new LocalDeployerProperties(); properties.setDebugPort(20075); Optional debugAddress = DebugAddress.from(properties, 0); assertThat(debugAddress.isPresent()).isTrue(); assertThat(debugAddress.get().getHost()).isNull(); assertThat(debugAddress.get().getPort()).isEqualTo("20075"); assertThat(debugAddress.get().getAddress()).isEqualTo("20075"); } @Test public void testDebugPortWithInstance() { LocalDeployerProperties properties = new LocalDeployerProperties(); properties.setDebugPort(20075); Optional debugAddress = DebugAddress.from(properties, 100); assertThat(debugAddress.isPresent()).isTrue(); assertThat(debugAddress.get().getHost()).isNull(); assertThat(debugAddress.get().getPort()).isEqualTo("20175"); assertThat(debugAddress.get().getAddress()).isEqualTo("20175"); } @Test public void testDebugPortInvalidValue() { LocalDeployerProperties properties = new LocalDeployerProperties(); properties.setDebugPort(-666); Optional debugAddress = DebugAddress.from(properties, 0); assertThat(debugAddress.isPresent()).isFalse(); } @Test public void testDebugAddressPortOnly() { LocalDeployerProperties properties = new LocalDeployerProperties(); properties.setDebugAddress("20075"); Optional debugAddress = DebugAddress.from(properties, 100); assertThat(debugAddress.isPresent()).isTrue(); assertThat(debugAddress.get().getHost()).isNull(); assertThat(debugAddress.get().getPort()).isEqualTo("20175"); assertThat(debugAddress.get().getAddress()).isEqualTo("20175"); } @Test public void testDebugAddressWildcardHost() { LocalDeployerProperties properties = new LocalDeployerProperties(); properties.setDebugAddress("*:20075"); Optional debugAddress = DebugAddress.from(properties, 100); assertThat(debugAddress.isPresent()).isTrue(); assertThat(debugAddress.get().getHost()).isEqualTo("*"); assertThat(debugAddress.get().getPort()).isEqualTo("20175"); assertThat(debugAddress.get().getAddress()).isEqualTo("*:20175"); } @Test public void testDebugAddressWithIP() { LocalDeployerProperties properties = new LocalDeployerProperties(); properties.setDebugAddress("127.0.0.1:20075"); Optional debugAddress = DebugAddress.from(properties, 100); assertThat(debugAddress.isPresent()).isTrue(); assertThat(debugAddress.get().getHost()).isEqualTo("127.0.0.1"); assertThat(debugAddress.get().getPort()).isEqualTo("20175"); assertThat(debugAddress.get().getAddress()).isEqualTo("127.0.0.1:20175"); } @Test public void testDebugAddressWithHostname() { LocalDeployerProperties properties = new LocalDeployerProperties(); properties.setDebugAddress("localhost:20075"); Optional debugAddress = DebugAddress.from(properties, 100); assertThat(debugAddress.isPresent()).isTrue(); assertThat(debugAddress.get().getHost()).isEqualTo("localhost"); assertThat(debugAddress.get().getPort()).isEqualTo("20175"); assertThat(debugAddress.get().getAddress()).isEqualTo("localhost:20175"); } @Test public void testDebugAddressWithInvalidIP() { LocalDeployerProperties properties = new LocalDeployerProperties(); properties.setDebugAddress("127.0.:20075"); Optional debugAddress = DebugAddress.from(properties, 100); assertThat(debugAddress.isPresent()).isFalse(); } } ================================================ FILE: src/test/java/org/springframework/cloud/deployer/spi/local/DeployerSocketUtilsTests.java ================================================ /* * Copyright 2016-2022 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.cloud.deployer.spi.local; import java.util.SortedSet; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** * Unit tests for {@link DeployerSocketUtils}. * * @author Sam Brannen * @author Gary Russell * @author Glenn Renfro */ class DeployerSocketUtilsTests { @Test void findAvailableTcpPortWithZeroMinPort() { assertThatIllegalArgumentException().isThrownBy(() -> DeployerSocketUtils.findAvailableTcpPort(0)); } @Test void findAvailableTcpPortWithNegativeMinPort() { assertThatIllegalArgumentException().isThrownBy(() -> DeployerSocketUtils.findAvailableTcpPort(-500)); } @Test void findAvailableTcpPortWithMin() { int port = DeployerSocketUtils.findAvailableTcpPort(50000); assertPortInRange(port, 50000, DeployerSocketUtils.PORT_RANGE_MAX); } @Test void find4AvailableTcpPortsInRange() { findAvailableTcpPorts(4, 30000, 35000); } @Test void find50AvailableTcpPortsInRange() { findAvailableTcpPorts(50, 40000, 45000); } @Test void findAvailableTcpPortsWithRequestedNumberGreaterThanSizeOfRange() { assertThatIllegalArgumentException().isThrownBy(() -> findAvailableTcpPorts(50, 45000, 45010)); } // Helpers private void findAvailableTcpPorts(int numRequested, int minPort, int maxPort) { SortedSet ports = DeployerSocketUtils.findAvailableTcpPorts(numRequested, minPort, maxPort); assertAvailablePorts(ports, numRequested, minPort, maxPort); } private void assertPortInRange(int port, int minPort, int maxPort) { assertThat(port >= minPort).as("port [" + port + "] >= " + minPort).isTrue(); assertThat(port <= maxPort).as("port [" + port + "] <= " + maxPort).isTrue(); } private void assertAvailablePorts(SortedSet ports, int numRequested, int minPort, int maxPort) { assertThat(ports.size()).as("number of ports requested").isEqualTo(numRequested); for (int port : ports) { assertPortInRange(port, minPort, maxPort); } } } ================================================ FILE: src/test/java/org/springframework/cloud/deployer/spi/local/DockerCommandBuilderTests.java ================================================ /* * Copyright 2017-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.cloud.deployer.spi.local; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Optional; import org.junit.jupiter.api.Test; import org.springframework.cloud.deployer.resource.docker.DockerResource; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.core.io.Resource; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link DockerCommandBuilder}. * * @author Eric Bottard * @author Christian Tzolov */ public class DockerCommandBuilderTests { @Test public void testContainerName() { AppDefinition appDefinition = new AppDefinition("foo", null); Resource resource = new DockerResource("foo/bar"); Map deploymentProperties = Collections.singletonMap(DockerCommandBuilder.DOCKER_CONTAINER_NAME_KEY, "gogo"); AppDeploymentRequest request = new AppDeploymentRequest(appDefinition, resource, deploymentProperties); ProcessBuilder builder = new DockerCommandBuilder(null) .buildExecutionCommand(request, new HashMap<>(), "deployerId", Optional.of(1), new LocalDeployerProperties(), Optional.empty()); assertThat(builder.command()).containsAnyElementsOf(Arrays.asList("docker", "run", "--rm", "--name=gogo-1", "foo/bar", "deployerId=deployerId")); } @Test public void testContainerNameWithDockerNetwork() { AppDefinition appDefinition = new AppDefinition("foo", null); Resource resource = new DockerResource("foo/bar"); Map deploymentProperties = Collections.singletonMap(DockerCommandBuilder.DOCKER_CONTAINER_NAME_KEY, "gogo"); AppDeploymentRequest request = new AppDeploymentRequest(appDefinition, resource, deploymentProperties); ProcessBuilder builder = new DockerCommandBuilder("scdf_default") .buildExecutionCommand(request, new HashMap<>(), "deployerId", Optional.of(1), new LocalDeployerProperties(), Optional.empty()); assertThat(builder.command()).containsAnyElementsOf(Arrays.asList("docker", "run", "--network", "scdf_default", "--rm", "--name=gogo-1", "foo/bar")); } @Test public void testContainerNameWithDockerNetworkAndKeepContainers() { AppDefinition appDefinition = new AppDefinition("foo", null); Resource resource = new DockerResource("foo/bar"); Map deploymentProperties = Collections.singletonMap(DockerCommandBuilder.DOCKER_CONTAINER_NAME_KEY, "gogo"); AppDeploymentRequest request = new AppDeploymentRequest(appDefinition, resource, deploymentProperties); LocalDeployerProperties localDeployerProperties = new LocalDeployerProperties(); localDeployerProperties.getDocker().setDeleteContainerOnExit(false); ProcessBuilder builder = new DockerCommandBuilder("scdf_default") .buildExecutionCommand(request, new HashMap<>(), "deployerId", Optional.of(1), localDeployerProperties, Optional.empty()); assertThat(builder.command()).containsAnyElementsOf(Arrays.asList("docker", "run", "--network", "scdf_default", "--name=gogo-1", "foo/bar")); assertThat(builder.command()).doesNotContain("--rm"); } @Test public void testUseLocalDeployerPropertiesToKeepStoppedContainer() { AppDefinition appDefinition = new AppDefinition("foo", null); Resource resource = new DockerResource("foo/bar"); Map deploymentProperties = Collections.singletonMap(DockerCommandBuilder.DOCKER_CONTAINER_NAME_KEY, "gogo"); AppDeploymentRequest request = new AppDeploymentRequest(appDefinition, resource, deploymentProperties); LocalDeployerProperties localDeployerProperties = new LocalDeployerProperties(); localDeployerProperties.getDocker().setDeleteContainerOnExit(false); ProcessBuilder builder = new DockerCommandBuilder("scdf_default") .buildExecutionCommand(request, new HashMap<>(), "deployerId", Optional.of(1), localDeployerProperties, Optional.empty()); assertThat(builder.command()).containsAnyElementsOf(Arrays.asList("docker", "run", "--network", "scdf_default", "--name=gogo-1", "foo/bar")); assertThat(builder.command()).doesNotContain("--rm"); } @Test public void testSpringApplicationJSON() { LocalDeployerProperties properties = new LocalDeployerProperties(); LocalAppDeployer deployer = new LocalAppDeployer(properties); AppDefinition definition = new AppDefinition("foo", Collections.singletonMap("foo", "bar")); Resource resource = new DockerResource("foo/bar"); Map deploymentProperties = new HashMap<>(); deploymentProperties.put(LocalDeployerProperties.DEBUG_ADDRESS, "*:9999"); deploymentProperties.put(LocalDeployerProperties.DEBUG_SUSPEND, "y"); deploymentProperties.put(LocalDeployerProperties.INHERIT_LOGGING, "true"); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, deploymentProperties); ProcessBuilder builder = deployer.buildProcessBuilder(request, request.getDefinition().getProperties(), Optional.of(1), "foo"); String SAJ = LocalDeployerUtils.isWindows() ? "SPRING_APPLICATION_JSON={\\\"foo\\\":\\\"bar\\\"}" : "SPRING_APPLICATION_JSON={\"foo\":\"bar\"}"; assertThat(builder.command()).contains("-e", SAJ); } @Test public void testContainerPortMappings(){ AppDefinition appDefinition = new AppDefinition("foo", null); Resource resource = new DockerResource("foo/bar"); Map deploymentProperties = Collections.emptyMap(); AppDeploymentRequest request = new AppDeploymentRequest(appDefinition, resource, deploymentProperties); String goodMapping1 = "9090:9090"; String goodMapping2 = "6090:7090"; String incompleteMapping = "8888"; LocalDeployerProperties localDeployerProperties = new LocalDeployerProperties(); localDeployerProperties.getDocker().setPortMappings(goodMapping1 + "," + goodMapping2 + "," + incompleteMapping); ProcessBuilder builder = new DockerCommandBuilder("scdf_default") .buildExecutionCommand(request, new HashMap<>(), "deployerId", Optional.of(1), localDeployerProperties, Optional.empty()); assertThat(builder.command()).contains(goodMapping1, goodMapping2); assertThat(builder.command()).doesNotContain(incompleteMapping); } @Test public void testContainerVolumeMount(){ AppDefinition appDefinition = new AppDefinition("foo", null); Resource resource = new DockerResource("foo/bar"); Map deploymentProperties = Collections.emptyMap(); AppDeploymentRequest request = new AppDeploymentRequest(appDefinition, resource, deploymentProperties); String goodMapping1 = "/tmp:/tmp"; String goodMapping2 = "/opt:/opt"; String incompleteMapping = "/dev/null"; LocalDeployerProperties localDeployerProperties = new LocalDeployerProperties(); localDeployerProperties.getDocker().setVolumeMounts(goodMapping1 + "," + goodMapping2 + "," + incompleteMapping); ProcessBuilder builder = new DockerCommandBuilder("scdf_default") .buildExecutionCommand(request, new HashMap<>(), "deployerId", Optional.of(1), localDeployerProperties, Optional.empty()); assertThat(builder.command()).contains(goodMapping1, goodMapping2); assertThat(builder.command()).doesNotContain(incompleteMapping); } } ================================================ FILE: src/test/java/org/springframework/cloud/deployer/spi/local/JavaExecutionCommandBuilderTests.java ================================================ /* * Copyright 2015-2021 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.cloud.deployer.spi.local; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.cloud.deployer.spi.app.AppDeployer; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.UrlResource; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class JavaExecutionCommandBuilderTests { private JavaCommandBuilder commandBuilder; private List args; private Map deploymentProperties; private LocalDeployerProperties localDeployerProperties; @BeforeEach public void setUp() { args = new ArrayList<>(); deploymentProperties = new HashMap<>(); localDeployerProperties = new LocalDeployerProperties(); commandBuilder = new JavaCommandBuilder(localDeployerProperties); } @Test public void testDirectJavaMemoryOption() { deploymentProperties.put(AppDeployer.MEMORY_PROPERTY_KEY, "1024m"); commandBuilder.addJavaOptions(args, deploymentProperties, localDeployerProperties); assertThat(args).hasSize(1); assertThat(args.get(0)).isEqualTo("-Xmx1024m"); } @Test public void testDirectJavaMemoryOptionWithG() { deploymentProperties.put(AppDeployer.MEMORY_PROPERTY_KEY, "1g"); commandBuilder.addJavaOptions(args, deploymentProperties, localDeployerProperties); assertThat(args).hasSize(1); assertThat(args.get(0)).isEqualTo("-Xmx1024m"); } @Test public void testJavaMemoryOption() { deploymentProperties.put(LocalDeployerProperties.PREFIX + ".javaOpts", "-Xmx1024m"); commandBuilder.addJavaOptions(args, deploymentProperties, localDeployerProperties); assertThat(args).hasSize(1); assertThat(args.get(0)).isEqualTo("-Xmx1024m"); } @Test public void testJavaMemoryOptionWithKebabCase() { deploymentProperties.put(LocalDeployerProperties.PREFIX + ".java-opts", "-Xmx1024m"); commandBuilder.addJavaOptions(args, deploymentProperties, localDeployerProperties); assertThat(args).hasSize(1); assertThat(args.get(0)).isEqualTo("-Xmx1024m"); } @Test public void testJavaCmdOption() throws Exception { Map properties = new HashMap<>(); properties.put(LocalDeployerProperties.PREFIX + ".javaCmd", "/test/java"); Resource resource = mock(Resource.class); when(resource.getFile()).thenReturn(new File("/")); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(mock(AppDefinition.class), resource, properties); ProcessBuilder builder = commandBuilder.buildExecutionCommand(appDeploymentRequest, new HashMap<>(), "deployerId", Optional.of(1), new LocalDeployerProperties(), Optional.empty()); assertThat(builder.command().get(0)).isEqualTo("/test/java"); } @Test public void testJavaCmdOptionWithKebabCase() throws Exception { Map properties = new HashMap<>(); properties.put(LocalDeployerProperties.PREFIX + ".java-cmd", "/test/java"); Resource resource = mock(Resource.class); when(resource.getFile()).thenReturn(new File("/")); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(mock(AppDefinition.class), resource, properties); ProcessBuilder builder = commandBuilder.buildExecutionCommand(appDeploymentRequest, new HashMap<>(), "deployerId", Optional.of(1), new LocalDeployerProperties(), Optional.empty()); assertThat(builder.command().get(0)).isEqualTo("/test/java"); } @Test public void testOverrideMemoryOptions() { deploymentProperties.put(AppDeployer.MEMORY_PROPERTY_KEY, "1024m"); deploymentProperties.put(LocalDeployerProperties.PREFIX + ".javaOpts", "-Xmx2048m"); commandBuilder.addJavaOptions(args, deploymentProperties, localDeployerProperties); assertThat(args).hasSize(1); assertThat(args.get(0)).isEqualTo("-Xmx2048m"); } @Test public void testDirectMemoryOptionsWithOtherOptions() { deploymentProperties.put(AppDeployer.MEMORY_PROPERTY_KEY, "1024m"); deploymentProperties.put(LocalDeployerProperties.PREFIX + ".javaOpts", "-Dtest=foo"); commandBuilder.addJavaOptions(args, deploymentProperties, localDeployerProperties); assertThat(args).hasSize(2); assertThat(args.get(0)).isEqualTo("-Xmx1024m"); assertThat(args.get(1)).isEqualTo("-Dtest=foo"); } @Test public void testMultipleOptions() { deploymentProperties.put(LocalDeployerProperties.PREFIX + ".javaOpts", "-Dtest=foo -Dbar=baz"); commandBuilder.addJavaOptions(args, deploymentProperties, localDeployerProperties); assertThat(args).hasSize(2); assertThat(args.get(0)).isEqualTo("-Dtest=foo"); assertThat(args.get(1)).isEqualTo("-Dbar=baz"); } @Test public void testConfigurationPropertiesOverride() { localDeployerProperties.setJavaOpts("-Dfoo=test -Dbaz=bar"); commandBuilder.addJavaOptions(args, deploymentProperties, localDeployerProperties); assertThat(args).hasSize(2); assertThat(args.get(0)).isEqualTo("-Dfoo=test"); assertThat(args.get(1)).isEqualTo("-Dbaz=bar"); } @Test public void testJarExecution() { AppDefinition definition = new AppDefinition("randomApp", new HashMap<>()); deploymentProperties.put(LocalDeployerProperties.PREFIX + ".javaOpts", "-Dtest=foo -Dbar=baz"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, testResource(), deploymentProperties); commandBuilder.addJavaExecutionOptions(args, appDeploymentRequest); assertThat(args).hasSize(2); assertThat(args.get(0)).isEqualTo("-jar"); assertThat(args.get(1)).contains("testResource.txt"); } @Test public void testBadResourceExecution() { Assertions.assertThrows(IllegalStateException.class, () -> { AppDefinition definition = new AppDefinition("randomApp", new HashMap<>()); deploymentProperties.put(LocalDeployerProperties.PREFIX + ".javaOpts", "-Dtest=foo -Dbar=baz"); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, new UrlResource("https://spring.io"), deploymentProperties); commandBuilder.addJavaExecutionOptions(args, appDeploymentRequest); }); } @Test public void testCommandBuilderSpringApplicationJson() { LocalDeployerProperties properties = new LocalDeployerProperties(); LocalAppDeployer deployer = new LocalAppDeployer(properties); AppDefinition definition = new AppDefinition("foo", Collections.singletonMap("foo", "bar")); deploymentProperties.put(LocalDeployerProperties.DEBUG_PORT, "9999"); deploymentProperties.put(LocalDeployerProperties.DEBUG_SUSPEND, "y"); deploymentProperties.put(LocalDeployerProperties.INHERIT_LOGGING, "true"); AppDeploymentRequest request = new AppDeploymentRequest(definition, testResource(), deploymentProperties); ProcessBuilder builder = deployer.buildProcessBuilder(request, definition.getProperties(), Optional.of(1), "foo"); assertThat(builder.environment().keySet()).contains(AbstractLocalDeployerSupport.SPRING_APPLICATION_JSON); assertThat(builder.environment().get(AbstractLocalDeployerSupport.SPRING_APPLICATION_JSON)).isEqualTo("{\"foo\":\"bar\"}"); } @Test public void testCommandBuilderWithSpringApplicationJson() { LocalDeployerProperties properties = new LocalDeployerProperties(); LocalAppDeployer deployer = new LocalAppDeployer(properties); Map applicationProperties = new HashMap<>(); applicationProperties.put("foo", "bar"); String SAJ = "{\"debug\":\"true\"}"; applicationProperties.put(AbstractLocalDeployerSupport.SPRING_APPLICATION_JSON, SAJ); AppDefinition definition = new AppDefinition("foo", applicationProperties); deploymentProperties.put(LocalDeployerProperties.DEBUG_PORT, "9999"); deploymentProperties.put(LocalDeployerProperties.DEBUG_SUSPEND, "y"); deploymentProperties.put(LocalDeployerProperties.INHERIT_LOGGING, "true"); AppDeploymentRequest request = new AppDeploymentRequest(definition, testResource(), deploymentProperties); ProcessBuilder builder = deployer.buildProcessBuilder(request, definition.getProperties(), Optional.of(1), "foo"); assertThat(builder.environment().keySet()).contains(AbstractLocalDeployerSupport.SPRING_APPLICATION_JSON); assertThat(builder.environment().get(AbstractLocalDeployerSupport.SPRING_APPLICATION_JSON)).isEqualTo("{\"foo\":\"bar\",\"debug\":\"true\"}"); } @Test public void testRetainEnv() { LocalDeployerProperties properties1 = new LocalDeployerProperties(); LocalAppDeployer deployer1 = new LocalAppDeployer(properties1); AppDefinition definition1 = new AppDefinition("foo", null); AppDeploymentRequest request1 = new AppDeploymentRequest(definition1, testResource(), deploymentProperties); ProcessBuilder builder1 = deployer1.buildProcessBuilder(request1, definition1.getProperties(), Optional.of(1), "foo"); List env1 = builder1.environment().keySet().stream().map(String::toLowerCase).collect(Collectors.toList()); LocalDeployerProperties properties2 = new LocalDeployerProperties(); properties2.setEnvVarsToInherit(new String[0]); LocalAppDeployer deployer2 = new LocalAppDeployer(properties2); AppDefinition definition2 = new AppDefinition("foo", null); AppDeploymentRequest request2 = new AppDeploymentRequest(definition2, testResource(), deploymentProperties); ProcessBuilder builder2 = deployer2.buildProcessBuilder(request2, definition2.getProperties(), Optional.of(1), "foo"); List env2 = builder2.environment().keySet().stream().map(String::toLowerCase).collect(Collectors.toList()); if (env1.contains("path")) { // path should be there, and it was check that something were removed assertThat(builder1.environment().keySet().size()).isGreaterThan(builder2.environment().keySet().size()); } assertThat(env2).doesNotContain("path"); } protected Resource testResource() { return new ClassPathResource("testResource.txt"); } } ================================================ FILE: src/test/java/org/springframework/cloud/deployer/spi/local/LocalAppDeployerEnvironmentIntegrationTests.java ================================================ /* * Copyright 2018-2022 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.cloud.deployer.spi.local; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.nio.ByteBuffer; import java.time.Duration; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.UUID; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.deployer.resource.docker.DockerResource; import org.springframework.cloud.deployer.spi.app.ActuatorOperations; import org.springframework.cloud.deployer.spi.app.AppDeployer; import org.springframework.cloud.deployer.spi.app.AppInstanceStatus; import org.springframework.cloud.deployer.spi.app.AppStatus; import org.springframework.cloud.deployer.spi.app.DeploymentState; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.local.LocalAppDeployerEnvironmentIntegrationTests.Config; import org.springframework.cloud.deployer.spi.test.AbstractAppDeployerIntegrationJUnit5Tests; import org.springframework.cloud.deployer.spi.test.AbstractIntegrationTests; import org.springframework.cloud.deployer.spi.test.Timeout; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.Resource; import org.springframework.web.client.RestTemplate; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; /** * Integration tests for {@link LocalAppDeployer} not using SAJ. * * Now supports running with Docker images for tests, just set this env var: * * SPRING_CLOUD_DEPLOYER_SPI_TEST_USE_DOCKER=true * * @author Eric Bottard * @author Mark Fisher * @author Oleg Zhurakousky * @author Janne Valkealahti * @author Ilayaperumal Gopinathan */ @SpringBootTest(classes = { Config.class, AbstractIntegrationTests.Config.class }, value = { "maven.remoteRepositories.springRepo.url=https://repo.spring.io/snapshot", "spring.cloud.deployer.local.use-spring-application-json=false" }) public class LocalAppDeployerEnvironmentIntegrationTests extends AbstractAppDeployerIntegrationJUnit5Tests { private static final String TESTAPP_DOCKER_IMAGE_NAME = "springcloud/spring-cloud-deployer-spi-test-app:latest"; @Autowired private AppDeployer appDeployer; @Autowired private ActuatorOperations actuatorOperations; @Value("${spring-cloud-deployer-spi-test-use-docker:false}") private boolean useDocker; @Override protected AppDeployer provideAppDeployer() { return appDeployer; } @Override protected Resource testApplication() { if (useDocker) { logger.info("Using Docker image for tests"); return new DockerResource(TESTAPP_DOCKER_IMAGE_NAME); } return super.testApplication(); } @Override protected String randomName() { if (LocalDeployerUtils.isWindows()) { // tweak random dir name on win to be shorter String uuid = UUID.randomUUID().toString(); long l = ByteBuffer.wrap(uuid.toString().getBytes()).getLong(); return testName + Long.toString(l, Character.MAX_RADIX); } else { return super.randomName(); } } @Test public void testEnvVariablesInheritedViaEnvEndpointNoSaj() { if (useDocker) { // would not expect to be able to check anything on docker return; } Map properties = new HashMap<>(); AppDefinition definition = new AppDefinition(randomName(), properties); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = appDeployer().deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); Map instances = appDeployer().status(deploymentId).getInstances(); String url = null; if (instances.size() == 1) { url = instances.entrySet().iterator().next().getValue().getAttributes().get("url"); } String env = null; if (url != null) { RestTemplate template = new RestTemplate(); env = template.getForObject(url + "/actuator/env", String.class); } logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer().undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); assertThat(url).isNotNull(); if (LocalDeployerUtils.isWindows()) { // windows is weird, we may still get Path or PATH assertThat(env).containsIgnoringCase("path"); } else { assertThat(env).contains("\"PATH\""); // we're not using SAJ so it's i.e. // INSTANCE_INDEX not instance.index assertThat(env).contains("\"INSTANCE_INDEX\""); assertThat(env).contains("\"SPRING_APPLICATION_INDEX\""); assertThat(env).contains("\"SPRING_CLOUD_APPLICATION_GUID\""); } } @Test public void testFailureToCallShutdownOnUndeploy() { Map properties = new HashMap<>(); // disable shutdown endpoint properties.put("endpoints.shutdown.enabled", "false"); AppDefinition definition = new AppDefinition(randomName(), properties); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = appDeployer().deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer().undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); } @Test // was triggered by GH-50 and subsequently GH-55 public void testNoStdoutStderrOnInheritLoggingAndNoNPEOnGetAttributes() { Map properties = new HashMap<>(); AppDefinition definition = new AppDefinition(randomName(), properties); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, Collections.singletonMap(LocalDeployerProperties.INHERIT_LOGGING, "true")); AppDeployer deployer = appDeployer(); String deploymentId = deployer.deploy(request); AppStatus appStatus = deployer.status(deploymentId); assertThat(appStatus.getInstances()).hasSizeGreaterThan(0); for (Entry instanceStatusEntry : appStatus.getInstances().entrySet()) { Map attributes = instanceStatusEntry.getValue().getAttributes(); assertThat(attributes).doesNotContainKey("stdout"); assertThat(attributes).doesNotContainKey("stderr"); } deployer.undeploy(deploymentId); } @Test public void testInDebugModeWithSuspended() throws Exception { Map properties = new HashMap<>(); AppDefinition definition = new AppDefinition(randomName(), properties); Resource resource = testApplication(); Map deploymentProperties = new HashMap<>(); deploymentProperties.put(LocalDeployerProperties.DEBUG_PORT, "9999"); deploymentProperties.put(LocalDeployerProperties.DEBUG_SUSPEND, "y"); deploymentProperties.put(LocalDeployerProperties.INHERIT_LOGGING, "true"); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, deploymentProperties); AppDeployer deployer = appDeployer(); String deploymentId = deployer.deploy(request); Thread.sleep(5000); AppStatus appStatus = deployer.status(deploymentId); if (resource instanceof DockerResource) { try { String containerId = getCommandOutput("docker ps -q --filter ancestor="+ TESTAPP_DOCKER_IMAGE_NAME); String logOutput = getCommandOutput("docker logs "+ containerId); assertThat(logOutput).contains("Listening for transport dt_socket at address: 9999"); } catch (IOException e) { } } else { assertThat(appStatus.toString()).contains("deploying"); } deployer.undeploy(deploymentId); } @Test public void testActuatorOperations() { if (useDocker) { // would not expect to be able to check anything on docker return; } Map properties = new HashMap<>(); AppDefinition definition = new AppDefinition(randomName(), properties); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = appDeployer().deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); String id = deploymentId + "-0"; Map env = actuatorOperations .getFromActuator(deploymentId, id, "/env", Map.class); assertThat(env).containsKeys("activeProfiles", "propertySources"); Map status = actuatorOperations .getFromActuator(deploymentId, id, "/health", Map.class); assertThat(status.get("status")).isEqualTo("UP"); Map loggers = actuatorOperations .getFromActuator(deploymentId, id, "/loggers/org.springframework", Map.class); assertThat(loggers).isNotNull(); assertThat(loggers.get("configuredLevel")).isNull(); actuatorOperations.postToActuator(deploymentId, id,"/loggers/org.springframework", Collections.singletonMap("configuredLevel", "debug"), Object.class); loggers = actuatorOperations .getFromActuator(deploymentId, id, "/loggers/org.springframework", Map.class); assertThat(((String)loggers.get("configuredLevel")).toLowerCase(Locale.ROOT)).isEqualTo("debug"); } private String getCommandOutput(String cmd) throws IOException { Process process = Runtime.getRuntime().exec(cmd); BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream())); return stdInput.lines().findFirst().get(); } @Configuration @EnableConfigurationProperties(LocalDeployerProperties.class) public static class Config { @Bean public AppDeployer appDeployer(LocalDeployerProperties properties) { return new LocalAppDeployer(properties); } @Bean ActuatorOperations actuatorOperations(AppDeployer appDeployer, LocalDeployerProperties properties) { return new LocalActuatorTemplate(new RestTemplate(), appDeployer, properties.getAppAdmin()); } } } ================================================ FILE: src/test/java/org/springframework/cloud/deployer/spi/local/LocalAppDeployerIntegrationTests.java ================================================ /* * Copyright 2016-2021 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.cloud.deployer.spi.local; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.nio.ByteBuffer; import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.UUID; import java.util.stream.Collectors; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.deployer.resource.docker.DockerResource; import org.springframework.cloud.deployer.spi.app.AppDeployer; import org.springframework.cloud.deployer.spi.app.AppInstanceStatus; import org.springframework.cloud.deployer.spi.app.AppStatus; import org.springframework.cloud.deployer.spi.app.DeploymentState; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.local.LocalAppDeployerIntegrationTests.Config; import org.springframework.cloud.deployer.spi.test.AbstractAppDeployerIntegrationJUnit5Tests; import org.springframework.cloud.deployer.spi.test.AbstractIntegrationTests; import org.springframework.cloud.deployer.spi.test.Timeout; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.Resource; import org.springframework.web.client.RestTemplate; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; /** * Integration tests for {@link LocalAppDeployer}. * * Now supports running with Docker images for tests, just set this env var: * * SPRING_CLOUD_DEPLOYER_SPI_TEST_USE_DOCKER=true * * @author Eric Bottard * @author Mark Fisher * @author Oleg Zhurakousky * @author Janne Valkealahti * @author Ilayaperumal Gopinathan */ @SpringBootTest(classes = {Config.class, AbstractIntegrationTests.Config.class}, value = { "maven.remoteRepositories.springRepo.url=https://repo.spring.io/snapshot"}) public class LocalAppDeployerIntegrationTests extends AbstractAppDeployerIntegrationJUnit5Tests { private static final String TESTAPP_DOCKER_IMAGE_NAME = "springcloud/spring-cloud-deployer-spi-test-app:latest"; @Autowired private AppDeployer appDeployer; @Value("${spring-cloud-deployer-spi-test-use-docker:false}") private boolean useDocker; @Override protected AppDeployer provideAppDeployer() { return appDeployer; } @Override protected Resource testApplication() { if (useDocker) { logger.info("Using Docker image for tests"); return new DockerResource(TESTAPP_DOCKER_IMAGE_NAME); } return super.testApplication(); } @Override protected String randomName() { if (LocalDeployerUtils.isWindows()) { // tweak random dir name on win to be shorter String uuid = UUID.randomUUID().toString(); long l = ByteBuffer.wrap(uuid.toString().getBytes()).getLong(); return testName + Long.toString(l, Character.MAX_RADIX); } else { return super.randomName(); } } @Test public void testEnvVariablesInheritedViaEnvEndpoint() { if (useDocker) { // would not expect to be able to check anything on docker return; } Map properties = new HashMap<>(); AppDefinition definition = new AppDefinition(randomName(), properties); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = appDeployer().deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); Map instances = appDeployer().status(deploymentId).getInstances(); String url = null; if (instances.size() == 1) { url = instances.entrySet().iterator().next().getValue().getAttributes().get("url"); } String env = null; if (url != null) { RestTemplate template = new RestTemplate(); env = template.getForObject(url + "/actuator/env", String.class); } logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer().undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); assertThat(url).isNotNull(); if (LocalDeployerUtils.isWindows()) { // windows is weird, we may still get Path or PATH assertThat(env).containsIgnoringCase("path"); } else { assertThat(env).contains("\"PATH\""); // we're defaulting to SAJ so it's i.e. // instance.index not INSTANCE_INDEX assertThat(env).contains("\"instance.index\""); assertThat(env).contains("\"spring.application.index\""); assertThat(env).contains("\"spring.cloud.application.guid\""); assertThat(env).contains("\"spring.cloud.stream.instanceIndex\""); } } @Test public void testAppLogRetrieval() { Map properties = new HashMap<>(); AppDefinition definition = new AppDefinition(randomName(), properties); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = appDeployer().deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); String logContent = appDeployer().getLog(deploymentId); assertThat(logContent).contains("Starting DeployerIntegrationTestApplication"); } // TODO: remove when these two are forced in tck tests @Test public void testScale() { doTestScale(false); } @Test public void testScaleWithIndex() { doTestScale(true); } @Test public void testFailureToCallShutdownOnUndeploy() { Map properties = new HashMap<>(); // disable shutdown endpoint properties.put("endpoints.shutdown.enabled", "false"); AppDefinition definition = new AppDefinition(randomName(), properties); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = appDeployer().deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer().undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); } @Test // was triggered by GH-50 and subsequently GH-55 public void testNoStdoutStderrOnInheritLoggingAndNoNPEOnGetAttributes() { Map properties = new HashMap<>(); AppDefinition definition = new AppDefinition(randomName(), properties); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, Collections.singletonMap(LocalDeployerProperties.INHERIT_LOGGING, "true")); AppDeployer deployer = appDeployer(); String deploymentId = deployer.deploy(request); AppStatus appStatus = deployer.status(deploymentId); assertThat(appStatus.getInstances()).hasSizeGreaterThan(0); for (Entry instanceStatusEntry : appStatus.getInstances().entrySet()) { Map attributes = instanceStatusEntry.getValue().getAttributes(); assertThat(attributes).doesNotContainKey("stdout"); assertThat(attributes).doesNotContainKey("stderr"); } deployer.undeploy(deploymentId); } @Test public void testInDebugModeWithSuspended() throws Exception { Map properties = new HashMap<>(); AppDefinition definition = new AppDefinition(randomName(), properties); Resource resource = testApplication(); Map deploymentProperties = new HashMap<>(); deploymentProperties.put(LocalDeployerProperties.DEBUG_PORT, "9999"); deploymentProperties.put(LocalDeployerProperties.DEBUG_SUSPEND, "y"); deploymentProperties.put(LocalDeployerProperties.INHERIT_LOGGING, "true"); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, deploymentProperties); AppDeployer deployer = appDeployer(); String deploymentId = deployer.deploy(request); Thread.sleep(5000); AppStatus appStatus = deployer.status(deploymentId); if (resource instanceof DockerResource) { try { String containerId = getCommandOutput("docker ps -q --filter ancestor="+ TESTAPP_DOCKER_IMAGE_NAME); String logOutput = getCommandOutput("docker logs "+ containerId); assertThat(logOutput).contains("Listening for transport dt_socket at address: 9999"); } catch (IOException e) { } } else { assertThat(appStatus.toString()).contains("deploying"); } deployer.undeploy(deploymentId); } @Test public void testInDebugModeWithSuspendedUseCamelCase() throws Exception { Map properties = new HashMap<>(); AppDefinition definition = new AppDefinition(randomName(), properties); Resource resource = testApplication(); Map deploymentProperties = new HashMap<>(); deploymentProperties.put(LocalDeployerProperties.PREFIX + ".debugPort", "8888"); deploymentProperties.put(LocalDeployerProperties.PREFIX + ".debugSuspend", "y"); deploymentProperties.put(LocalDeployerProperties.PREFIX + ".inheritLogging", "true"); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, deploymentProperties); AppDeployer deployer = appDeployer(); String deploymentId = deployer.deploy(request); Thread.sleep(5000); AppStatus appStatus = deployer.status(deploymentId); if (resource instanceof DockerResource) { try { String containerId = getCommandOutput("docker ps -q --filter ancestor="+ TESTAPP_DOCKER_IMAGE_NAME); String logOutput = getCommandOutput("docker logs "+ containerId); assertThat(logOutput).contains("Listening for transport dt_socket at address: 8888"); String containerPorts = getCommandOutputAll("docker port "+ containerId); assertThat(containerPorts).contains("8888/tcp -> 0.0.0.0:8888"); } catch (IOException e) { } } else { assertThat(appStatus.toString()).contains("deploying"); } deployer.undeploy(deploymentId); } @Test public void testUseDefaultDeployerProperties() throws IOException { LocalDeployerProperties localDeployerProperties = new LocalDeployerProperties(); Path tmpPath = new File(System.getProperty("java.io.tmpdir")).toPath(); Path customWorkDirRoot = tmpPath.resolve("test-default-directory"); localDeployerProperties.setWorkingDirectoriesRoot(customWorkDirRoot.toFile().getAbsolutePath()); // Create a new LocalAppDeployer using a working directory that is different from the default value. AppDeployer appDeployer = new LocalAppDeployer(localDeployerProperties); List beforeDirs = getBeforePaths(customWorkDirRoot); Map properties = new HashMap<>(); properties.put("server.port", "0"); AppDefinition definition = new AppDefinition(randomName(), properties); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); // Deploy String deploymentId = appDeployer.deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer.status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); timeout = undeploymentTimeout(); // Undeploy appDeployer.undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer.status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); List afterDirs = getAfterPaths(customWorkDirRoot); assertThat(afterDirs).as("Additional working directory not created").hasSize(beforeDirs.size() + 1); } @Test public void testZeroPortReportsDeployed() throws IOException { Map properties = new HashMap<>(); properties.put("server.port", "0"); Path tmpPath = new File(System.getProperty("java.io.tmpdir")).toPath(); Path customWorkDirRoot = tmpPath.resolve("spring-cloud-deployer-app-workdir"); Map deploymentProperties = new HashMap<>(); deploymentProperties.put(LocalDeployerProperties.PREFIX + ".working-directories-root", customWorkDirRoot.toFile().getAbsolutePath()); AppDefinition definition = new AppDefinition(randomName(), properties); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, deploymentProperties); List beforeDirs = getBeforePaths(customWorkDirRoot); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = appDeployer().deploy(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.deployed); }); logger.info("Undeploying {}...", deploymentId); timeout = undeploymentTimeout(); appDeployer().undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); List afterDirs = getAfterPaths(customWorkDirRoot); assertThat(afterDirs.size()).as("Additional working directory not created").isEqualTo(beforeDirs.size() + 1); } @Test public void testStartupProbeFail() { Map deploymentProperties = new HashMap<>(); deploymentProperties.put(LocalDeployerProperties.PREFIX + ".startup-probe.path", "/fake"); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, deploymentProperties); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = appDeployer().deploy(request); await().atMost(Duration.ofSeconds(30)).until(() -> { return appDeployer().status(deploymentId).getState() == DeploymentState.deploying; }); await().during(Duration.ofSeconds(10)).atMost(Duration.ofSeconds(12)).until(() -> { return appDeployer().status(deploymentId).getState() == DeploymentState.deploying; }); logger.info("Undeploying {}...", deploymentId); Timeout timeout = undeploymentTimeout(); appDeployer().undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); } @Test public void testStartupProbeSucceed() { Map deploymentProperties = new HashMap<>(); deploymentProperties.put(LocalDeployerProperties.PREFIX + ".startup-probe.path", "/actuator/info"); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, deploymentProperties); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = appDeployer().deploy(request); await().atMost(Duration.ofSeconds(30)).until(() -> { return appDeployer().status(deploymentId).getState() == DeploymentState.deploying; }); await().atMost(Duration.ofSeconds(12)).until(() -> { return appDeployer().status(deploymentId).getState() == DeploymentState.deployed; }); logger.info("Undeploying {}...", deploymentId); Timeout timeout = undeploymentTimeout(); appDeployer().undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); } @Test public void testHealthProbeFail() { Map deploymentProperties = new HashMap<>(); deploymentProperties.put(LocalDeployerProperties.PREFIX + ".health-probe.path", "/fake"); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, deploymentProperties); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = appDeployer().deploy(request); await().during(Duration.ofSeconds(10)).atMost(Duration.ofSeconds(12)).until(() -> { return appDeployer().status(deploymentId).getState() == DeploymentState.failed; }); logger.info("Undeploying {}...", deploymentId); Timeout timeout = undeploymentTimeout(); appDeployer().undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); } @Test public void testHealthProbeSucceed() { Map deploymentProperties = new HashMap<>(); deploymentProperties.put(LocalDeployerProperties.PREFIX + ".health-probe.path", "/actuator/info"); AppDefinition definition = new AppDefinition(randomName(), null); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, deploymentProperties); logger.info("Deploying {}...", request.getDefinition().getName()); String deploymentId = appDeployer().deploy(request); await().atMost(Duration.ofSeconds(30)).until(() -> { return appDeployer().status(deploymentId).getState() == DeploymentState.deployed; }); await().during(Duration.ofSeconds(10)).atMost(Duration.ofSeconds(15)).until(() -> { return appDeployer().status(deploymentId).getState() == DeploymentState.deployed; }); logger.info("Undeploying {}...", deploymentId); Timeout timeout = undeploymentTimeout(); appDeployer().undeploy(deploymentId); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(appDeployer().status(deploymentId).getState()).isEqualTo(DeploymentState.unknown); }); } private List getAfterPaths(Path customWorkDirRoot) throws IOException { if (!Files.exists(customWorkDirRoot)) { return new ArrayList<>(); } return Files.walk(customWorkDirRoot, 1) .filter(path -> Files.isDirectory(path)) .filter(path -> !path.getFileName().toString().startsWith(".")) .collect(Collectors.toList()); } private List getBeforePaths(Path customWorkDirRoot) throws IOException { List beforeDirs = new ArrayList<>(); beforeDirs.add(customWorkDirRoot); if (Files.exists(customWorkDirRoot)) { beforeDirs = Files.walk(customWorkDirRoot, 1) .filter(path -> Files.isDirectory(path)) .collect(Collectors.toList()); } return beforeDirs; } private String getCommandOutput(String cmd) throws IOException { Process process = Runtime.getRuntime().exec(cmd); BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream())); return stdInput.lines().findFirst().get(); } private String getCommandOutputAll(String cmd) throws IOException { Process process = Runtime.getRuntime().exec(cmd); BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream())); return stdInput.lines().collect(Collectors.joining()); } @Configuration @EnableConfigurationProperties(LocalDeployerProperties.class) public static class Config { @Bean public AppDeployer appDeployer(LocalDeployerProperties properties) { return new LocalAppDeployer(properties); } } } ================================================ FILE: src/test/java/org/springframework/cloud/deployer/spi/local/LocalDeployerPropertiesTests.java ================================================ /* * Copyright 2019-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.cloud.deployer.spi.local; import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledOnOs; import org.junit.jupiter.api.condition.OS; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.cloud.deployer.spi.app.AppAdmin; import org.springframework.core.env.StandardEnvironment; import org.springframework.core.env.SystemEnvironmentPropertySource; import static org.assertj.core.api.Assertions.assertThat; public class LocalDeployerPropertiesTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner(); @Test @EnabledOnOs(OS.LINUX) public void defaultNoPropertiesSet() { this.contextRunner .withUserConfiguration(Config1.class) .run((context) -> { LocalDeployerProperties properties = context.getBean(LocalDeployerProperties.class); assertThat(properties.getDebugPort()).isNull(); assertThat(properties.getDebugSuspend()).isEqualTo(LocalDeployerProperties.DebugSuspendType.y); assertThat(properties.isDeleteFilesOnExit()).isTrue(); assertThat(properties.getEnvVarsToInherit()).containsExactly("TMP", "LANG", "LANGUAGE", "LC_.*", "PATH", "SPRING_APPLICATION_JSON"); assertThat(properties.isInheritLogging()).isFalse(); assertThat(properties.getJavaCmd()).contains("java"); assertThat(properties.getJavaOpts()).isNull(); assertThat(properties.getMaximumConcurrentTasks()).isEqualTo(20); assertThat(properties.getPortRange()).isNotNull(); assertThat(properties.getPortRange().getLow()).isEqualTo(20000); assertThat(properties.getPortRange().getHigh()).isEqualTo(61000); assertThat(properties.getShutdownTimeout()).isEqualTo(30); assertThat(properties.isUseSpringApplicationJson()).isTrue(); assertThat(properties.getDocker().getNetwork()).isEqualTo("bridge"); assertThat(properties.getStartupProbe()).isNotNull(); assertThat(properties.getStartupProbe().getPath()).isNull(); assertThat(properties.getHealthProbe()).isNotNull(); assertThat(properties.getHealthProbe().getPath()).isNull(); }); } @Test public void setAllProperties() { this.contextRunner .withInitializer(context -> { Map map = new HashMap<>(); map.put("spring.cloud.deployer.local.debug-port", "8888"); map.put("spring.cloud.deployer.local.debug-suspend", "n"); map.put("spring.cloud.deployer.local.delete-files-on-exit", false); map.put("spring.cloud.deployer.local.env-vars-to-inherit", "FOO,BAR"); map.put("spring.cloud.deployer.local.inherit-logging", true); map.put("spring.cloud.deployer.local.java-cmd", "foobar1"); map.put("spring.cloud.deployer.local.java-opts", "foobar2"); map.put("spring.cloud.deployer.local.maximum-concurrent-tasks", 1234); map.put("spring.cloud.deployer.local.port-range.low", 2345); map.put("spring.cloud.deployer.local.port-range.high", 2346); map.put("spring.cloud.deployer.local.shutdown-timeout", 3456); map.put("spring.cloud.deployer.local.use-spring-application-json", false); map.put("spring.cloud.deployer.local.docker.network", "spring-cloud-dataflow-server_default"); map.put("spring.cloud.deployer.local.docker.port-mappings", "9091:5678"); map.put("spring.cloud.deployer.local.docker.volume-mounts", "/tmp:/opt"); map.put("spring.cloud.deployer.local.startup-probe.path", "/path1"); map.put("spring.cloud.deployer.local.health-probe.path", "/path2"); map.put("spring.cloud.deployer.local.app-admin.user","user"); map.put("spring.cloud.deployer.local.app-admin.password","password"); context.getEnvironment().getPropertySources().addLast(new SystemEnvironmentPropertySource( StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, map)); }) .withUserConfiguration(Config1.class) .run((context) -> { LocalDeployerProperties properties = context.getBean(LocalDeployerProperties.class); assertThat(properties.getDebugPort()).isEqualTo(8888); assertThat(properties.getDebugSuspend()).isEqualTo(LocalDeployerProperties.DebugSuspendType.n); assertThat(properties.isDeleteFilesOnExit()).isFalse(); assertThat(properties.getEnvVarsToInherit()).containsExactly("FOO", "BAR"); assertThat(properties.isInheritLogging()).isTrue(); assertThat(properties.getJavaCmd()).contains("foobar1"); assertThat(properties.getJavaOpts()).contains("foobar2"); assertThat(properties.getMaximumConcurrentTasks()).isEqualTo(1234); assertThat(properties.getPortRange()).isNotNull(); assertThat(properties.getPortRange().getLow()).isEqualTo(2345); assertThat(properties.getPortRange().getHigh()).isEqualTo(2346); assertThat(properties.getShutdownTimeout()).isEqualTo(3456); assertThat(properties.isUseSpringApplicationJson()).isFalse(); assertThat(properties.getDocker().getNetwork()).isEqualTo("spring-cloud-dataflow-server_default"); assertThat(properties.getDocker().getPortMappings()).isEqualTo("9091:5678"); assertThat(properties.getDocker().getVolumeMounts()).isEqualTo("/tmp:/opt"); assertThat(properties.getStartupProbe().getPath()).isEqualTo("/path1"); assertThat(properties.getHealthProbe().getPath()).isEqualTo("/path2"); assertThat(properties.getAppAdmin().getUser()).isEqualTo("user"); assertThat(properties.getAppAdmin().getPassword()).isEqualTo("password"); }); } @Test public void setAllPropertiesCamelCase() { this.contextRunner .withInitializer(context -> { AppAdmin appAdmin = new AppAdmin(); appAdmin.setUser("user"); appAdmin.setPassword("password"); Map map = new HashMap<>(); map.put("spring.cloud.deployer.local.debugPort", "8888"); map.put("spring.cloud.deployer.local.debugSuspend", "n"); map.put("spring.cloud.deployer.local.deleteFilesOnExit", false); map.put("spring.cloud.deployer.local.envVarsToInherit", "FOO,BAR"); map.put("spring.cloud.deployer.local.inheritLogging", true); map.put("spring.cloud.deployer.local.javaCmd", "foobar1"); map.put("spring.cloud.deployer.local.javaOpts", "foobar2"); map.put("spring.cloud.deployer.local.maximumConcurrentTasks", 1234); map.put("spring.cloud.deployer.local.portRange.low", 2345); map.put("spring.cloud.deployer.local.portRange.high", 2346); map.put("spring.cloud.deployer.local.shutdownTimeout", 3456); map.put("spring.cloud.deployer.local.useSpringApplicationJson", false); map.put("spring.cloud.deployer.local.docker.network", "spring-cloud-dataflow-server_default"); map.put("spring.cloud.deployer.local.docker.portMappings", "9091:5678"); map.put("spring.cloud.deployer.local.docker.volumeMounts", "/tmp:/opt"); map.put("spring.cloud.deployer.local.appAdmin.user","user"); map.put("spring.cloud.deployer.local.appAdmin.password","password"); context.getEnvironment().getPropertySources().addLast(new SystemEnvironmentPropertySource( StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, map)); }) .withUserConfiguration(Config1.class) .run((context) -> { LocalDeployerProperties properties = context.getBean(LocalDeployerProperties.class); assertThat(properties.getDebugPort()).isEqualTo(8888); assertThat(properties.getDebugSuspend()).isEqualTo(LocalDeployerProperties.DebugSuspendType.n); assertThat(properties.isDeleteFilesOnExit()).isFalse(); assertThat(properties.getEnvVarsToInherit()).containsExactly("FOO", "BAR"); assertThat(properties.isInheritLogging()).isTrue(); assertThat(properties.getJavaCmd()).contains("foobar1"); assertThat(properties.getJavaOpts()).contains("foobar2"); assertThat(properties.getMaximumConcurrentTasks()).isEqualTo(1234); assertThat(properties.getPortRange()).isNotNull(); assertThat(properties.getPortRange().getLow()).isEqualTo(2345); assertThat(properties.getPortRange().getHigh()).isEqualTo(2346); assertThat(properties.getShutdownTimeout()).isEqualTo(3456); assertThat(properties.isUseSpringApplicationJson()).isFalse(); assertThat(properties.getDocker().getNetwork()).isEqualTo("spring-cloud-dataflow-server_default"); assertThat(properties.getDocker().getPortMappings()).isEqualTo("9091:5678"); assertThat(properties.getDocker().getVolumeMounts()).isEqualTo("/tmp:/opt"); assertThat(properties.getAppAdmin().getUser()).isEqualTo("user"); assertThat(properties.getAppAdmin().getPassword()).isEqualTo("password"); }); } @Test @EnabledOnOs(OS.WINDOWS) public void testOnWindows() { this.contextRunner .withInitializer(context -> { Map map = new HashMap<>(); map.put("spring.cloud.deployer.local.working-directories-root", "file:/C:/tmp"); context.getEnvironment().getPropertySources().addLast(new SystemEnvironmentPropertySource( StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, map)); }) .withUserConfiguration(Config1.class) .run((context) -> { LocalDeployerProperties properties = context.getBean(LocalDeployerProperties.class); assertThat(properties.getWorkingDirectoriesRoot()).isNotNull(); assertThat(properties.getWorkingDirectoriesRoot().toString()).isEqualTo("C:\\tmp"); }); } @Test @EnabledOnOs(OS.LINUX) public void testOnLinux() { this.contextRunner .withInitializer(context -> { Map map = new HashMap<>(); map.put("spring.cloud.deployer.local.working-directories-root", "/tmp"); context.getEnvironment().getPropertySources().addLast(new SystemEnvironmentPropertySource( StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, map)); }) .withUserConfiguration(Config1.class) .run((context) -> { LocalDeployerProperties properties = context.getBean(LocalDeployerProperties.class); assertThat(properties.getWorkingDirectoriesRoot()).isNotNull(); assertThat(properties.getWorkingDirectoriesRoot().toString()).isEqualTo("/tmp"); }); } @Test @EnabledOnOs(OS.LINUX) public void testCopyProperties() { LocalDeployerProperties from = new LocalDeployerProperties(); LocalDeployerProperties to = new LocalDeployerProperties(from); assertThat(from).isEqualTo(to); from = new LocalDeployerProperties(); from.setDebugPort(1234); from.setDebugSuspend(LocalDeployerProperties.DebugSuspendType.y); from.getDocker().setNetwork("network"); from.setDeleteFilesOnExit(true); from.setEnvVarsToInherit(new String[] { "foobar" }); from.setInheritLogging(false); from.setJavaCmd("javaCmd"); from.setJavaOpts("javaOpts"); from.setMaximumConcurrentTasks(234); from.getPortRange().setHigh(2345); from.getPortRange().setLow(2344); from.setShutdownTimeout(345); from.setUseSpringApplicationJson(false); from.setWorkingDirectoriesRoot(Paths.get("/first")); to = new LocalDeployerProperties(from); assertThat(from).isEqualTo(to); } @EnableConfigurationProperties({ LocalDeployerProperties.class }) private static class Config1 { } } ================================================ FILE: src/test/java/org/springframework/cloud/deployer/spi/local/LocalDeployerSupportTests.java ================================================ /* * Copyright 2017-2021 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.cloud.deployer.spi.local; import java.net.MalformedURLException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; /** * Tests for the AbstractLocalDeployerSupport * * @author Thomas Risberg */ public class LocalDeployerSupportTests { private LocalDeployerProperties localDeployerProperties; private AbstractLocalDeployerSupport localDeployerSupport; @BeforeEach public void setUp() { localDeployerProperties = new LocalDeployerProperties(); localDeployerSupport = new AbstractLocalDeployerSupport(this.localDeployerProperties) {}; } @Test public void testAppPropsAsSAJ() throws MalformedURLException { AppDeploymentRequest appDeploymentRequest = createAppDeploymentRequest(); HashMap envVarsToUse = new HashMap<>(appDeploymentRequest.getDefinition().getProperties()); Map environmentVariables = localDeployerSupport.formatApplicationProperties(appDeploymentRequest, envVarsToUse); assertThat(environmentVariables).hasSize(1); assertThat(environmentVariables).containsEntry(AbstractLocalDeployerSupport.SPRING_APPLICATION_JSON, "{\"test.foo\":\"foo\",\"test.bar\":\"bar\"}"); } @Test public void testCalcServerPort() throws MalformedURLException { Map applicationProperties = new HashMap<>(); Map deploymentPropertites = new HashMap<>(); List commandLineArgs = new ArrayList<>(); // test adding to application properties applicationProperties.put("server.port", "9292"); AppDefinition definition = new AppDefinition("randomApp", applicationProperties); AppDeploymentRequest appDeploymentRequest = new AppDeploymentRequest(definition, testResource(), deploymentPropertites, commandLineArgs); int portToUse = localDeployerSupport.calcServerPort(appDeploymentRequest, false, new HashMap<>()); assertThat(portToUse).isEqualTo(9292); // test adding to command line args, which has higher precedence than application properties commandLineArgs.add(LocalTaskLauncher.SERVER_PORT_KEY_COMMAND_LINE_ARG + 9191); appDeploymentRequest = new AppDeploymentRequest(definition, testResource(), deploymentPropertites, commandLineArgs); portToUse = localDeployerSupport.calcServerPort(appDeploymentRequest, false, new HashMap<>()); assertThat(portToUse).isEqualTo(9191); // test using dynamic port assignment portToUse = localDeployerSupport.calcServerPort(appDeploymentRequest, true, new HashMap<>()); assertThat(portToUse).isNotEqualTo(9191); assertThat(portToUse).isNotEqualTo(9292); } @Test public void testShutdownPropertyConfiguresRequestFactory() throws Exception { LocalDeployerProperties properties = new LocalDeployerProperties(); properties.setShutdownTimeout(1); AbstractLocalDeployerSupport abstractLocalDeployerSupport = new AbstractLocalDeployerSupport(properties) {}; Object restTemplate = ReflectionTestUtils.getField(abstractLocalDeployerSupport, "restTemplate"); Object requestFactory = ReflectionTestUtils.getField(restTemplate, "requestFactory"); Object connectTimeout = ReflectionTestUtils.getField(requestFactory, "connectTimeout"); Object readTimeout = ReflectionTestUtils.getField(requestFactory, "readTimeout"); assertThat(connectTimeout).isEqualTo(1000); assertThat(readTimeout).isEqualTo(1000); } @Test public void testShutdownPropertyNotConfiguresRequestFactory() throws Exception { LocalDeployerProperties properties = new LocalDeployerProperties(); properties.setShutdownTimeout(-1); AbstractLocalDeployerSupport abstractLocalDeployerSupport = new AbstractLocalDeployerSupport(properties) {}; Object restTemplate = ReflectionTestUtils.getField(abstractLocalDeployerSupport, "restTemplate"); Object requestFactory = ReflectionTestUtils.getField(restTemplate, "requestFactory"); Object connectTimeout = ReflectionTestUtils.getField(requestFactory,"connectTimeout"); Object readTimeout = ReflectionTestUtils.getField(requestFactory, "readTimeout"); assertThat(connectTimeout).isEqualTo(-1); assertThat(readTimeout).isEqualTo(-1); } protected AppDeploymentRequest createAppDeploymentRequest() throws MalformedURLException { return createAppDeploymentRequest(new HashMap<>()); } protected AppDeploymentRequest createAppDeploymentRequest(Map depProps) throws MalformedURLException { Map appProperties = new HashMap<>(); appProperties.put("test.foo", "foo"); appProperties.put("test.bar", "bar"); AppDefinition definition = new AppDefinition("randomApp", appProperties); return new AppDeploymentRequest(definition, testResource(), depProps); } protected Resource testResource() { return new ClassPathResource("testResource.txt"); } } ================================================ FILE: src/test/java/org/springframework/cloud/deployer/spi/local/LocalTaskLauncherIntegrationTests.java ================================================ /* * Copyright 2016-2022 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.cloud.deployer.spi.local; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Duration; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.stream.Collectors; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.io.TempDir; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.cloud.deployer.resource.docker.DockerResource; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.local.LocalTaskLauncherIntegrationTests.Config; import org.springframework.cloud.deployer.spi.task.LaunchState; import org.springframework.cloud.deployer.spi.task.TaskLauncher; import org.springframework.cloud.deployer.spi.test.AbstractIntegrationTests; import org.springframework.cloud.deployer.spi.test.AbstractTaskLauncherIntegrationJUnit5Tests; import org.springframework.cloud.deployer.spi.test.Timeout; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.Resource; import org.springframework.util.FileSystemUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; /** * Integration tests for {@link LocalTaskLauncher}. * * Now supports running with Docker images for tests, just set this env var: * * SPRING_CLOUD_DEPLOYER_SPI_TEST_USE_DOCKER=true * * @author Eric Bottard * @author Janne Valkealahti * @author David Turanski * @author Glenn Renfro * @author Ilayaperumal Gopinathan * @author Ben Blinebury * */ @SpringBootTest(classes = {Config.class, AbstractIntegrationTests.Config.class}, value = { "maven.remoteRepositories.springRepo.url=https://repo.spring.io/snapshot" }) @ExtendWith(OutputCaptureExtension.class) public class LocalTaskLauncherIntegrationTests extends AbstractTaskLauncherIntegrationJUnit5Tests { private static final String SYMBOLIC_LINK = "symbolic_link.txt"; @Autowired private TaskLauncher taskLauncher; @Value("${spring-cloud-deployer-spi-test-use-docker:false}") private boolean useDocker; @Override protected TaskLauncher provideTaskLauncher() { return taskLauncher; } @Override protected Resource testApplication() { if (useDocker) { logger.info("Using Docker image for tests"); return new DockerResource("springcloud/spring-cloud-deployer-spi-test-app:latest"); } return super.testApplication(); } @Override protected String randomName() { if (LocalDeployerUtils.isWindows()) { // tweak random dir name on win to be shorter String uuid = UUID.randomUUID().toString(); long l = ByteBuffer.wrap(uuid.toString().getBytes()).getLong(); return testName + Long.toString(l, Character.MAX_RADIX); } else { return super.randomName(); } } @Test public void testPassingServerPortViaCommandLineArgs(CapturedOutput output){ Map appProperties = new HashMap<>(); appProperties.put("killDelay", "0"); appProperties.put("exitCode", "0"); AppDefinition definition = new AppDefinition(this.randomName(), appProperties); basicLaunchAndValidation(definition, null); assertThat(output).contains("Logs will be in"); } @Test public void testBasicLaunchWithSymbolicLink(CapturedOutput output) throws Exception { Map appProperties = new HashMap<>(); appProperties.put("killDelay", "0"); appProperties.put("exitCode", "0"); Path symlink = createSymbolicLink(); AppDefinition definition = new AppDefinition(this.randomName(), appProperties); Map deploymentProperties = Collections.singletonMap("spring.cloud.deployer.local.workingDirectoriesRoot", SYMBOLIC_LINK); basicLaunchAndValidation(definition, deploymentProperties); assertThat(output).contains("Logs will be in"); Files.delete(Paths.get(symlink.toString())); } @TempDir private File tempDirectory; private Path createSymbolicLink() throws IOException { File testFile = new File(tempDirectory, "testFile.txt"); Path target = testFile.toPath(); Path link = Paths.get(SYMBOLIC_LINK); if (Files.exists(link)) { Files.delete(link); } return Files.createSymbolicLink(link, target); } @Test public void testInheritLoggingAndWorkDir(CapturedOutput output) throws IOException { Map appProperties = new HashMap<>(); appProperties.put("killDelay", "0"); appProperties.put("exitCode", "0"); Map deploymentProperties = new HashMap<>(); deploymentProperties.put(LocalDeployerProperties.INHERIT_LOGGING, "true"); Path tmpPath = new File(System.getProperty("java.io.tmpdir")).toPath(); Path customWorkDirRoot = tmpPath.resolve("spring-cloud-deployer-task-workdir"); deploymentProperties.put(LocalDeployerProperties.PREFIX + ".working-directories-root", customWorkDirRoot.toFile().getAbsolutePath()); AppDefinition definition = new AppDefinition(this.randomName(), appProperties); List beforeDirs = new ArrayList<>(); beforeDirs.add(customWorkDirRoot); if (Files.exists(customWorkDirRoot)) { beforeDirs = Files.walk(customWorkDirRoot, 1) .filter(path -> Files.isDirectory(path)) .collect(Collectors.toList()); } basicLaunchAndValidation(definition, deploymentProperties); assertThat(output).contains("Logs will be inherited."); List afterDirs = Files.walk(customWorkDirRoot, 1) .filter(path -> Files.isDirectory(path)) .collect(Collectors.toList()); assertThat(afterDirs).as("Additional working directory not created").hasSize(beforeDirs.size() + 1); // clean up if test passed FileSystemUtils.deleteRecursively(customWorkDirRoot); } @Test public void testAppLogRetrieval() { Map appProperties = new HashMap<>(); appProperties.put("killDelay", "0"); appProperties.put("exitCode", "0"); AppDefinition definition = new AppDefinition(randomName(), appProperties); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); String launchId1 = taskLauncher().launch(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(taskLauncher().status(launchId1).getState()).isEqualTo(LaunchState.complete); }); String logContent = taskLauncher().getLog(launchId1); assertThat(logContent).contains("Starting DeployerIntegrationTestApplication"); } @Test public void testDeleteHistoryOnReLaunch() { Map appProperties = new HashMap<>(); appProperties.put("killDelay", "0"); appProperties.put("exitCode", "0"); AppDefinition definition = new AppDefinition(randomName(), appProperties); Resource resource = testApplication(); AppDeploymentRequest request = new AppDeploymentRequest(definition, resource); String launchId1 = taskLauncher().launch(request); Timeout timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(taskLauncher().status(launchId1).getState()).isEqualTo(LaunchState.complete); }); String launchId2 = taskLauncher().launch(request); assertThat(launchId2).isNotEqualTo(launchId1); timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(taskLauncher().status(launchId2).getState()).isEqualTo(LaunchState.complete); }); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(taskLauncher().status(launchId1).getState()).isEqualTo(LaunchState.unknown); }); String launchId3 = taskLauncher().launch(request); assertThat(launchId3).isNotEqualTo(launchId1); assertThat(launchId3).isNotEqualTo(launchId2); timeout = deploymentTimeout(); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(taskLauncher().status(launchId3).getState()).isEqualTo(LaunchState.complete); }); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(taskLauncher().status(launchId1).getState()).isEqualTo(LaunchState.unknown); }); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(taskLauncher().status(launchId2).getState()).isEqualTo(LaunchState.unknown); }); taskLauncher().destroy(definition.getName()); } private void basicLaunchAndValidation(AppDefinition definition, Map deploymentProperties) { List commandLineArgs = new ArrayList<>(1); // Test to ensure no issues parsing server.port command line arg. commandLineArgs.add(LocalTaskLauncher.SERVER_PORT_KEY_COMMAND_LINE_ARG + DeployerSocketUtils.findAvailableTcpPort(LocalTaskLauncher.DEFAULT_SERVER_PORT)); AppDeploymentRequest request = new AppDeploymentRequest(definition, this.testApplication(), deploymentProperties, commandLineArgs); this.logger.info("Launching {}...", request.getDefinition().getName()); String launchId = this.taskLauncher().launch(request); Timeout timeout = this.deploymentTimeout(); await().pollInterval(Duration.ofMillis(1)) .atMost(Duration.ofSeconds(30)) .untilAsserted(() -> { assertThat(taskLauncher.getRunningTaskExecutionCount()).isEqualTo(1); }); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(taskLauncher().status(launchId).getState()).isEqualTo(LaunchState.complete); }); this.taskLauncher().destroy(definition.getName()); await().pollInterval(Duration.ofMillis(timeout.pause)) .atMost(Duration.ofMillis(timeout.totalTime)) .untilAsserted(() -> { assertThat(taskLauncher.getRunningTaskExecutionCount()).isEqualTo(0); }); } @Configuration @EnableConfigurationProperties(LocalDeployerProperties.class) public static class Config { @Bean public TaskLauncher taskLauncher(LocalDeployerProperties properties) { return new LocalTaskLauncher(properties); } } } ================================================ FILE: src/test/java/org/springframework/cloud/deployer/spi/local/RandomPortRangeContextTests.java ================================================ /* * Copyright 2018-2021 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.cloud.deployer.spi.local; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import static org.assertj.core.api.Assertions.assertThat; /** * @author Christian Tzolov */ public class RandomPortRangeContextTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(LocalDeployerAutoConfiguration.class)); @Test public void defaultProtRangeProperties() { this.contextRunner .withUserConfiguration(LocalDeployerAutoConfiguration.class) .run((context) -> { assertThat(context).hasSingleBean(LocalDeployerProperties.class); assertThat(context).hasSingleBean(LocalDeployerAutoConfiguration.class); assertThat(context).getBean(LocalDeployerProperties.class) .hasFieldOrPropertyWithValue("portRange.low", 20000); assertThat(context).getBean(LocalDeployerProperties.class) .hasFieldOrPropertyWithValue("portRange.high", 61000); }); } @Test public void presetProtRangeProperties() { this.contextRunner .withUserConfiguration(LocalDeployerAutoConfiguration.class) .withPropertyValues("spring.cloud.deployer.local.portRange.low=20001", "spring.cloud.deployer.local.portRange.high=20003") .run((context) -> { assertThat(context).hasSingleBean(LocalDeployerProperties.class); assertThat(context).hasSingleBean(LocalDeployerAutoConfiguration.class); assertThat(context).getBean(LocalDeployerProperties.class) .hasFieldOrPropertyWithValue("portRange.low", 20001); assertThat(context).getBean(LocalDeployerProperties.class) .hasFieldOrPropertyWithValue("portRange.high", 20003); }); } } ================================================ FILE: src/test/java/org/springframework/cloud/deployer/spi/local/RandomPortRangeTests.java ================================================ /* * Copyright 2018 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.cloud.deployer.spi.local; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.cloud.deployer.resource.docker.DockerResource; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.core.io.ClassPathResource; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; /** * @author Christian Tzolov */ @ExtendWith(MockitoExtension.class) public class RandomPortRangeTests { private AbstractLocalDeployerSupport localDeployerSupport; @Mock AppDeploymentRequest appDeploymentRequest; @BeforeEach public void setUp() { LocalDeployerProperties properties = new LocalDeployerProperties(); properties.getPortRange().setLow(30001); properties.getPortRange().setHigh(30213); properties.getDocker().getPortRange().setLow(40001); properties.getDocker().getPortRange().setHigh(40213); localDeployerSupport = new AbstractLocalDeployerSupport(properties) {}; } @Test public void portTests() { when(appDeploymentRequest.getResource()).thenReturn(new ClassPathResource("")); for (int i = 0; i < 30; i++) { int port = localDeployerSupport.getRandomPort(appDeploymentRequest); assertThat(port).isGreaterThanOrEqualTo(30001); assertThat(port).isLessThanOrEqualTo(30213 + 5); } } @Test public void portTests2() { when(appDeploymentRequest.getResource()).thenReturn(new DockerResource("/test:test")); for (int i = 0; i < 5; i++) { int port = localDeployerSupport.getRandomPort(appDeploymentRequest); assertThat(port).isGreaterThanOrEqualTo(40001); assertThat(port).isLessThanOrEqualTo(40213 + 5); } } } ================================================ FILE: src/test/java/org/springframework/cloud/deployer/spi/local/RandomPortTests.java ================================================ /* * Copyright 2018 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.cloud.deployer.spi.local; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.core.io.ClassPathResource; import static org.mockito.Mockito.when; /** * @author Mark Pollack */ @ExtendWith(MockitoExtension.class) public class RandomPortTests { private AbstractLocalDeployerSupport localDeployerSupport; @Mock AppDeploymentRequest appDeploymentRequest; @BeforeEach public void setUp() { localDeployerSupport = new AbstractLocalDeployerSupport(new LocalDeployerProperties()) {}; when(appDeploymentRequest.getResource()).thenReturn(new ClassPathResource("")); } @Test public void portTests() { //No exception should be thrown for (int i = 0; i < 100; i++) { localDeployerSupport.getRandomPort(appDeploymentRequest); } } } ================================================ FILE: src/test/resources/dataflow-server-podsecuritycontext.yml ================================================ # spring.cloud.deployer.kubernetes.podSecurityContext: podSecurityContext: fsGroup: 65534 fsGroupChangePolicy: Always runAsUser: 65534 runAsGroup: 65534 runAsNonRoot: true seLinuxOptions: level: "s0:c123,c456" seccompProfile: type: Localhost localhostProfile: my-profiles/profile-allow.json supplementalGroups: - 65534 - 65535 sysctls: - name: "kernel.shm_rmid_forced" value: 0 - name: "net.core.somaxconn" value: 1024 windowsOptions: gmsaCredentialSpec: specA gmsaCredentialSpecName: specA-name hostProcess: true runAsUserName: userA ================================================ FILE: src/test/resources/testResource.txt ================================================