Skip to content

Commit

Permalink
Add metadata to the applications
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
joshiste committed Dec 14, 2016
1 parent ccd73ea commit 21c1790
Show file tree
Hide file tree
Showing 11 changed files with 174 additions and 48 deletions.
4 changes: 4 additions & 0 deletions spring-boot-admin-docs/src/main/asciidoc/client.adoc
Expand Up @@ -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
|
|===

----

This file was deleted.

Expand Up @@ -19,6 +19,7 @@
import static org.apache.commons.lang.StringUtils.stripStart;

import java.net.URI;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -67,6 +68,11 @@ public Application convert(ServiceInstance instance) {
builder.withServiceUrl(serviceUrl.toString());
}

Map<String, String> metadata = getMetadata(instance);
if (metadata != null) {
builder.withMetadata(metadata);
}

return builder.build();
}

Expand Down Expand Up @@ -96,6 +102,10 @@ protected URI getServiceUrl(ServiceInstance instance) {
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.
Expand Down
Expand Up @@ -17,15 +17,25 @@

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.
Expand All @@ -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!");
Expand All @@ -53,6 +65,7 @@ protected Application(Builder builder) {
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) {
Expand All @@ -71,6 +84,7 @@ public static class Builder {
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;
Expand All @@ -84,6 +98,7 @@ private Builder(Application application) {
this.id = application.id;
this.statusInfo = application.statusInfo;
this.source = application.source;
this.metadata.putAll(application.getMetadata());
}

public Builder withName(String name) {
Expand Down Expand Up @@ -121,6 +136,16 @@ public Builder withSource(String source) {
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);
}
Expand Down Expand Up @@ -154,6 +179,9 @@ public String getSource() {
return source;
}

public Map<String, String> getMetadata() {
return metadata;
}
@Override
public String toString() {
return "Application [id=" + id + ", name=" + name + ", managementUrl="
Expand Down Expand Up @@ -251,7 +279,53 @@ public Application deserialize(JsonParser p, DeserializationContext ctxt)
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;
}
}
}
Expand Up @@ -4,6 +4,9 @@
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;
Expand Down Expand Up @@ -43,9 +46,11 @@ public void test_convert_with_custom_defaults() {
@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);

Expand All @@ -54,6 +59,7 @@ public void test_convert_with_metadata() {
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));
}

}
@@ -1,12 +1,15 @@
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;

Expand All @@ -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"));
Expand All @@ -30,10 +31,10 @@ public void test_1_2_json_format() throws JsonProcessingException, IOException {

@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"));
Expand All @@ -42,20 +43,21 @@ public void test_1_4_json_format() throws JsonProcessingException, IOException {

@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"));
Expand All @@ -65,16 +67,27 @@ public void test_onlyHealhUrl() throws JsonProcessingException, IOException {

@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")
Expand Down
Expand Up @@ -23,6 +23,7 @@

import java.io.UnsupportedEncodingException;

import org.json.JSONObject;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
Expand All @@ -40,8 +41,10 @@

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
Expand Down Expand Up @@ -82,7 +85,6 @@ public void test_register_twice_get_and_remove() throws Exception {
.andExpect(jsonPath("$.id").value(id));

mvc.perform(get("/api/applications/{id}", id)).andExpect(status().isNotFound());

}


Expand Down
Expand Up @@ -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;

Expand Down Expand Up @@ -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<>();

This comment has been minimized.

Copy link
@wlami

wlami May 30, 2017

There is no setter for metadata so that properties from the application.yml are not set. They would be required for security (user.name and password)


public String getManagementUrl() {
return managementUrl;
}
Expand Down Expand Up @@ -88,4 +96,8 @@ public boolean isPreferIp() {
public void setPreferIp(boolean preferIp) {
this.preferIp = preferIp;
}

public Map<String, String> getMetadata() {
return metadata;
}
}

0 comments on commit 21c1790

Please sign in to comment.