Commit 21c17900 by Johannes Edmeier

Add metadata to the applications

With this commit you can now associate your applications with custom metadata using `spring.boot.admin.client.metadata.*` sensitive values are recognized by key and are masked in the http interface. This can be useful in the future if there are instance specific settings which should be used by the admin server, e.g. custom notification recipients. For now there is no extra imetadata view in the ui. The values are shown in the environment view.
parent ccd73ea0
......@@ -116,6 +116,10 @@ spring.boot.admin.password
| spring.boot.admin.client.prefer-ip
| Use the ip-address rather then the hostname in the guessed urls. If `server.address` / `management.address` is set, it get used. Otherwise the IP address returned from `InetAddress.getLocalHost()` gets used.
| `false`
| spring.boot.admin.client.metadata.*
| Metadata to be asscoiated with this instance
|
|===
----
\ No newline at end of file
......@@ -19,6 +19,7 @@ import static org.apache.commons.lang.StringUtils.defaultIfEmpty;
import static org.apache.commons.lang.StringUtils.stripStart;
import java.net.URI;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
......@@ -67,6 +68,11 @@ public class DefaultServiceInstanceConverter implements ServiceInstanceConverter
builder.withServiceUrl(serviceUrl.toString());
}
Map<String, String> metadata = getMetadata(instance);
if (metadata != null) {
builder.withMetadata(metadata);
}
return builder.build();
}
......@@ -96,6 +102,10 @@ public class DefaultServiceInstanceConverter implements ServiceInstanceConverter
return instance.getUri();
}
protected Map<String, String> getMetadata(ServiceInstance instance) {
return instance.getMetadata();
}
/**
* Default <code>management.context-path</code> to be appended to the url of the discovered
* service for the managment-url.
......
......@@ -17,15 +17,25 @@ package de.codecentric.boot.admin.model;
import java.io.IOException;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Pattern;
import org.springframework.util.Assert;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
/**
* The domain model for all registered application at the spring boot admin application.
......@@ -41,6 +51,8 @@ public class Application implements Serializable {
private final String serviceUrl;
private final StatusInfo statusInfo;
private final String source;
@JsonSerialize(using = Application.MetadataSerializer.class)
private final Map<String, String> metadata;
protected Application(Builder builder) {
Assert.hasText(builder.name, "name must not be empty!");
......@@ -53,6 +65,7 @@ public class Application implements Serializable {
this.id = builder.id;
this.statusInfo = builder.statusInfo;
this.source = builder.source;
this.metadata = Collections.unmodifiableMap(new HashMap<>(builder.metadata));
}
public static Builder create(String name) {
......@@ -71,6 +84,7 @@ public class Application implements Serializable {
private String serviceUrl;
private StatusInfo statusInfo = StatusInfo.ofUnknown();
private String source;
private Map<String, String> metadata = new HashMap<>();
private Builder(String name) {
this.name = name;
......@@ -84,6 +98,7 @@ public class Application implements Serializable {
this.id = application.id;
this.statusInfo = application.statusInfo;
this.source = application.source;
this.metadata.putAll(application.getMetadata());
}
public Builder withName(String name) {
......@@ -121,6 +136,16 @@ public class Application implements Serializable {
return this;
}
public Builder withMetadata(String key, String value) {
this.metadata.put(key, value);
return this;
}
public Builder withMetadata(Map<String, String> metadata) {
this.metadata.putAll(metadata);
return this;
}
public Application build() {
return new Application(this);
}
......@@ -154,6 +179,9 @@ public class Application implements Serializable {
return source;
}
public Map<String, String> getMetadata() {
return metadata;
}
@Override
public String toString() {
return "Application [id=" + id + ", name=" + name + ", managementUrl="
......@@ -251,7 +279,53 @@ public class Application implements Serializable {
builder.withServiceUrl(node.get("serviceUrl").asText());
}
}
if (node.has("metadata")) {
Iterator<Entry<String, JsonNode>> it = node.get("metadata").fields();
while (it.hasNext()) {
Entry<String, JsonNode> entry = it.next();
builder.withMetadata(entry.getKey(), entry.getValue().asText());
}
}
return builder.build();
}
}
public static class MetadataSerializer extends StdSerializer<Map<String, String>> {
private static final long serialVersionUID = 1L;
private static Pattern[] keysToSanitize = createPatterns(".*password$", ".*secret$",
".*key$", ".*$token$", ".*credentials.*", ".*vcap_services$");
@SuppressWarnings("unchecked")
public MetadataSerializer() {
super((Class<Map<String, String>>) (Class<?>) Map.class);
}
private static Pattern[] createPatterns(String... keys) {
Pattern[] patterns = new Pattern[keys.length];
for (int i = 0; i < keys.length; i++) {
patterns[i] = Pattern.compile(keys[i]);
}
return patterns;
}
@Override
public void serialize(Map<String, String> value, JsonGenerator gen,
SerializerProvider provider) throws IOException {
gen.writeStartObject();
for (Entry<String, String> entry : value.entrySet()) {
gen.writeStringField(entry.getKey(), sanitize(entry.getKey(), entry.getValue()));
}
gen.writeEndObject();
}
private String sanitize(String key, String value) {
for (Pattern pattern : MetadataSerializer.keysToSanitize) {
if (pattern.matcher(key).matches()) {
return (value == null ? null : "******");
}
}
return value;
}
}
}
......@@ -4,6 +4,9 @@ import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.junit.Assert.assertThat;
import java.util.HashMap;
import java.util.Map;
import org.junit.Test;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.client.ServiceInstance;
......@@ -43,9 +46,11 @@ public class DefaultServiceInstanceConverterTest {
@Test
public void test_convert_with_metadata() {
ServiceInstance service = new DefaultServiceInstance("test", "localhost", 80, false);
service.getMetadata().put("health.path", "ping");
service.getMetadata().put("management.context-path", "mgmt");
service.getMetadata().put("management.port", "1234");
Map<String, String> metadata = new HashMap<>();
metadata.put("health.path", "ping");
metadata.put("management.context-path", "mgmt");
metadata.put("management.port", "1234");
service.getMetadata().putAll(metadata);
Application application = new DefaultServiceInstanceConverter().convert(service);
......@@ -54,6 +59,7 @@ public class DefaultServiceInstanceConverterTest {
assertThat(application.getServiceUrl(), is("http://localhost:80"));
assertThat(application.getManagementUrl(), is("http://localhost:1234/mgmt"));
assertThat(application.getHealthUrl(), is("http://localhost:1234/mgmt/ping"));
assertThat(application.getMetadata(), is(metadata));
}
}
package de.codecentric.boot.admin.model;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
import java.io.IOException;
import java.util.Collections;
import org.json.JSONObject;
import org.junit.Test;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
......@@ -18,10 +21,8 @@ public class ApplicationTest {
@Test
public void test_1_2_json_format() throws JsonProcessingException, IOException {
String json = "{ \"name\" : \"test\", \"url\" : \"http://test\" }";
String json = new JSONObject().put("name", "test").put("url", "http://test").toString();
Application value = objectMapper.readValue(json, Application.class);
assertThat(value.getName(), is("test"));
assertThat(value.getManagementUrl(), is("http://test"));
assertThat(value.getHealthUrl(), is("http://test/health"));
......@@ -30,10 +31,10 @@ public class ApplicationTest {
@Test
public void test_1_4_json_format() throws JsonProcessingException, IOException {
String json = "{ \"name\" : \"test\", \"managementUrl\" : \"http://test\" , \"healthUrl\" : \"http://health\" , \"serviceUrl\" : \"http://service\", \"statusInfo\": {\"status\":\"UNKNOWN\"} }";
String json = new JSONObject().put("name", "test").put("managementUrl", "http://test")
.put("healthUrl", "http://health").put("serviceUrl", "http://service")
.put("statusInfo", new JSONObject().put("status", "UNKNOWN")).toString();
Application value = objectMapper.readValue(json, Application.class);
assertThat(value.getName(), is("test"));
assertThat(value.getManagementUrl(), is("http://test"));
assertThat(value.getHealthUrl(), is("http://health"));
......@@ -42,20 +43,21 @@ public class ApplicationTest {
@Test
public void test_1_5_json_format() throws JsonProcessingException, IOException {
String json = "{ \"name\" : \"test\", \"managementUrl\" : \"http://test\" , \"healthUrl\" : \"http://health\" , \"serviceUrl\" : \"http://service\"}";
String json = new JSONObject().put("name", "test").put("managementUrl", "http://test")
.put("healthUrl", "http://health").put("serviceUrl", "http://service")
.put("metadata", new JSONObject().put("labels", "foo,bar")).toString();
Application value = objectMapper.readValue(json, Application.class);
assertThat(value.getName(), is("test"));
assertThat(value.getManagementUrl(), is("http://test"));
assertThat(value.getHealthUrl(), is("http://health"));
assertThat(value.getServiceUrl(), is("http://service"));
assertThat(value.getMetadata(), is(Collections.singletonMap("labels", "foo,bar")));
}
@Test
public void test_onlyHealhUrl() throws JsonProcessingException, IOException {
String json = "{ \"name\" : \"test\", \"healthUrl\" : \"http://test\" }";
public void test_onlyHealthUrl() throws JsonProcessingException, IOException {
String json = new JSONObject().put("name", "test").put("healthUrl", "http://test")
.toString();
Application value = objectMapper.readValue(json, Application.class);
assertThat(value.getName(), is("test"));
assertThat(value.getHealthUrl(), is("http://test"));
......@@ -65,17 +67,28 @@ public class ApplicationTest {
@Test(expected = IllegalArgumentException.class)
public void test_name_expected() throws JsonProcessingException, IOException {
String json = "{ \"name\" : \"\", \"managementUrl\" : \"http://test\" , \"healthUrl\" : \"http://health\" , \"serviceUrl\" : \"http://service\"}";
String json = new JSONObject().put("name", "").put("managementUrl", "http://test")
.put("healthUrl", "http://health").put("serviceUrl", "http://service").toString();
objectMapper.readValue(json, Application.class);
}
@Test(expected = IllegalArgumentException.class)
public void test_healthUrl_expected() throws JsonProcessingException, IOException {
String json = "{ \"name\" : \"test\", \"managementUrl\" : \"http://test\" , \"healthUrl\" : \"\" , \"serviceUrl\" : \"http://service\"}";
String json = new JSONObject().put("name", "test").put("managementUrl", "http://test")
.put("healthUrl", "").put("serviceUrl", "http://service").toString();
objectMapper.readValue(json, Application.class);
}
@Test
public void test_sanitize_metadata() throws JsonProcessingException {
Application app = Application.create("test").withHealthUrl("http://health")
.withMetadata("password", "qwertz123").withMetadata("user", "humptydumpty").build();
String json = objectMapper.writeValueAsString(app);
assertThat(json, not(containsString("qwertz123")));
assertThat(json, containsString("humptydumpty"));
}
@Test
public void test_equals_hashCode() {
Application a1 = Application.create("foo").withHealthUrl("healthUrl")
.withManagementUrl("mgmt").withServiceUrl("svc").withId("id").build();
......
......@@ -23,6 +23,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import java.io.UnsupportedEncodingException;
import org.json.JSONObject;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
......@@ -40,8 +41,10 @@ import de.codecentric.boot.admin.registry.store.SimpleApplicationStore;
public class RegistryControllerTest {
private static final String APPLICATION_TEST_JSON = "{ \"name\":\"test\", \"healthUrl\":\"http://localhost/mgmt/health\"}";
private static final String APPLICATION_TWICE_JSON = "{ \"name\":\"twice\", \"healthUrl\":\"http://localhost/mgmt/health\"}";
private static final String APPLICATION_TEST_JSON = new JSONObject().put("name", "test")
.put("healthUrl", "http://localhost/mgmt/health").toString();
private static final String APPLICATION_TWICE_JSON = new JSONObject().put("name", "twice")
.put("healthUrl", "http://localhost/mgmt/health").toString();
private MockMvc mvc;
@Before
......@@ -82,7 +85,6 @@ public class RegistryControllerTest {
.andExpect(jsonPath("$.id").value(id));
mvc.perform(get("/api/applications/{id}", id)).andExpect(status().isNotFound());
}
......
......@@ -15,6 +15,9 @@
*/
package de.codecentric.boot.admin.client.config;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
......@@ -49,6 +52,11 @@ public class AdminClientProperties {
*/
private boolean preferIp = false;
/**
* Metadata that should be associated with this application
*/
private Map<String, String> metadata = new HashMap<>();
public String getManagementUrl() {
return managementUrl;
}
......@@ -88,4 +96,8 @@ public class AdminClientProperties {
public void setPreferIp(boolean preferIp) {
this.preferIp = preferIp;
}
public Map<String, String> getMetadata() {
return metadata;
}
}
......@@ -15,20 +15,23 @@
*/
package de.codecentric.boot.admin.client.registration;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.springframework.util.Assert;
/**
* The domain model for all registered application at the spring boot admin application.
* Contains all informations which is used when this application is registered.
*
* @author Johannes Edmeier
*/
public class Application implements Serializable {
private static final long serialVersionUID = 1L;
public class Application {
private final String name;
private final String managementUrl;
private final String healthUrl;
private final String serviceUrl;
private final Map<String, String> metadata;
protected Application(Builder builder) {
Assert.hasText(builder.name, "name must not be empty!");
......@@ -37,33 +40,24 @@ public class Application implements Serializable {
this.managementUrl = builder.managementUrl;
this.serviceUrl = builder.serviceUrl;
this.name = builder.name;
this.metadata = Collections.unmodifiableMap(new HashMap<>(builder.metadata));
}
public static Builder create(String name) {
return new Builder(name);
}
public static Builder create(Application application) {
return new Builder(application);
}
public static class Builder {
private String name;
private String managementUrl;
private String healthUrl;
private String serviceUrl;
private Map<String, String> metadata = new HashMap<>();
private Builder(String name) {
this.name = name;
}
private Builder(Application application) {
this.healthUrl = application.healthUrl;
this.managementUrl = application.managementUrl;
this.serviceUrl = application.serviceUrl;
this.name = application.name;
}
public Builder withName(String name) {
this.name = name;
return this;
......@@ -84,6 +78,16 @@ public class Application implements Serializable {
return this;
}
public Builder withMetadata(String key, String value) {
this.metadata.put(key, value);
return this;
}
public Builder withMetadata(Map<String, String> metadata) {
this.metadata.putAll(metadata);
return this;
}
public Application build() {
return new Application(this);
}
......@@ -105,10 +109,14 @@ public class Application implements Serializable {
return serviceUrl;
}
public Map<String, String> getMetadata() {
return metadata;
}
@Override
public String toString() {
return "Application [name=" + name + ", managementUrl="
+ managementUrl + ", healthUrl=" + healthUrl + ", serviceUrl=" + serviceUrl + "]";
return "Application [name=" + name + ", managementUrl=" + managementUrl + ", healthUrl="
+ healthUrl + ", serviceUrl=" + serviceUrl + "]";
}
@Override
......
......@@ -4,6 +4,7 @@ import static org.springframework.util.StringUtils.trimLeadingCharacter;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Map;
import org.springframework.boot.actuate.autoconfigure.ManagementServerProperties;
import org.springframework.boot.autoconfigure.web.ServerProperties;
......@@ -43,7 +44,8 @@ public class DefaultApplicationFactory implements ApplicationFactory {
@Override
public Application createApplication() {
return Application.create(getName()).withHealthUrl(getHealthUrl())
.withManagementUrl(getManagementUrl()).withServiceUrl(getServiceUrl()).build();
.withManagementUrl(getManagementUrl()).withServiceUrl(getServiceUrl())
.withMetadata(getMetadata()).build();
}
protected String getName() {
......@@ -92,6 +94,10 @@ public class DefaultApplicationFactory implements ApplicationFactory {
.toUriString();
}
protected Map<String, String> getMetadata() {
return client.getMetadata();
}
protected String getServiceHost() {
InetAddress address = server.getAddress();
if (address == null) {
......
......@@ -50,12 +50,4 @@ public class ApplicationTest {
assertThat(a1, not(is(a3)));
assertThat(a2, not(is(a3)));
}
@Test
public void test_builder_copy() {
Application app = Application.create("App").withHealthUrl("http://health")
.withManagementUrl("http://mgmgt").withServiceUrl("http://svc").build();
Application copy = Application.create(app).build();
assertThat(app, is(copy));
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment