From 3572d747d1cd84a2757c73b6e17d76f751a7d09f Mon Sep 17 00:00:00 2001 From: kvmw Date: Thu, 7 Nov 2024 10:29:40 +0100 Subject: [PATCH 1/2] Supports zone-configuratin for eureka Signed-off-by: kvmw --- .../EurekaBindingsPropertiesProcessor.java | 29 ++++++++++- ...EurekaBindingsPropertiesProcessorTest.java | 50 ++++++++++++++++++- 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/spring-cloud-bindings/src/main/java/org/springframework/cloud/bindings/boot/EurekaBindingsPropertiesProcessor.java b/spring-cloud-bindings/src/main/java/org/springframework/cloud/bindings/boot/EurekaBindingsPropertiesProcessor.java index 92e15ab..14d8bf0 100644 --- a/spring-cloud-bindings/src/main/java/org/springframework/cloud/bindings/boot/EurekaBindingsPropertiesProcessor.java +++ b/spring-cloud-bindings/src/main/java/org/springframework/cloud/bindings/boot/EurekaBindingsPropertiesProcessor.java @@ -22,6 +22,7 @@ import org.springframework.core.env.Environment; import org.springframework.cloud.bindings.boot.pem.PemSslStoreHelper; import org.springframework.util.StringUtils; +import java.net.URI; import java.nio.file.Path; import java.util.Map; @@ -51,14 +52,19 @@ final class EurekaBindingsPropertiesProcessor implements BindingsPropertiesProce map.from("uri").to("eureka.client.serviceUrl.defaultZone", (uri) -> String.format("%s/eureka/", uri) ); + map.from("uri").to("eureka.instance.metadata-map.zone", + this::hostnameFromUri + ); properties.put("eureka.client.region", "default"); + properties.put("spring.cloud.loadbalancer.configurations", "zone-preference"); + String caCert = secret.get("ca.crt"); if (caCert != null && !caCert.isEmpty()) { // generally apps using TLS bindings will be running in k8s where the host name is not meaningful, // but we don't want to override the endpoint behavior the app has already set, in case they want to // explicitly set eureka.instance.hostname to route traffic through normal ingress. - if (! environment.containsProperty("eureka.instance.preferIpAddress")) { + if (!environment.containsProperty("eureka.instance.preferIpAddress")) { properties.put("eureka.instance.preferIpAddress", true); } @@ -68,7 +74,7 @@ final class EurekaBindingsPropertiesProcessor implements BindingsPropertiesProce Path trustFilePath = PemSslStoreHelper.createKeyStoreFile("eureka-truststore", generatedPassword, caCert, null, "rootca"); properties.put("eureka.client.tls.enabled", true); - properties.put("eureka.client.tls.trust-store", "file:"+trustFilePath); + properties.put("eureka.client.tls.trust-store", "file:" + trustFilePath); properties.put("eureka.client.tls.trust-store-type", PemSslStoreHelper.PKCS12_STORY_TYPE); properties.put("eureka.client.tls.trust-store-password", generatedPassword); @@ -91,4 +97,23 @@ final class EurekaBindingsPropertiesProcessor implements BindingsPropertiesProce } }); } + + private String hostnameFromUri(String uri) { + if (!StringUtils.hasText(uri)) { + return ""; + } + + try { + URI u = URI.create(uri); + if (u.getHost() != null) { + return u.getHost(); + } + if (u.getScheme() == null) { + return URI.create("ignore://" + uri).getHost(); + } + } catch (IllegalArgumentException e) { + //ignore malformed uri + } + return ""; + } } diff --git a/spring-cloud-bindings/src/test/java/org/springframework/cloud/bindings/boot/EurekaBindingsPropertiesProcessorTest.java b/spring-cloud-bindings/src/test/java/org/springframework/cloud/bindings/boot/EurekaBindingsPropertiesProcessorTest.java index 67d94a4..604714d 100644 --- a/spring-cloud-bindings/src/test/java/org/springframework/cloud/bindings/boot/EurekaBindingsPropertiesProcessorTest.java +++ b/spring-cloud-bindings/src/test/java/org/springframework/cloud/bindings/boot/EurekaBindingsPropertiesProcessorTest.java @@ -40,7 +40,7 @@ final class EurekaBindingsPropertiesProcessorTest { new Binding("test-name", Paths.get("test-path"), new FluentMap() .withEntry(Binding.TYPE, TYPE) - .withEntry("uri", "test-uri") + .withEntry("uri", "https://test-uri") ) ); private final MockEnvironment environment = new MockEnvironment(); @@ -63,7 +63,9 @@ final class EurekaBindingsPropertiesProcessorTest { assertThat(properties) .containsEntry("eureka.client.region", "default") - .containsEntry("eureka.client.serviceUrl.defaultZone", "test-uri/eureka/") + .containsEntry("eureka.client.serviceUrl.defaultZone", "https://test-uri/eureka/") + .containsEntry("spring.cloud.loadbalancer.configurations", "zone-preference") + .containsEntry("eureka.instance.metadata-map.zone", "test-uri") .doesNotContainKey("eureka.client.oauth2.client-id") .doesNotContainKey("eureka.client.oauth2.access-token-uri") .doesNotContainKey("eureka.client.tls.trust-store") @@ -226,6 +228,7 @@ final class EurekaBindingsPropertiesProcessorTest { new EurekaBindingsPropertiesProcessor().process(environment, bindings, properties); }); } + @Test @DisplayName("throws when tls.crt is set but tls.key isn't") void testNoTlsKey() { @@ -243,6 +246,7 @@ final class EurekaBindingsPropertiesProcessorTest { new EurekaBindingsPropertiesProcessor().process(environment, bindings, properties); }); } + @Test @DisplayName("throws when tls.key is set but tls.crt isn't") void testNoTlsCrt() { @@ -261,6 +265,48 @@ final class EurekaBindingsPropertiesProcessorTest { }); } + @Test + @DisplayName("handles eureka zone for uri without scheme") + void zoneFromUriWithoutScheme() { + bindings = new Bindings( + new Binding("test-name", Paths.get("test-path"), + new FluentMap() + .withEntry(Binding.TYPE, TYPE) + .withEntry("uri", "test-uri") + ) + ); + new EurekaBindingsPropertiesProcessor().process(environment, bindings, properties); + + assertThat(properties) + .containsExactlyInAnyOrderEntriesOf(new FluentMap() + .withEntry("eureka.client.region", "default") + .withEntry("eureka.client.serviceUrl.defaultZone", "test-uri/eureka/") + .withEntry("spring.cloud.loadbalancer.configurations", "zone-preference") + .withEntry("eureka.instance.metadata-map.zone", "test-uri") + ); + } + + @Test + @DisplayName("handles eureka zone for malformed uri") + void zoneFromMalformedUri() { + bindings = new Bindings( + new Binding("test-name", Paths.get("test-path"), + new FluentMap() + .withEntry(Binding.TYPE, TYPE) + .withEntry("uri", "http:") + ) + ); + new EurekaBindingsPropertiesProcessor().process(environment, bindings, properties); + + assertThat(properties) + .containsExactlyInAnyOrderEntriesOf(new FluentMap() + .withEntry("eureka.client.region", "default") + .withEntry("eureka.client.serviceUrl.defaultZone", "http:/eureka/") + .withEntry("spring.cloud.loadbalancer.configurations", "zone-preference") + .withEntry("eureka.instance.metadata-map.zone", "") + ); + } + @Test @DisplayName("can be disabled") void disabled() { From 09451c11a4a588352bf0eb2bfbec791fa3b9eb95 Mon Sep 17 00:00:00 2001 From: kvmw Date: Fri, 8 Nov 2024 15:26:42 +0100 Subject: [PATCH 2/2] Detect runtime platform using CloudPlatform Signed-off-by: kvmw --- .../EurekaBindingsPropertiesProcessor.java | 13 ++++-- ...EurekaBindingsPropertiesProcessorTest.java | 44 ++++++++++++------- 2 files changed, 37 insertions(+), 20 deletions(-) diff --git a/spring-cloud-bindings/src/main/java/org/springframework/cloud/bindings/boot/EurekaBindingsPropertiesProcessor.java b/spring-cloud-bindings/src/main/java/org/springframework/cloud/bindings/boot/EurekaBindingsPropertiesProcessor.java index 14d8bf0..0d2e54c 100644 --- a/spring-cloud-bindings/src/main/java/org/springframework/cloud/bindings/boot/EurekaBindingsPropertiesProcessor.java +++ b/spring-cloud-bindings/src/main/java/org/springframework/cloud/bindings/boot/EurekaBindingsPropertiesProcessor.java @@ -16,6 +16,7 @@ package org.springframework.cloud.bindings.boot; +import org.springframework.boot.cloud.CloudPlatform; import org.springframework.cloud.bindings.Binding; import org.springframework.cloud.bindings.Bindings; import org.springframework.core.env.Environment; @@ -59,15 +60,17 @@ final class EurekaBindingsPropertiesProcessor implements BindingsPropertiesProce properties.put("spring.cloud.loadbalancer.configurations", "zone-preference"); - String caCert = secret.get("ca.crt"); - if (caCert != null && !caCert.isEmpty()) { - // generally apps using TLS bindings will be running in k8s where the host name is not meaningful, + if (isKubernetesPlatform(environment)) { + // generally for apps running in k8s hostname is not meaningful, // but we don't want to override the endpoint behavior the app has already set, in case they want to // explicitly set eureka.instance.hostname to route traffic through normal ingress. if (!environment.containsProperty("eureka.instance.preferIpAddress")) { properties.put("eureka.instance.preferIpAddress", true); } + } + String caCert = secret.get("ca.crt"); + if (caCert != null && !caCert.isEmpty()) { String generatedPassword = PemSslStoreHelper.generatePassword(); // Create a trust store from the CA cert @@ -98,6 +101,10 @@ final class EurekaBindingsPropertiesProcessor implements BindingsPropertiesProce }); } + private boolean isKubernetesPlatform(Environment environment) { + return CloudPlatform.KUBERNETES == CloudPlatform.getActive(environment); + } + private String hostnameFromUri(String uri) { if (!StringUtils.hasText(uri)) { return ""; diff --git a/spring-cloud-bindings/src/test/java/org/springframework/cloud/bindings/boot/EurekaBindingsPropertiesProcessorTest.java b/spring-cloud-bindings/src/test/java/org/springframework/cloud/bindings/boot/EurekaBindingsPropertiesProcessorTest.java index 604714d..ab40c03 100644 --- a/spring-cloud-bindings/src/test/java/org/springframework/cloud/bindings/boot/EurekaBindingsPropertiesProcessorTest.java +++ b/spring-cloud-bindings/src/test/java/org/springframework/cloud/bindings/boot/EurekaBindingsPropertiesProcessorTest.java @@ -62,21 +62,11 @@ final class EurekaBindingsPropertiesProcessorTest { new EurekaBindingsPropertiesProcessor().process(environment, bindings, properties); assertThat(properties) - .containsEntry("eureka.client.region", "default") - .containsEntry("eureka.client.serviceUrl.defaultZone", "https://test-uri/eureka/") - .containsEntry("spring.cloud.loadbalancer.configurations", "zone-preference") - .containsEntry("eureka.instance.metadata-map.zone", "test-uri") - .doesNotContainKey("eureka.client.oauth2.client-id") - .doesNotContainKey("eureka.client.oauth2.access-token-uri") - .doesNotContainKey("eureka.client.tls.trust-store") - .doesNotContainKey("eureka.client.tls.trust-store-type") - .doesNotContainKey("eureka.client.tls.trust-store-password") - .doesNotContainKey("eureka.client.tls.key-alias") - .doesNotContainKey("eureka.client.tls.key-store") - .doesNotContainKey("eureka.client.tls.key-store-type") - .doesNotContainKey("eureka.client.tls.key-store-password") - .doesNotContainKey("eureka.client.tls.key-password") - .doesNotContainKey("eureka.instance.preferIpAddress"); + .containsExactlyInAnyOrderEntriesOf(new FluentMap() + .withEntry("eureka.client.region", "default") + .withEntry("eureka.client.serviceUrl.defaultZone", "https://test-uri/eureka/") + .withEntry("spring.cloud.loadbalancer.configurations", "zone-preference") + .withEntry("eureka.instance.metadata-map.zone", "test-uri")); } @Test @@ -120,8 +110,7 @@ final class EurekaBindingsPropertiesProcessorTest { .containsEntry("eureka.client.region", "default") .containsKey("eureka.client.tls.trust-store") .containsEntry("eureka.client.tls.trust-store-type", "PKCS12") - .containsKey("eureka.client.tls.trust-store-password") - .containsEntry("eureka.instance.preferIpAddress", true); + .containsKey("eureka.client.tls.trust-store-password"); assertDoesNotThrow(() -> { String path = properties.get("eureka.client.tls.trust-store").toString().substring(5); File f = new File(path); @@ -307,6 +296,27 @@ final class EurekaBindingsPropertiesProcessorTest { ); } + @Test + @DisplayName("prefers ip address in kubernetes") + void preferIpAddressInKubernetes() { + environment.setProperty("spring.main.cloud-platform", "kubernetes"); + + new EurekaBindingsPropertiesProcessor().process(environment, bindings, properties); + + assertThat(properties).containsEntry("eureka.instance.preferIpAddress", true); + } + + @Test + @DisplayName("prefers ip address in kubernetes") + void doesNotOverridePreferIpAddressInKubernetes() { + environment.setProperty("eureka.instance.preferIpAddress", "false"); + environment.setProperty("spring.main.cloud-platform", "kubernetes"); + + new EurekaBindingsPropertiesProcessor().process(environment, bindings, properties); + + assertThat(properties).doesNotContainKey("eureka.instance.preferIpAddress"); + } + @Test @DisplayName("can be disabled") void disabled() {