package org.github.etcd.service.api.v2; import java.io.IOException; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import javax.ws.rs.NotFoundException; import javax.ws.rs.RedirectionException; import javax.ws.rs.WebApplicationException; import javax.ws.rs.client.Client; import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.client.ClientRequestContext; import javax.ws.rs.client.ClientRequestFilter; import javax.ws.rs.client.Entity; import javax.ws.rs.client.Invocation; import javax.ws.rs.client.WebTarget; import javax.ws.rs.core.Form; import javax.ws.rs.core.GenericType; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.UriBuilder; import org.github.etcd.service.api.EtcdError; import org.github.etcd.service.api.EtcdException; import org.github.etcd.service.api.EtcdMember; import org.github.etcd.service.api.EtcdMembers; import org.github.etcd.service.api.EtcdNode; import org.github.etcd.service.api.EtcdProxy; import org.github.etcd.service.api.EtcdResponse; import org.github.etcd.service.api.EtcdSelfStats; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider; public class EtcdV2ProxyImpl implements EtcdProxy { private static final Logger LOG = LoggerFactory.getLogger(EtcdProxy.class); private final String targetUrl; private final String authenticationToken; private Client client; public EtcdV2ProxyImpl(String targetUrl, String authenticationToken) { this.targetUrl = targetUrl; this.authenticationToken = authenticationToken; } public EtcdV2ProxyImpl(String targetUrl) { this(targetUrl, null); } private WebTarget getWebTarget() { if (client == null) { client = ClientBuilder.newClient(); client.register(JacksonJsonProvider.class); // register the basic authentication filter if authentication information is provided if (authenticationToken != null) { client.register(new ClientRequestFilter() { @Override public void filter(ClientRequestContext requestContext) throws IOException { requestContext.getHeaders().add("Authorization", "Basic " + authenticationToken); } }); } } WebTarget target = client.target(targetUrl); return target; } @Override public void close() { if (client != null) { client.close(); client = null; } } @Override public String getVersion() { return getWebTarget() .path("/version") .request(MediaType.TEXT_PLAIN) .get(String.class); } @Override public Boolean isAuthEnabled() { try { Map<String, Object> result = getWebTarget().path("/v2/auth/enable").request(MediaType.APPLICATION_JSON).get(new GenericType<Map<String, Object>>() {}); return (Boolean) result.get("enabled"); } catch (NotFoundException e) { // LOG.warn(e.toString(), e); return false; } } @Override public EtcdSelfStats getSelfStats() { return new ExceptionHandlingProcessor<>(EtcdSelfStats.class).process(getWebTarget().path("/v2/stats/self").request(MediaType.APPLICATION_JSON).buildGet()); } @Override public List<EtcdMember> getMembers() { String version = getVersion(); LOG.info("Using version: '{}' to detect cluster members", version); LOG.info("Authentication is: " + (isAuthEnabled() ? "enabled" : "disabled")); if (version.contains("etcd 0.4")) { // alternatively we could use the key-value to retrieve the nodes contained at /v2/keys/_etcd/machines URI raftUri = UriBuilder.fromUri(targetUrl).port(7001).build(); List<Map<String, String>> items = client.target(raftUri).path("/v2/admin/machines").request(MediaType.APPLICATION_JSON).get(new GenericType<List<Map<String, String>>>() {}); System.out.println("Retrieved: " + items); List<EtcdMember> members = new ArrayList<>(items.size()); for (Map<String, String> item : items) { EtcdMember member = new EtcdMember(); member.setId(item.get("name")); member.setName(member.getId()); member.setState(item.get("state")); member.setClientURLs(Arrays.asList(item.get("clientURL"))); member.setPeerURLs(Arrays.asList(item.get("peerURL"))); members.add(member); } return members; } else { EtcdMembers members = new ExceptionHandlingProcessor<>(EtcdMembers.class).process(getWebTarget().path("/v2/members").request(MediaType.APPLICATION_JSON).buildGet()); return members.getMembers(); } } @Override public EtcdNode getNode(final String key) { Invocation getNode = getWebTarget() .path("/v2/keys/{key}") .resolveTemplate("key", normalizeKey(key), false) .request(MediaType.APPLICATION_JSON) .buildGet(); return new ExceptionHandlingProcessor<>(EtcdResponse.class).process(getNode).getNode(); } @Override public void saveNode(EtcdNode node) { EtcdResponse response = saveOrUpdateNode(node, false); LOG.debug("Created Node: " + response); } @Override public EtcdNode updateNode(EtcdNode node) { EtcdResponse response = saveOrUpdateNode(node, true); LOG.debug("Updated Node: " + response); return response.getPrevNode(); } @Override public EtcdNode deleteNode(EtcdNode node) { return deleteNode(node, false); } @Override public EtcdNode deleteNode(EtcdNode node, boolean recursive) { WebTarget target = getWebTarget().path("/v2/keys/{key}").resolveTemplate("key", normalizeKey(node.getKey()), false); if (node.isDir()) { if (recursive) { target = target.queryParam("recursive", recursive); } else { target = target.queryParam("dir", node.isDir()); } } Invocation deleteInvocation = target.request(MediaType.APPLICATION_JSON).buildDelete(); return new ExceptionHandlingProcessor<>(EtcdResponse.class).process(deleteInvocation).getNode(); } private String normalizeKey(final String key) { if (key == null) { return "/"; } return key.startsWith("/") ? key.substring(1) + "/" : key + "/"; } protected EtcdResponse saveOrUpdateNode(EtcdNode node, Boolean update) { if (node.isDir() && update && node.getTtl() == null) { LOG.warn("Remove directory TTL is not supported by etcd version 0.4.9"); } Form form = new Form(); if (node.isDir()) { form.param("dir", Boolean.TRUE.toString()); } else { form.param("value", node.getValue()); } if (update) { form.param("ttl", node.getTtl() == null ? "" : node.getTtl().toString()); } else if (node.getTtl() != null) { form.param("ttl", node.getTtl().toString()); } // we include prevExist parameter within all requests for safety form.param("prevExist", update.toString()); Invocation invocation = getWebTarget() .path("/v2/keys/{key}") .resolveTemplate("key", normalizeKey(node.getKey()), false) .request(MediaType.APPLICATION_JSON) .buildPut(Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED)); return new ExceptionHandlingProcessor<>(EtcdResponse.class).process(invocation); } private static class ExceptionHandlingProcessor<T> { private final Class<T> responseType; public ExceptionHandlingProcessor(Class<T> responseType) { this.responseType = responseType; } public T process(Invocation invocation) { try { return invocation.invoke(responseType); } catch (RedirectionException e) { // TODO: maybe create another invocation and start over ??? throw new EtcdException(e); } catch (WebApplicationException e) { try { // try to read the contained api error if it exists EtcdError error = e.getResponse().readEntity(EtcdError.class); throw new EtcdException(e, error); } catch (EtcdException e1) { throw e1; } catch (Exception e1) { // just ignore this one and wrap the original LOG.debug(e1.getLocalizedMessage(), e1); throw new EtcdException(e); } } } } }