Allow to configure elasticsearch service with only a URI

Closes gh-789
This commit is contained in:
Stephane Nicoll 2019-01-03 13:52:35 +01:00
parent 3b88b952a0
commit b9000f322d
8 changed files with 133 additions and 74 deletions

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2018 the original author or authors. * Copyright 2012-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,9 +16,13 @@
package io.spring.initializr.actuate.stat; package io.spring.initializr.actuate.stat;
import java.net.URI;
import java.net.URISyntaxException;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import io.spring.initializr.actuate.stat.StatsProperties.Elastic;
import io.spring.initializr.generator.ProjectRequestEvent; import io.spring.initializr.generator.ProjectRequestEvent;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -31,6 +35,7 @@ import org.springframework.retry.support.RetryTemplate;
import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Async;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
/** /**
* Publish stats for each project generated to an Elastic index. * Publish stats for each project generated to an Elastic index.
@ -44,29 +49,25 @@ public class ProjectGenerationStatPublisher {
private final ProjectRequestDocumentFactory documentFactory; private final ProjectRequestDocumentFactory documentFactory;
private final StatsProperties statsProperties;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final RestTemplate restTemplate; private final RestTemplate restTemplate;
private URI requestUrl;
private final RetryTemplate retryTemplate; private final RetryTemplate retryTemplate;
public ProjectGenerationStatPublisher(ProjectRequestDocumentFactory documentFactory, public ProjectGenerationStatPublisher(ProjectRequestDocumentFactory documentFactory,
StatsProperties statsProperties, RestTemplateBuilder restTemplateBuilder, StatsProperties statsProperties, RestTemplateBuilder restTemplateBuilder,
RetryTemplate retryTemplate) { RetryTemplate retryTemplate) {
this.documentFactory = documentFactory; this.documentFactory = documentFactory;
this.statsProperties = statsProperties;
this.objectMapper = createObjectMapper(); this.objectMapper = createObjectMapper();
StatsProperties.Elastic elastic = statsProperties.getElastic(); StatsProperties.Elastic elastic = statsProperties.getElastic();
if (StringUtils.hasText(elastic.getUsername())) { UriComponentsBuilder uriBuilder = UriComponentsBuilder
this.restTemplate = restTemplateBuilder .fromUri(determineEntityUrl(elastic));
.basicAuthentication(elastic.getUsername(), elastic.getPassword()) this.restTemplate = configureAuthorization(restTemplateBuilder, elastic,
.build(); uriBuilder).build();
} this.requestUrl = uriBuilder.userInfo(null).build().toUri();
else {
this.restTemplate = restTemplateBuilder.build();
}
this.retryTemplate = retryTemplate; this.retryTemplate = retryTemplate;
} }
@ -81,8 +82,7 @@ public class ProjectGenerationStatPublisher {
} }
json = toJson(document); json = toJson(document);
RequestEntity<String> request = RequestEntity RequestEntity<String> request = RequestEntity.post(this.requestUrl)
.post(this.statsProperties.getElastic().getEntityUrl())
.contentType(MediaType.APPLICATION_JSON).body(json); .contentType(MediaType.APPLICATION_JSON).body(json);
this.retryTemplate.execute((context) -> { this.retryTemplate.execute((context) -> {
@ -112,8 +112,40 @@ public class ProjectGenerationStatPublisher {
return mapper; return mapper;
} }
// For testing purposes only
protected RestTemplate getRestTemplate() { protected RestTemplate getRestTemplate() {
return this.restTemplate; return this.restTemplate;
} }
protected void updateRequestUrl(URI requestUrl) {
this.requestUrl = requestUrl;
}
private static RestTemplateBuilder configureAuthorization(
RestTemplateBuilder restTemplateBuilder, Elastic elastic,
UriComponentsBuilder uriComponentsBuilder) {
String userInfo = uriComponentsBuilder.build().getUserInfo();
if (StringUtils.hasText(userInfo)) {
String[] credentials = userInfo.split(":");
return restTemplateBuilder.basicAuthentication(credentials[0],
credentials[1]);
}
else if (StringUtils.hasText(elastic.getUsername())) {
return restTemplateBuilder.basicAuthentication(elastic.getUsername(),
elastic.getPassword());
}
return restTemplateBuilder;
}
private static URI determineEntityUrl(Elastic elastic) {
String entityUrl = elastic.getUri() + "/" + elastic.getIndexName() + "/"
+ elastic.getEntityName();
try {
return new URI(entityUrl);
}
catch (URISyntaxException ex) {
throw new IllegalStateException("Cannot create entity URL: " + entityUrl, ex);
}
}
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2018 the original author or authors. * Copyright 2012-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,9 +16,6 @@
package io.spring.initializr.actuate.stat; package io.spring.initializr.actuate.stat;
import java.net.URI;
import java.net.URISyntaxException;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty; import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
@ -44,7 +41,7 @@ public class StatsProperties {
public static final class Elastic { public static final class Elastic {
/** /**
* Elastic service uri. * Elastic service uri. Overrides username and password when UserInfo is set.
*/ */
private String uri; private String uri;
@ -121,17 +118,6 @@ public class StatsProperties {
this.uri = cleanUri(uri); this.uri = cleanUri(uri);
} }
public URI getEntityUrl() {
String string = this.uri + "/" + this.indexName + "/" + this.entityName;
try {
return new URI(string);
}
catch (URISyntaxException ex) {
throw new IllegalStateException("Cannot create entity URL: " + string,
ex);
}
}
private static String cleanUri(String contextPath) { private static String cleanUri(String contextPath) {
if (StringUtils.hasText(contextPath) && contextPath.endsWith("/")) { if (StringUtils.hasText(contextPath) && contextPath.endsWith("/")) {
return contextPath.substring(0, contextPath.length() - 1); return contextPath.substring(0, contextPath.length() - 1);

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2018 the original author or authors. * Copyright 2012-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -54,14 +54,14 @@ public class MainControllerStatsIntegrationTests
private StatsMockController statsMockController; private StatsMockController statsMockController;
@Autowired @Autowired
private StatsProperties statsProperties; private ProjectGenerationStatPublisher projectGenerationStatPublisher;
@Before @Before
public void setup() { public void setup() {
this.statsMockController.stats.clear(); this.statsMockController.stats.clear();
// Make sure our mock is going to be invoked with the stats // Make sure our mock is going to be invoked with the stats
this.statsProperties.getElastic() this.projectGenerationStatPublisher.updateRequestUrl(
.setUri("http://localhost:" + this.port + "/elastic"); URI.create("http://localhost:" + this.port + "/elastic/test/my-entity"));
} }
@Test @Test
@ -168,8 +168,8 @@ public class MainControllerStatsIntegrationTests
@Test @Test
public void errorPublishingStatsDoesNotBubbleUp() { public void errorPublishingStatsDoesNotBubbleUp() {
this.statsProperties.getElastic() this.projectGenerationStatPublisher.updateRequestUrl(
.setUri("http://localhost:" + this.port + "/elastic-error"); URI.create("http://localhost:" + this.port + "/elastic-error"));
downloadArchive("/starter.zip"); downloadArchive("/starter.zip");
assertThat(this.statsMockController.stats).as("No stat should be available") assertThat(this.statsMockController.stats).as("No stat should be available")
.isEmpty(); .isEmpty();

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2018 the original author or authors. * Copyright 2012-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -32,13 +32,18 @@ import org.springframework.http.MediaType;
import org.springframework.retry.policy.SimpleRetryPolicy; import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate; import org.springframework.retry.support.RetryTemplate;
import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.test.web.client.RequestMatcher;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.header;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.jsonPath; import static org.springframework.test.web.client.match.MockRestRequestMatchers.jsonPath;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus; import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
/** /**
* Tests for {@link ProjectGenerationStatPublisher}.
*
* @author Stephane Nicoll * @author Stephane Nicoll
*/ */
public class ProjectGenerationStatPublisherTests extends AbstractInitializrStatTests { public class ProjectGenerationStatPublisherTests extends AbstractInitializrStatTests {
@ -51,7 +56,10 @@ public class ProjectGenerationStatPublisherTests extends AbstractInitializrStatT
@Before @Before
public void setUp() { public void setUp() {
StatsProperties properties = createProperties(); configureService(createProperties());
}
private void configureService(StatsProperties properties) {
ProjectRequestDocumentFactory documentFactory = new ProjectRequestDocumentFactory( ProjectRequestDocumentFactory documentFactory = new ProjectRequestDocumentFactory(
createProvider(getMetadata())); createProvider(getMetadata()));
this.retryTemplate = new RetryTemplate(); this.retryTemplate = new RetryTemplate();
@ -62,7 +70,62 @@ public class ProjectGenerationStatPublisherTests extends AbstractInitializrStatT
} }
@Test @Test
public void publishSimpleDocument() { public void publishDocumentWithUserNameAndPassword() {
StatsProperties properties = new StatsProperties();
properties.getElastic().setUri("http://example.com/elastic");
properties.getElastic().setUsername("foo");
properties.getElastic().setPassword("bar");
configureService(properties);
testAuthorization("http://example.com/elastic/initializr/request",
header("Authorization", "Basic Zm9vOmJhcg=="));
}
@Test
public void publishDocumentWithUserInfo() {
StatsProperties properties = new StatsProperties();
properties.getElastic().setUri("https://elastic:secret@es.example.com");
configureService(properties);
testAuthorization("https://es.example.com/initializr/request",
header("Authorization", "Basic ZWxhc3RpYzpzZWNyZXQ="));
}
@Test
public void publishDocumentWithUserInfoOverridesUserNamePassword() {
StatsProperties properties = new StatsProperties();
properties.getElastic().setUri("https://elastic:secret@es.example.com");
properties.getElastic().setUsername("another");
properties.getElastic().setPassword("ignored-secret");
configureService(properties);
testAuthorization("https://es.example.com/initializr/request",
header("Authorization", "Basic ZWxhc3RpYzpzZWNyZXQ="));
}
@Test
public void publishDocumentWithNoAuthentication() {
StatsProperties properties = new StatsProperties();
properties.getElastic().setUri("https://example.com/test/");
configureService(properties);
testAuthorization("https://example.com/test/initializr/request",
(request) -> assertThat(request.getHeaders().containsKey("Authorization"))
.isFalse());
}
private void testAuthorization(String expectedUri,
RequestMatcher authorizationMatcher) {
ProjectRequest request = createProjectRequest();
request.setGroupId("com.example.foo");
request.setArtifactId("my-project");
this.mockServer.expect(requestTo(expectedUri)).andExpect(method(HttpMethod.POST))
.andExpect(authorizationMatcher)
.andRespond(withStatus(HttpStatus.CREATED)
.body(mockResponse(UUID.randomUUID().toString(), true))
.contentType(MediaType.APPLICATION_JSON));
this.statPublisher.handleEvent(new ProjectGeneratedEvent(request));
this.mockServer.verify();
}
@Test
public void publishDocument() {
ProjectRequest request = createProjectRequest(); ProjectRequest request = createProjectRequest();
request.setGroupId("com.example.foo"); request.setGroupId("com.example.foo");
request.setArtifactId("my-project"); request.setArtifactId("my-project");
@ -130,8 +193,6 @@ public class ProjectGenerationStatPublisherTests extends AbstractInitializrStatT
StatsProperties properties = new StatsProperties(); StatsProperties properties = new StatsProperties();
Elastic elastic = properties.getElastic(); Elastic elastic = properties.getElastic();
elastic.setUri("http://example.com/elastic"); elastic.setUri("http://example.com/elastic");
elastic.setUsername("foo");
elastic.setPassword("bar");
return properties; return properties;
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2018 the original author or authors. * Copyright 2012-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -21,6 +21,8 @@ import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
/** /**
* Tests for {@link StatsProperties}.
*
* @author Stephane Nicoll * @author Stephane Nicoll
*/ */
public class StatsPropertiesTests { public class StatsPropertiesTests {
@ -33,13 +35,4 @@ public class StatsPropertiesTests {
assertThat(this.properties.getElastic().getUri()).isEqualTo("http://example.com"); assertThat(this.properties.getElastic().getUri()).isEqualTo("http://example.com");
} }
@Test
public void provideEntityUrl() {
this.properties.getElastic().setUri("http://example.com/");
this.properties.getElastic().setIndexName("my-index");
this.properties.getElastic().setEntityName("foo");
assertThat(this.properties.getElastic().getEntityUrl().toString())
.isEqualTo("http://example.com/my-index/foo");
}
} }

View File

@ -1,9 +1,7 @@
initializr: initializr:
stats: stats:
elastic: elastic:
uri: http://localhost:${server.port}/elastic uri: http://localhost/elastic
indexName: test
entityName: my-entity
username: test-user username: test-user
password: test-password password: test-password
max-attempts: 1 max-attempts: 1

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2017 the original author or authors. * Copyright 2012-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -28,8 +28,6 @@ import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource; import org.springframework.core.env.PropertySource;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
/** /**
* Post-process the environment to extract the service credentials provided by * Post-process the environment to extract the service credentials provided by
@ -51,16 +49,7 @@ public class CloudfoundryEnvironmentPostProcessor
Map<String, Object> map = new LinkedHashMap<>(); Map<String, Object> map = new LinkedHashMap<>();
String uri = environment.getProperty("vcap.services.stats-index.credentials.uri"); String uri = environment.getProperty("vcap.services.stats-index.credentials.uri");
if (StringUtils.hasText(uri)) { if (StringUtils.hasText(uri)) {
UriComponents uriComponents = UriComponentsBuilder.fromUriString(uri).build(); map.put("initializr.stats.elastic.uri", uri);
String userInfo = uriComponents.getUserInfo();
if (StringUtils.hasText(userInfo)) {
String[] credentials = userInfo.split(":");
map.put("initializr.stats.elastic.username", credentials[0]);
map.put("initializr.stats.elastic.password", credentials[1]);
}
map.put("initializr.stats.elastic.uri", UriComponentsBuilder
.fromUriString(uri).userInfo(null).build().toString());
addOrReplace(environment.getPropertySources(), map); addOrReplace(environment.getPropertySources(), map);
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2018 the original author or authors. * Copyright 2012-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -35,21 +35,21 @@ public class CloudfoundryEnvironmentPostProcessorTests {
private final SpringApplication application = new SpringApplication(); private final SpringApplication application = new SpringApplication();
@Test @Test
public void parseCredentials() { public void parseUriWithCredentials() {
this.environment.setProperty("vcap.services.stats-index.credentials.uri", this.environment.setProperty("vcap.services.stats-index.credentials.uri",
"http://user:pass@example.com/bar/biz?param=one"); "https://user:pass@example.com/bar/biz?param=one");
this.postProcessor.postProcessEnvironment(this.environment, this.application); this.postProcessor.postProcessEnvironment(this.environment, this.application);
assertThat(this.environment.getProperty("initializr.stats.elastic.uri")) assertThat(this.environment.getProperty("initializr.stats.elastic.uri"))
.isEqualTo("http://example.com/bar/biz?param=one"); .isEqualTo("https://user:pass@example.com/bar/biz?param=one");
assertThat(this.environment.getProperty("initializr.stats.elastic.username")) assertThat(this.environment.getProperty("initializr.stats.elastic.username"))
.isEqualTo("user"); .isNull();
assertThat(this.environment.getProperty("initializr.stats.elastic.password")) assertThat(this.environment.getProperty("initializr.stats.elastic.password"))
.isEqualTo("pass"); .isNull();
} }
@Test @Test
public void parseNoCredentials() { public void parseUri() {
this.environment.setProperty("vcap.services.stats-index.credentials.uri", this.environment.setProperty("vcap.services.stats-index.credentials.uri",
"http://example.com/bar/biz?param=one"); "http://example.com/bar/biz?param=one");
this.postProcessor.postProcessEnvironment(this.environment, this.application); this.postProcessor.postProcessEnvironment(this.environment, this.application);