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");
* you may not use this file except in compliance with the License.
@ -16,9 +16,13 @@
package io.spring.initializr.actuate.stat;
import java.net.URI;
import java.net.URISyntaxException;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.spring.initializr.actuate.stat.StatsProperties.Elastic;
import io.spring.initializr.generator.ProjectRequestEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -31,6 +35,7 @@ import org.springframework.retry.support.RetryTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
/**
* Publish stats for each project generated to an Elastic index.
@ -44,29 +49,25 @@ public class ProjectGenerationStatPublisher {
private final ProjectRequestDocumentFactory documentFactory;
private final StatsProperties statsProperties;
private final ObjectMapper objectMapper;
private final RestTemplate restTemplate;
private URI requestUrl;
private final RetryTemplate retryTemplate;
public ProjectGenerationStatPublisher(ProjectRequestDocumentFactory documentFactory,
StatsProperties statsProperties, RestTemplateBuilder restTemplateBuilder,
RetryTemplate retryTemplate) {
this.documentFactory = documentFactory;
this.statsProperties = statsProperties;
this.objectMapper = createObjectMapper();
StatsProperties.Elastic elastic = statsProperties.getElastic();
if (StringUtils.hasText(elastic.getUsername())) {
this.restTemplate = restTemplateBuilder
.basicAuthentication(elastic.getUsername(), elastic.getPassword())
.build();
}
else {
this.restTemplate = restTemplateBuilder.build();
}
UriComponentsBuilder uriBuilder = UriComponentsBuilder
.fromUri(determineEntityUrl(elastic));
this.restTemplate = configureAuthorization(restTemplateBuilder, elastic,
uriBuilder).build();
this.requestUrl = uriBuilder.userInfo(null).build().toUri();
this.retryTemplate = retryTemplate;
}
@ -81,8 +82,7 @@ public class ProjectGenerationStatPublisher {
}
json = toJson(document);
RequestEntity<String> request = RequestEntity
.post(this.statsProperties.getElastic().getEntityUrl())
RequestEntity<String> request = RequestEntity.post(this.requestUrl)
.contentType(MediaType.APPLICATION_JSON).body(json);
this.retryTemplate.execute((context) -> {
@ -112,8 +112,40 @@ public class ProjectGenerationStatPublisher {
return mapper;
}
// For testing purposes only
protected RestTemplate getRestTemplate() {
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");
* you may not use this file except in compliance with the License.
@ -16,9 +16,6 @@
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.NestedConfigurationProperty;
import org.springframework.util.StringUtils;
@ -44,7 +41,7 @@ public class StatsProperties {
public static final class Elastic {
/**
* Elastic service uri.
* Elastic service uri. Overrides username and password when UserInfo is set.
*/
private String uri;
@ -121,17 +118,6 @@ public class StatsProperties {
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) {
if (StringUtils.hasText(contextPath) && contextPath.endsWith("/")) {
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");
* you may not use this file except in compliance with the License.
@ -54,14 +54,14 @@ public class MainControllerStatsIntegrationTests
private StatsMockController statsMockController;
@Autowired
private StatsProperties statsProperties;
private ProjectGenerationStatPublisher projectGenerationStatPublisher;
@Before
public void setup() {
this.statsMockController.stats.clear();
// Make sure our mock is going to be invoked with the stats
this.statsProperties.getElastic()
.setUri("http://localhost:" + this.port + "/elastic");
this.projectGenerationStatPublisher.updateRequestUrl(
URI.create("http://localhost:" + this.port + "/elastic/test/my-entity"));
}
@Test
@ -168,8 +168,8 @@ public class MainControllerStatsIntegrationTests
@Test
public void errorPublishingStatsDoesNotBubbleUp() {
this.statsProperties.getElastic()
.setUri("http://localhost:" + this.port + "/elastic-error");
this.projectGenerationStatPublisher.updateRequestUrl(
URI.create("http://localhost:" + this.port + "/elastic-error"));
downloadArchive("/starter.zip");
assertThat(this.statsMockController.stats).as("No stat should be available")
.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");
* 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.support.RetryTemplate;
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.method;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
/**
* Tests for {@link ProjectGenerationStatPublisher}.
*
* @author Stephane Nicoll
*/
public class ProjectGenerationStatPublisherTests extends AbstractInitializrStatTests {
@ -51,7 +56,10 @@ public class ProjectGenerationStatPublisherTests extends AbstractInitializrStatT
@Before
public void setUp() {
StatsProperties properties = createProperties();
configureService(createProperties());
}
private void configureService(StatsProperties properties) {
ProjectRequestDocumentFactory documentFactory = new ProjectRequestDocumentFactory(
createProvider(getMetadata()));
this.retryTemplate = new RetryTemplate();
@ -62,7 +70,62 @@ public class ProjectGenerationStatPublisherTests extends AbstractInitializrStatT
}
@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();
request.setGroupId("com.example.foo");
request.setArtifactId("my-project");
@ -130,8 +193,6 @@ public class ProjectGenerationStatPublisherTests extends AbstractInitializrStatT
StatsProperties properties = new StatsProperties();
Elastic elastic = properties.getElastic();
elastic.setUri("http://example.com/elastic");
elastic.setUsername("foo");
elastic.setPassword("bar");
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");
* 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;
/**
* Tests for {@link StatsProperties}.
*
* @author Stephane Nicoll
*/
public class StatsPropertiesTests {
@ -33,13 +35,4 @@ public class StatsPropertiesTests {
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:
stats:
elastic:
uri: http://localhost:${server.port}/elastic
indexName: test
entityName: my-entity
uri: http://localhost/elastic
username: test-user
password: test-password
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");
* 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.PropertySource;
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
@ -51,16 +49,7 @@ public class CloudfoundryEnvironmentPostProcessor
Map<String, Object> map = new LinkedHashMap<>();
String uri = environment.getProperty("vcap.services.stats-index.credentials.uri");
if (StringUtils.hasText(uri)) {
UriComponents uriComponents = UriComponentsBuilder.fromUriString(uri).build();
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());
map.put("initializr.stats.elastic.uri", uri);
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");
* 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();
@Test
public void parseCredentials() {
public void parseUriWithCredentials() {
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);
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"))
.isEqualTo("user");
.isNull();
assertThat(this.environment.getProperty("initializr.stats.elastic.password"))
.isEqualTo("pass");
.isNull();
}
@Test
public void parseNoCredentials() {
public void parseUri() {
this.environment.setProperty("vcap.services.stats-index.credentials.uri",
"http://example.com/bar/biz?param=one");
this.postProcessor.postProcessEnvironment(this.environment, this.application);