diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/kubernetes/ProbesHealthContributorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/kubernetes/ProbesHealthContributorAutoConfiguration.java new file mode 100644 index 0000000000..82e014ae53 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/kubernetes/ProbesHealthContributorAutoConfiguration.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.kubernetes; + +import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator; +import org.springframework.boot.actuate.health.HealthEndpointGroupsRegistryCustomizer; +import org.springframework.boot.actuate.kubernetes.LivenessProbeHealthIndicator; +import org.springframework.boot.actuate.kubernetes.ProbesHealthEndpointGroupsRegistrar; +import org.springframework.boot.actuate.kubernetes.ReadinessProbeHealthIndicator; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform; +import org.springframework.boot.autoconfigure.kubernetes.ApplicationStateAutoConfiguration; +import org.springframework.boot.cloud.CloudPlatform; +import org.springframework.boot.kubernetes.ApplicationStateProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for + * {@link LivenessProbeHealthIndicator} and {@link ReadinessProbeHealthIndicator}. + * + * @author Brian Clozel + * @since 2.3.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) +@AutoConfigureAfter(ApplicationStateAutoConfiguration.class) +public class ProbesHealthContributorAutoConfiguration { + + @Bean + @ConditionalOnEnabledHealthIndicator("livenessProbe") + public LivenessProbeHealthIndicator livenessProbeHealthIndicator( + ApplicationStateProvider applicationStateProvider) { + return new LivenessProbeHealthIndicator(applicationStateProvider); + } + + @Bean + @ConditionalOnEnabledHealthIndicator("readinessProbe") + public ReadinessProbeHealthIndicator readinessProbeHealthIndicator( + ApplicationStateProvider applicationStateProvider) { + return new ReadinessProbeHealthIndicator(applicationStateProvider); + } + + @Bean + public HealthEndpointGroupsRegistryCustomizer probesRegistryCustomizer() { + return new ProbesHealthEndpointGroupsRegistrar(); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/kubernetes/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/kubernetes/package-info.java new file mode 100644 index 0000000000..cab34f7e47 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/kubernetes/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Auto-configuration for actuator kubernetes concerns. + */ +package org.springframework.boot.actuate.autoconfigure.kubernetes; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories index c14ab11434..7c21d9b101 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories @@ -29,6 +29,7 @@ org.springframework.boot.actuate.autoconfigure.integration.IntegrationGraphEndpo org.springframework.boot.actuate.autoconfigure.jdbc.DataSourceHealthContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.jms.JmsHealthContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.jolokia.JolokiaEndpointAutoConfiguration,\ +org.springframework.boot.actuate.autoconfigure.kubernetes.ProbesHealthContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.ldap.LdapHealthContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.liquibase.LiquibaseEndpointAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.logging.LogFileWebEndpointAutoConfiguration,\ diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/kubernetes/LivenessProbeHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/kubernetes/LivenessProbeHealthIndicator.java new file mode 100644 index 0000000000..77a7da544c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/kubernetes/LivenessProbeHealthIndicator.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.kubernetes; + +import org.springframework.boot.actuate.health.AbstractHealthIndicator; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.kubernetes.ApplicationStateProvider; +import org.springframework.boot.kubernetes.LivenessState; + +/** + * A {@link HealthIndicator} that checks the {@link LivenessState} of the application. + * + * @author Brian Clozel + * @since 2.3.0 + */ +public class LivenessProbeHealthIndicator extends AbstractHealthIndicator { + + private final ApplicationStateProvider applicationStateProvider; + + public LivenessProbeHealthIndicator(ApplicationStateProvider applicationStateProvider) { + this.applicationStateProvider = applicationStateProvider; + } + + @Override + protected void doHealthCheck(Health.Builder builder) throws Exception { + if (LivenessState.live().equals(this.applicationStateProvider.getLivenessState())) { + builder.up(); + } + else { + builder.down(); + } + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/kubernetes/ProbesHealthEndpointGroupsRegistrar.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/kubernetes/ProbesHealthEndpointGroupsRegistrar.java new file mode 100644 index 0000000000..04ce82781a --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/kubernetes/ProbesHealthEndpointGroupsRegistrar.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.kubernetes; + +import org.springframework.boot.actuate.health.HealthEndpointGroup; +import org.springframework.boot.actuate.health.HealthEndpointGroupsRegistry; +import org.springframework.boot.actuate.health.HealthEndpointGroupsRegistryCustomizer; + +/** + * {@link HealthEndpointGroupsRegistryCustomizer} that registers {@code "liveness"} and + * {@code "readiness"} {@link HealthEndpointGroup groups} if they don't exist already. + * + * @author Brian Clozel + * @since 2.3.0 + */ +public class ProbesHealthEndpointGroupsRegistrar implements HealthEndpointGroupsRegistryCustomizer { + + private static final String LIVENESS_GROUP_NAME = "liveness"; + + private static final String READINESS_GROUP_NAME = "readiness"; + + private static final String LIVENESS_PROBE_INDICATOR = "livenessProbe"; + + private static final String READINESS_PROBE_INDICATOR = "readinessProbe"; + + @Override + public void customize(HealthEndpointGroupsRegistry registry) { + if (registry.get(LIVENESS_GROUP_NAME) == null) { + registry.add(LIVENESS_GROUP_NAME, (configurer) -> configurer.include(LIVENESS_PROBE_INDICATOR)); + } + if (registry.get(READINESS_GROUP_NAME) == null) { + registry.add(READINESS_GROUP_NAME, (configurer) -> configurer.include(READINESS_PROBE_INDICATOR)); + } + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/kubernetes/ReadinessProbeHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/kubernetes/ReadinessProbeHealthIndicator.java new file mode 100644 index 0000000000..3af8598083 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/kubernetes/ReadinessProbeHealthIndicator.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.kubernetes; + +import org.springframework.boot.actuate.health.AbstractHealthIndicator; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.kubernetes.ApplicationStateProvider; +import org.springframework.boot.kubernetes.ReadinessState; + +/** + * A {@link HealthIndicator} that checks the {@link ReadinessState} of the application. + * + * @author Brian Clozel + * @since 2.3.0 + */ +public class ReadinessProbeHealthIndicator extends AbstractHealthIndicator { + + private final ApplicationStateProvider applicationStateProvider; + + public ReadinessProbeHealthIndicator(ApplicationStateProvider applicationStateProvider) { + this.applicationStateProvider = applicationStateProvider; + } + + @Override + protected void doHealthCheck(Health.Builder builder) throws Exception { + if (ReadinessState.ready().equals(this.applicationStateProvider.getReadinessState())) { + builder.up(); + } + else { + builder.outOfService(); + } + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/kubernetes/package-info.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/kubernetes/package-info.java new file mode 100644 index 0000000000..b0a7397531 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/kubernetes/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Actuator support for kubernetes-related concerns. + */ +package org.springframework.boot.actuate.kubernetes; diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/TestHealthEndpointGroupsRegistry.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/TestHealthEndpointGroupsRegistry.java new file mode 100644 index 0000000000..30db51a576 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/TestHealthEndpointGroupsRegistry.java @@ -0,0 +1,124 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.health; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * Test implementation for {@link HealthEndpointGroupsRegistry} + * + * @author Brian Clozel + */ +public class TestHealthEndpointGroupsRegistry implements HealthEndpointGroupsRegistry { + + private TestHealthEndpointGroup primary = new TestHealthEndpointGroup(); + + private Map groups = new HashMap<>(); + + @Override + public HealthEndpointGroupsRegistry add(String groupName, Consumer builder) { + TestHealthEndpointGroupConfigurer configurer = new TestHealthEndpointGroupConfigurer(); + builder.accept(configurer); + this.groups.put(groupName, configurer.toHealthEndpointGroup()); + return this; + } + + @Override + public HealthEndpointGroupsRegistry remove(String groupName) { + this.groups.remove(groupName); + return this; + } + + @Override + public HealthEndpointGroups toGroups() { + return this; + } + + @Override + public HealthEndpointGroup getPrimary() { + return this.primary; + } + + @Override + public Set getNames() { + return this.groups.keySet(); + } + + @Override + public HealthEndpointGroup get(String name) { + return this.groups.get(name); + } + + class TestHealthEndpointGroupConfigurer implements HealthEndpointGroupConfigurer { + + private Predicate predicate = (name) -> true; + + @Override + public HealthEndpointGroupConfigurer include(String... indicators) { + Predicate included = Arrays.asList(indicators).stream() + .map((group) -> (Predicate) (s) -> s.equals(group)).reduce(Predicate::or) + .orElse((s) -> true); + this.predicate = this.predicate.and(included); + return this; + } + + @Override + public HealthEndpointGroupConfigurer exclude(String... indicators) { + Predicate excluded = Arrays.asList(indicators).stream() + .map((group) -> (Predicate) (s) -> !s.equals(group)).reduce(Predicate::or) + .orElse((s) -> true); + this.predicate = this.predicate.and(excluded); + return this; + } + + @Override + public HealthEndpointGroupConfigurer statusAggregator(StatusAggregator statusAggregator) { + throw new UnsupportedOperationException(); + } + + @Override + public HealthEndpointGroupConfigurer httpCodeStatusMapper(HttpCodeStatusMapper httpCodeStatusMapper) { + throw new UnsupportedOperationException(); + } + + @Override + public HealthEndpointGroupConfigurer showComponents(HealthEndpointGroup.Show showComponents) { + throw new UnsupportedOperationException(); + } + + @Override + public HealthEndpointGroupConfigurer showDetails(HealthEndpointGroup.Show showDetails) { + throw new UnsupportedOperationException(); + } + + @Override + public HealthEndpointGroupConfigurer roles(String... roles) { + throw new UnsupportedOperationException(); + } + + TestHealthEndpointGroup toHealthEndpointGroup() { + return new TestHealthEndpointGroup(this.predicate); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/kubernetes/LivenessProbeHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/kubernetes/LivenessProbeHealthIndicatorTests.java new file mode 100644 index 0000000000..96086eb1e1 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/kubernetes/LivenessProbeHealthIndicatorTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.kubernetes; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.health.Status; +import org.springframework.boot.kubernetes.ApplicationStateProvider; +import org.springframework.boot.kubernetes.LivenessState; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.when; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link LivenessProbeHealthIndicator} + * + * @author Brian Clozel + */ +class LivenessProbeHealthIndicatorTests { + + private ApplicationStateProvider stateProvider; + + private LivenessProbeHealthIndicator healthIndicator; + + @BeforeEach + void setUp() { + this.stateProvider = mock(ApplicationStateProvider.class); + this.healthIndicator = new LivenessProbeHealthIndicator(this.stateProvider); + } + + @Test + void livenessIsLive() { + when(this.stateProvider.getLivenessState()).thenReturn(LivenessState.live()); + assertThat(this.healthIndicator.health().getStatus()).isEqualTo(Status.UP); + } + + @Test + void livenessIsBroken() { + when(this.stateProvider.getLivenessState()).thenReturn(LivenessState.broken()); + assertThat(this.healthIndicator.health().getStatus()).isEqualTo(Status.DOWN); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/kubernetes/ProbesHealthEndpointGroupsRegistrarTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/kubernetes/ProbesHealthEndpointGroupsRegistrarTests.java new file mode 100644 index 0000000000..9059fe58c1 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/kubernetes/ProbesHealthEndpointGroupsRegistrarTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.kubernetes; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.health.HealthEndpointGroup; +import org.springframework.boot.actuate.health.HealthEndpointGroupsRegistry; +import org.springframework.boot.actuate.health.TestHealthEndpointGroupsRegistry; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@code ProbesHealthEndpointGroupsRegistrar } + * + * @author Brian Clozel + */ +class ProbesHealthEndpointGroupsRegistrarTests { + + private ProbesHealthEndpointGroupsRegistrar registrar = new ProbesHealthEndpointGroupsRegistrar(); + + @Test + void shouldAddKubernetesProbes() { + HealthEndpointGroupsRegistry registry = new TestHealthEndpointGroupsRegistry(); + this.registrar.customize(registry); + HealthEndpointGroup liveness = registry.get("liveness"); + assertThat(liveness).isNotNull(); + assertThat(liveness.isMember("livenessProbe")).isTrue(); + HealthEndpointGroup readiness = registry.get("readiness"); + assertThat(readiness).isNotNull(); + assertThat(readiness.isMember("readinessProbe")).isTrue(); + } + + @Test + void shouldNotAddProbeIfGroupExists() { + HealthEndpointGroupsRegistry registry = new TestHealthEndpointGroupsRegistry(); + registry.add("readiness", (configurer) -> configurer.include("test")); + this.registrar.customize(registry); + + HealthEndpointGroup liveness = registry.get("liveness"); + assertThat(liveness).isNotNull(); + assertThat(liveness.isMember("livenessProbe")).isTrue(); + + HealthEndpointGroup readiness = registry.get("readiness"); + assertThat(readiness).isNotNull(); + assertThat(readiness.isMember("readinessProbe")).isFalse(); + assertThat(readiness.isMember("test")).isTrue(); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/kubernetes/ReadinessProbeHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/kubernetes/ReadinessProbeHealthIndicatorTests.java new file mode 100644 index 0000000000..43d11add83 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/kubernetes/ReadinessProbeHealthIndicatorTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.kubernetes; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.health.Status; +import org.springframework.boot.kubernetes.ApplicationStateProvider; +import org.springframework.boot.kubernetes.ReadinessState; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.when; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link ReadinessProbeHealthIndicator} + * + * @author Brian Clozel + */ +class ReadinessProbeHealthIndicatorTests { + + private ApplicationStateProvider stateProvider; + + private ReadinessProbeHealthIndicator healthIndicator; + + @BeforeEach + void setUp() { + this.stateProvider = mock(ApplicationStateProvider.class); + this.healthIndicator = new ReadinessProbeHealthIndicator(this.stateProvider); + } + + @Test + void readinessIsReady() { + when(this.stateProvider.getReadinessState()).thenReturn(ReadinessState.ready()); + assertThat(this.healthIndicator.health().getStatus()).isEqualTo(Status.UP); + } + + @Test + void readinessIsBusy() { + when(this.stateProvider.getReadinessState()).thenReturn(ReadinessState.busy()); + assertThat(this.healthIndicator.health().getStatus()).isEqualTo(Status.OUT_OF_SERVICE); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kubernetes/ApplicationStateAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kubernetes/ApplicationStateAutoConfiguration.java new file mode 100644 index 0000000000..1fa4d8f3d6 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kubernetes/ApplicationStateAutoConfiguration.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.kubernetes; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform; +import org.springframework.boot.cloud.CloudPlatform; +import org.springframework.boot.kubernetes.ApplicationStateProvider; +import org.springframework.boot.kubernetes.SpringApplicationEventListener; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration} for + * {@link ApplicationStateProvider}. + * + * @author Brian Clozel + * @since 2.3.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) +public class ApplicationStateAutoConfiguration { + + @Bean + public ApplicationStateProvider applicationStateProvider() { + return new ApplicationStateProvider(); + } + + @Bean + public SpringApplicationEventListener springApplicationEventListener() { + return new SpringApplicationEventListener(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kubernetes/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kubernetes/package-info.java new file mode 100644 index 0000000000..bf6feee5dd --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kubernetes/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration for Kubernetes application features. + */ +package org.springframework.boot.autoconfigure.kubernetes; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index dfd9aeacfa..00b9500ff1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -63,6 +63,7 @@ org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration,\ org.springframework.boot.autoconfigure.elasticsearch.rest.RestClientAutoConfiguration,\ org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration,\ org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration,\ +org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration,\ org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration,\ org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration,\ org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration,\ @@ -84,11 +85,11 @@ org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration,\ org.springframework.boot.autoconfigure.jms.JndiConnectionFactoryAutoConfiguration,\ org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration,\ org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration,\ -org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration,\ org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration,\ org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration,\ org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration,\ org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration,\ +org.springframework.boot.autoconfigure.kubernetes.ApplicationStateAutoConfiguration,\ org.springframework.boot.autoconfigure.ldap.embedded.EmbeddedLdapAutoConfiguration,\ org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration,\ org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration,\ diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kubernetes/ApplicationStateAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kubernetes/ApplicationStateAutoConfigurationTests.java new file mode 100644 index 0000000000..5934efdccf --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kubernetes/ApplicationStateAutoConfigurationTests.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.kubernetes; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.kubernetes.ApplicationStateProvider; +import org.springframework.boot.kubernetes.SpringApplicationEventListener; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ApplicationStateAutoConfiguration} + * + * @author Brian Clozel + */ +class ApplicationStateAutoConfigurationTests { + + private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ApplicationStateAutoConfiguration.class)); + + @Test + void disabledWhenNotDeployedOnKubernetes() { + this.contextRunner.run(((context) -> assertThat(context).doesNotHaveBean(ApplicationStateProvider.class) + .doesNotHaveBean(SpringApplicationEventListener.class))); + } + + @Test + void enabledWhenDeployedOnKubernetes() { + this.contextRunner.withPropertyValues("spring.main.cloud-platform:kubernetes") + .run(((context) -> assertThat(context).hasSingleBean(ApplicationStateProvider.class) + .hasSingleBean(SpringApplicationEventListener.class))); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment.adoc index b62dccbe39..9f5ca67f6c 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment.adoc @@ -181,6 +181,11 @@ See the {spring-boot-module-api}/cloud/CloudFoundryVcapEnvironmentPostProcessor. TIP: The https://github.com/pivotal-cf/java-cfenv/[Java CFEnv] project is a better fit for tasks such as configuring a DataSource. +[[cloud-deployment-kubernetes]] +=== Kubernetes +Spring Boot auto-detects Kubernetes deployment environments by checking the environment for `"*_SERVICE_HOST"` and `"*_SERVICE_PORT"` variables. + +Spring Boot helps you to <> and export it with <>. [[cloud-deployment-heroku]] @@ -254,10 +259,7 @@ For more details, refer to https://devcenter.heroku.com/articles/deploying-sprin [[cloud-deployment-openshift]] === OpenShift -https://www.openshift.com/[OpenShift] is the Red Hat public (and enterprise) extension of the Kubernetes container orchestration platform. -Similarly to Kubernetes, OpenShift has many options for installing Spring Boot based applications. - -OpenShift has many resources describing how to deploy Spring Boot applications, including: +https://www.openshift.com/[OpenShift] has many resources describing how to deploy Spring Boot applications, including: * https://blog.openshift.com/using-openshift-enterprise-grade-spring-boot-deployments/[Using the S2I builder] * https://access.redhat.com/documentation/en-us/reference_architectures/2017/html-single/spring_boot_microservices_on_red_hat_openshift_container_platform_3/[Architecture guide] diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc index 664d8a9626..6e37e6a5fe 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc @@ -647,7 +647,7 @@ If no `HealthIndicator` returns a status that is known to the `StatusAggregator` TIP: The `HealthContributorRegistry` can be used to register and unregister health indicators at runtime. - +[[production-ready-health-indicators]] ==== Auto-configured HealthIndicators The following `HealthIndicators` are auto-configured by Spring Boot when appropriate: @@ -679,6 +679,12 @@ The following `HealthIndicators` are auto-configured by Spring Boot when appropr | {spring-boot-actuator-module-code}/jms/JmsHealthIndicator.java[`JmsHealthIndicator`] | Checks that a JMS broker is up. +| {spring-boot-actuator-module-code}/kubernetes/LivenessProbeHealthIndicator.java[`LivenessProbeHealthIndicator`] +| Exposes the "Liveness" application state. + +| {spring-boot-actuator-module-code}/kubernetes/ReadinessProbeHealthIndicator.java[`ReadinessProbeHealthIndicator`] +| Exposes the "Readiness" application state. + | {spring-boot-actuator-module-code}/ldap/LdapHealthIndicator.java[`LdapHealthIndicator`] | Checks that an LDAP server is up. @@ -835,10 +841,9 @@ TIP: If necessary, reactive indicators replace the regular ones. Also, any `HealthIndicator` that is not handled explicitly is wrapped automatically. - +[[production-ready-health-groups]] ==== Health Groups It's sometimes useful to organize health indicators into groups that can be used for different purposes. -For example, if you deploy your application to Kubernetes, you may want one different sets of health indicators for your "`liveness`" and "`readiness`" probes. To create a health indicator group you can use the `management.endpoint.health.group.` property and specify a list of health indicator IDs to `include` or `exclude`. For example, to create a group that includes only database indicators you can define the following: @@ -865,6 +870,35 @@ It's also possible to override the `show-details` and `roles` properties if requ TIP: You can use `@Qualifier("groupname")` if you need to register custom `StatusAggregator` or `HttpCodeStatusMapper` beans for use with the group. +[[production-ready-kubernetes-probes]] +=== Kubernetes Probes +Applications deployed on Kubernetes can provide information about their internal state with https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-probes[Container Probes]. +Depending on https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/[your Kubernetes configuration], the kubelet will call those probes and react to the result. + +When deployed in a Kubernetes environment, Spring Boot will manage your <>. +Spring Boot Actuator will gather the "Liveness" and "Readiness" information using <> +and expose them as HTTP Probes using <>. +You can then point your Kubernetes `"livenessProbe"` to `"/actuator/health/liveness"` and `"readinessProbe"` to `"/actuator/health/readiness"`. + +If your application context startup can be longer than the configured `"livenessProbe"` timeout, configuring a `"startupProbe"` can solve startup issues. + +TIP: Application should perform long-running startup tasks with `ApplicationRunners`; in this case, you don't necessarily need a `"startupProbe"` as such tasks are performed after the "Liveness" state is marked as successful. +The application won't receive traffic until tasks are finished - only then the `"readinessProbe"` will be successful. + +As explained in the <>, depending on external systems for defining the "Liveness" state might result in cascading failures. +In case your application needs to rely on a specific subset of health checks for "Liveness" or "Readiness", you should consider configuring <> manually: + +[source,properties,indent=0,configprops] +---- + management.endpoint.health.group.readiness.include=readinessProbe,customCheck +---- + +WARNING: If your Actuator endpoints are deployed on a separate management context, be aware that endpoints are then not using the same web infrastructure (port, connection pools, framework components) as the main application. +In this case, a Probe check could be successful even if the main application does not work properly (for example, it cannot accept new connections). + +You'll find more information in the <> section. + + [[production-ready-application-info]] === Application Information diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc index 5f83001ea2..28c33973b4 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc @@ -6013,6 +6013,90 @@ For instance, it is possible to customize the name of the table for the JDBC sto For setting the timeout of the session you can use the configprop:spring.session.timeout[] property. If that property is not set, the auto-configuration falls back to the value of configprop:server.servlet.session.timeout[]. +[[boot-features-kubernetes-application-state]] +== Kubernetes Application State +When deployed on Kubernetes, applications can provide information about their state to the platform using https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/[Kubernetes Probes]. +Spring Boot manages this application state with the `ApplicationStateProvider` and makes it available to application components and the cloud platform itself. +This feature is only enabled when the application is running on Kubernetes. + +TIP: The cloud platform detection relies on platform-specific environment variables, but you can override this detection with the configprop:spring.main.cloud-platform[] configuration property. + +[[boot-features-kubernetes-liveness-state]] +=== Liveness State +The "Liveness" state of an application tells whether its internal state allows it to work correctly, or recover by itself if it's currently failing. +An invalid "Liveness" state means that the application is in a broken state and cannot recover from it; the infrastructure should then restart the application to mitigate that problem. + +NOTE: In general, the "Liveness" state should not be based external checks, such as <>. +If it did, a failing external system (a database, a Web API, an external cache) would trigger massive restarts and cascading failures across the platform. + +The internal state of Spring Boot applications is mostly represented by the Spring application context. +If the application context has started successfully, Spring Boot assumes that the application is in a valid state. +An application is considered live as soon as the `ApplicationStartedEvent` has been published, see <>. + +[[boot-features-kubernetes-readiness-state]] +=== Readiness State +The "Readiness" state of an application tells whether the application is ready to handle traffic. +A failing "Readiness" state tells Kubernetes that it should not route traffic to the application for now. +This typically happens during startup, while `CommandLineRunner` and `ApplicationRunner` components are being processed, or at any time if the application decides that it's too busy for additional traffic. + +An application is considered live as soon as the `ApplicationReadyEvent` has been published, see <>. + +TIP: Tasks expected to run during startup should be executed by `CommandLineRunner` and `ApplicationRunner` components instead of using Spring component lifecycle callbacks such as `@PostConstruct`. + +[[boot-features-kubernetes-managing-state]] +=== Managing Kubernetes Application State +Application components can retrieve the current application state at any time, by injecting the `ApplicationStateProvider` and calling methods on it. +More often, applications will want to listen to state updates or update the state of the application. + +For example, we can export the "Readiness" state of the application to a file so that a Kubernetes "exec Probe" can look at this file: + +[source,java,indent=0] +---- + @Component + public class LivenessStateExporter implements ApplicationListener { + + @Override + public void onApplicationEvent(LivenessStateChangedEvent event) { + switch (event.getLivenessState().getStatus()) { + case LIVE: + // create file /tmp/healthy + break; + case BROKEN: + // remove file /tmp/healthy + break; + } + } + + } +---- + +We can also update the state of the application, when the application breaks and cannot recover: + +[source,java,indent=0] +---- + @Component + public class LocalCacheVerifier { + + private final ApplicationEventPublisher eventPublisher; + + public LocalCacheVerifier(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + + public void checkLocalCache() { + try { + //... + } + catch (CacheCompletelyBroken ex) { + this.eventPublisher.publishEvent(LivenessStateChangedEvent.broken(ex)); + } + } + + } +---- + +Spring Boot provides <>. +You can get more guidance about <>. [[boot-features-jmx]] diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/ApplicationStateProvider.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/ApplicationStateProvider.java new file mode 100644 index 0000000000..9e08413c7c --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/ApplicationStateProvider.java @@ -0,0 +1,75 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.kubernetes; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; + +/** + * Holds the application state, when deployed in a Kubernetes environment. + *

+ * Other application components can get the current state information from the + * {@code ApplicationStateProvider}, or publish application evens such as + * {@link ReadinessStateChangedEvent} and {@link LivenessStateChangedEvent} to update the + * state of the application. + * + * @author Brian Clozel + * @since 2.3.0 + */ +public class ApplicationStateProvider implements ApplicationListener { + + private static final Log logger = LogFactory.getLog(ApplicationStateProvider.class); + + private LivenessState livenessState; + + private ReadinessState readinessState; + + public ApplicationStateProvider() { + this(LivenessState.broken(), ReadinessState.busy()); + } + + public ApplicationStateProvider(LivenessState livenessState, ReadinessState readinessState) { + this.livenessState = livenessState; + this.readinessState = readinessState; + } + + public LivenessState getLivenessState() { + return this.livenessState; + } + + public ReadinessState getReadinessState() { + return this.readinessState; + } + + @Override + public void onApplicationEvent(ApplicationEvent event) { + if (event instanceof LivenessStateChangedEvent) { + LivenessStateChangedEvent livenessEvent = (LivenessStateChangedEvent) event; + this.livenessState = livenessEvent.getLivenessState(); + logger.info("Application State is now " + this.livenessState.toString()); + } + else if (event instanceof ReadinessStateChangedEvent) { + ReadinessStateChangedEvent readinessEvent = (ReadinessStateChangedEvent) event; + this.readinessState = readinessEvent.getReadinessState(); + logger.info("Application State is now " + this.readinessState.toString()); + } + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/LivenessState.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/LivenessState.java new file mode 100644 index 0000000000..8545e9114b --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/LivenessState.java @@ -0,0 +1,86 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.kubernetes; + +import java.util.Objects; + +/** + * "Liveness" state of the application, when deployed on Kubernetes. + *

+ * An application is considered live when it's running with a correct internal state. + * "Liveness" failure means that the internal state of the application is broken and we + * cannot recover from it. As a result, the platform should restart the application. + * + * @author Brian Clozel + * @since 2.3.0 + */ +public final class LivenessState { + + private final Status status; + + LivenessState(Status status) { + this.status = status; + } + + public Status getStatus() { + return this.status; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + LivenessState that = (LivenessState) o; + return this.status == that.status; + } + + @Override + public int hashCode() { + return Objects.hash(this.status); + } + + @Override + public String toString() { + return "LivenessState{" + "status=" + this.status + '}'; + } + + public static LivenessState broken() { + return new LivenessState(Status.BROKEN); + } + + public static LivenessState live() { + return new LivenessState(Status.LIVE); + } + + public enum Status { + + /** + * The application is running and its internal state is correct. + */ + LIVE, + /** + * The internal state of the application is broken. + */ + BROKEN + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/LivenessStateChangedEvent.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/LivenessStateChangedEvent.java new file mode 100644 index 0000000000..c2f5ae7dae --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/LivenessStateChangedEvent.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.kubernetes; + +import org.springframework.context.ApplicationEvent; + +/** + * {@link ApplicationEvent} sent when the {@link LivenessState} of the application + * changes. + *

+ * Any application component can send such events to update the state of the application. + * + * @author Brian Clozel + * @since 2.3.0 + */ +public class LivenessStateChangedEvent extends ApplicationEvent { + + private final String cause; + + LivenessStateChangedEvent(LivenessState state, String cause) { + super(state); + this.cause = cause; + } + + public LivenessState getLivenessState() { + return (LivenessState) getSource(); + } + + /** + * Create a new {@code ApplicationEvent} signaling that the {@link LivenessState} is + * broken. + * @param cause the cause of the broken internal state of the application + * @return the application event + */ + public static LivenessStateChangedEvent broken(String cause) { + return new LivenessStateChangedEvent(LivenessState.broken(), cause); + } + + /** + * Create a new {@code ApplicationEvent} signaling that the {@link LivenessState} is + * broken. + * @param throwable the exception that caused the broken internal state of the + * application + * @return the application event + */ + public static LivenessStateChangedEvent broken(Throwable throwable) { + return new LivenessStateChangedEvent(LivenessState.broken(), throwable.getMessage()); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/ReadinessState.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/ReadinessState.java new file mode 100644 index 0000000000..aedca872bc --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/ReadinessState.java @@ -0,0 +1,86 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.kubernetes; + +import java.util.Objects; + +/** + * "Readiness" state of the application, when deployed on Kubernetes. + *

+ * An application is considered ready when it's {@link LivenessState live} and willing to + * accept traffic. "Readiness" failure means that the application is not able to accept + * traffic and that the infrastructure should stop routing requests to it. + * + * @author Brian Clozel + * @since 2.3.0 + */ +public final class ReadinessState { + + private final Availability availability; + + private ReadinessState(Availability availability) { + this.availability = availability; + } + + public Availability getAvailability() { + return this.availability; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ReadinessState that = (ReadinessState) o; + return this.availability == that.availability; + } + + @Override + public int hashCode() { + return Objects.hash(this.availability); + } + + @Override + public String toString() { + return "ReadinessState{" + "availability=" + this.availability + '}'; + } + + public static ReadinessState ready() { + return new ReadinessState(Availability.READY); + } + + public static ReadinessState busy() { + return new ReadinessState(Availability.BUSY); + } + + public enum Availability { + + /** + * The application is not willing to receive traffic. + */ + BUSY, + /** + * The application is ready to receive traffic. + */ + READY + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/ReadinessStateChangedEvent.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/ReadinessStateChangedEvent.java new file mode 100644 index 0000000000..83b4bc108d --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/ReadinessStateChangedEvent.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.kubernetes; + +import org.springframework.context.ApplicationEvent; + +/** + * {@link ApplicationEvent} sent when the {@link ReadinessState} of the application + * changes. + *

+ * Any application component can send such events to update the state of the application. + * + * @author Brian Clozel + * @since 2.3.0 + */ +public class ReadinessStateChangedEvent extends ApplicationEvent { + + ReadinessStateChangedEvent(ReadinessState state) { + super(state); + } + + public ReadinessState getReadinessState() { + return (ReadinessState) getSource(); + } + + /** + * Create a new {@code ApplicationEvent} signaling that the {@link ReadinessState} is + * ready. + * @return the application event + */ + public static ReadinessStateChangedEvent ready() { + return new ReadinessStateChangedEvent(ReadinessState.ready()); + } + + /** + * Create a new {@code ApplicationEvent} signaling that the {@link ReadinessState} is + * busy. + * @return the application event + */ + public static ReadinessStateChangedEvent busy() { + return new ReadinessStateChangedEvent(ReadinessState.busy()); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/SpringApplicationEventListener.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/SpringApplicationEventListener.java new file mode 100644 index 0000000000..1fbc5bfc39 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/SpringApplicationEventListener.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.kubernetes; + +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextClosedEvent; + +/** + * {@link ApplicationListener} that listens for application lifecycle events such as + * {@link ApplicationStartedEvent}, {@link ApplicationReadyEvent}, + * {@link ContextClosedEvent}. Those events are then translated and published into events + * consumed by {@link ApplicationStateProvider} to update the application state. + * + * @author Brian Clozel + * @since 2.3.0 + */ +public class SpringApplicationEventListener + implements ApplicationListener, ApplicationEventPublisherAware { + + private ApplicationEventPublisher applicationEventPublisher; + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.applicationEventPublisher = applicationEventPublisher; + } + + @Override + public void onApplicationEvent(ApplicationEvent event) { + if (event instanceof ApplicationStartedEvent) { + LivenessState livenessState = LivenessState.live(); + this.applicationEventPublisher + .publishEvent(new LivenessStateChangedEvent(livenessState, "Application has started")); + } + else if (event instanceof ApplicationReadyEvent) { + this.applicationEventPublisher.publishEvent(ReadinessStateChangedEvent.ready()); + } + else if (event instanceof ContextClosedEvent) { + this.applicationEventPublisher.publishEvent(ReadinessStateChangedEvent.busy()); + } + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/package-info.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/package-info.java new file mode 100644 index 0000000000..43205acafa --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/kubernetes/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Support for Kubernetes deployment environments. + */ +package org.springframework.boot.kubernetes; diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/kubernetes/ApplicationStateProviderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/kubernetes/ApplicationStateProviderTests.java new file mode 100644 index 0000000000..4edd2e4574 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/kubernetes/ApplicationStateProviderTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.kubernetes; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ApplicationStateProvider} + * + * @author Brian Clozel + */ +class ApplicationStateProviderTests { + + @Test + void initialStateShouldBeFailures() { + ApplicationStateProvider stateProvider = new ApplicationStateProvider(); + assertThat(stateProvider.getLivenessState()).isEqualTo(LivenessState.broken()); + assertThat(stateProvider.getReadinessState()).isEqualTo(ReadinessState.busy()); + } + + @Test + void updateLivenessState() { + ApplicationStateProvider stateProvider = new ApplicationStateProvider(); + LivenessState livenessState = LivenessState.live(); + stateProvider.onApplicationEvent(new LivenessStateChangedEvent(livenessState, "Startup complete")); + assertThat(stateProvider.getLivenessState()).isEqualTo(livenessState); + } + + @Test + void updateReadiessState() { + ApplicationStateProvider stateProvider = new ApplicationStateProvider(); + stateProvider.onApplicationEvent(ReadinessStateChangedEvent.ready()); + assertThat(stateProvider.getReadinessState()).isEqualTo(ReadinessState.ready()); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/kubernetes/SpringApplicationEventListenerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/kubernetes/SpringApplicationEventListenerTests.java new file mode 100644 index 0000000000..3ce5b1c5de --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/kubernetes/SpringApplicationEventListenerTests.java @@ -0,0 +1,83 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.kubernetes; + +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.context.support.StaticApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link SpringApplicationEventListener} + * + * @author Brian Clozel + */ +class SpringApplicationEventListenerTests { + + @Test + void shouldReactToApplicationStartedEvent() { + ApplicationEvent event = publishAndReceiveApplicationEvent( + new ApplicationStartedEvent(new SpringApplication(), null, null)); + + assertThat(event).isInstanceOf(LivenessStateChangedEvent.class); + LivenessStateChangedEvent livenessEvent = (LivenessStateChangedEvent) event; + assertThat(livenessEvent.getLivenessState()).isEqualTo(LivenessState.live()); + } + + @Test + void shouldReactToApplicationReadyEvent() { + ApplicationEvent event = publishAndReceiveApplicationEvent( + new ApplicationReadyEvent(new SpringApplication(), null, null)); + + assertThat(event).isInstanceOf(ReadinessStateChangedEvent.class); + ReadinessStateChangedEvent readinessEvent = (ReadinessStateChangedEvent) event; + assertThat(readinessEvent.getReadinessState()).isEqualTo(ReadinessState.ready()); + } + + @Test + void shouldReactToContextClosedEvent() { + ApplicationEvent event = publishAndReceiveApplicationEvent( + new ContextClosedEvent(new StaticApplicationContext())); + + assertThat(event).isInstanceOf(ReadinessStateChangedEvent.class); + ReadinessStateChangedEvent readinessEvent = (ReadinessStateChangedEvent) event; + assertThat(readinessEvent.getReadinessState()).isEqualTo(ReadinessState.busy()); + } + + private ApplicationEvent publishAndReceiveApplicationEvent(ApplicationEvent eventToSend) { + ApplicationEventPublisher eventPublisher = mock(ApplicationEventPublisher.class); + SpringApplicationEventListener eventListener = new SpringApplicationEventListener(); + eventListener.setApplicationEventPublisher(eventPublisher); + + eventListener.onApplicationEvent(eventToSend); + ArgumentCaptor event = ArgumentCaptor.forClass(ApplicationEvent.class); + verify(eventPublisher).publishEvent(event.capture()); + + return event.getValue(); + } + +}