Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Edge cache integration #231

Merged
merged 33 commits into from Oct 5, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
7524a0b
New interface to allow edge cache integrations.
baconmania Sep 25, 2017
38f99f3
Add Cloudflare API client.
baconmania Sep 25, 2017
cb9a5da
Remove unused dependency.
baconmania Sep 27, 2017
f55b3d0
Add config class for EdgeCaches.
baconmania Sep 27, 2017
e215d3c
Include EdgeCacheConfiguration in top-level config.
baconmania Sep 27, 2017
396079a
Wire up EdgeCacheConfiguration into Guice.
baconmania Sep 27, 2017
e9b15ee
Add POJOs for interacting with Cloudflare API.
baconmania Sep 27, 2017
8f400d4
Give EdgeCache implementations control over whether they want to inva…
baconmania Sep 27, 2017
6e90428
Add exception class for Cloudflare interactions.
baconmania Sep 27, 2017
8a3ca70
Fill out remainder of required Cloudflare client implementation.
baconmania Sep 27, 2017
092a426
Add actual EdgeCache implementation.
baconmania Sep 27, 2017
8f3169a
Set up Guice wiring to bind configured EdgeCache implementation at ru…
baconmania Sep 27, 2017
17af46d
Insert edge cache handling into the top-level worker logic.
baconmania Sep 27, 2017
8f484ef
Minor cleanup.
baconmania Sep 27, 2017
58896f6
Simpler technique for binding the configured EdgeCache implementation.
baconmania Sep 28, 2017
89337cc
Add documentation.
baconmania Oct 2, 2017
42a9b2a
Represent edgeCacheDNS as a separate field, instead of using the opti…
baconmania Oct 2, 2017
8930c4c
Make implementation-specific edge cache configuration generic.
baconmania Oct 2, 2017
26dbf61
Make cache tag format directly configurable.
baconmania Oct 2, 2017
3c6a5a2
Add JSON deserialization annotations.
baconmania Oct 3, 2017
52ace44
Minor logging change.
baconmania Oct 3, 2017
f1b7cac
Cloudflare tells us how many pages remain.
baconmania Oct 3, 2017
5e931fb
Write body using an ObjectMapper.
baconmania Oct 3, 2017
60e5b33
Add logging.
baconmania Oct 3, 2017
151c4fa
Minor refactor.
baconmania Oct 3, 2017
e3a1272
Move Cloudflare-specific models into their own package.
baconmania Oct 3, 2017
f809fd9
Add equals() & hashCode() implementations.
baconmania Oct 3, 2017
52629ec
Add caching layer for Cloudflare API GET operations.
baconmania Oct 3, 2017
7806809
We care about the DNS record's name, not the zone's name.
baconmania Oct 3, 2017
ecea2cf
Use maps instead of lists for efficiency.
baconmania Oct 3, 2017
b961792
Merge branch 'master' into edge-cache-integration
ssalinas Oct 4, 2017
0942a83
add deprecated constructor
ssalinas Oct 4, 2017
830e52c
add toString methods for better logging
ssalinas Oct 5, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -41,14 +41,17 @@ public class BaragonService {

private final Set<String> domains;

private final Optional<String> edgeCacheDNS;

public BaragonService(@JsonProperty("serviceId") String serviceId,
@JsonProperty("owners") Collection<String> owners,
@JsonProperty("serviceBasePath") String serviceBasePath,
@JsonProperty("additionalPaths") List<String> additionalPaths,
@JsonProperty("loadBalancerGroups") Set<String> loadBalancerGroups,
@JsonProperty("options") Map<String, Object> options,
@JsonProperty("templateName") Optional<String> templateName,
@JsonProperty("domains") Set<String> domains) {
@JsonProperty("domains") Set<String> domains,
@JsonProperty("edgeCacheDNS") Optional<String> edgeCacheDNS) {
this.serviceId = serviceId;
this.owners = owners;
this.serviceBasePath = serviceBasePath;
Expand All @@ -57,14 +60,19 @@ public BaragonService(@JsonProperty("serviceId") String serviceId,
this.options = options;
this.templateName = templateName;
this.domains = MoreObjects.firstNonNull(domains, Collections.<String>emptySet());
this.edgeCacheDNS = edgeCacheDNS;
}

public BaragonService(String serviceId, Collection<String> owners, String serviceBasePath, List<String> additionalPaths, Set<String> loadBalancerGroups, Map<String, Object> options, Optional<String> templateName, Set<String> domains) {
this(serviceId, owners, serviceBasePath, additionalPaths, loadBalancerGroups, options, templateName, domains, Optional.absent());
}

public BaragonService(String serviceId, Collection<String> owners, String serviceBasePath, List<String> additionalPaths, Set<String> loadBalancerGroups, Map<String, Object> options, Optional<String> templateName) {
this(serviceId, owners, serviceBasePath, additionalPaths, loadBalancerGroups, options, templateName, Collections.<String>emptySet());
this(serviceId, owners, serviceBasePath, additionalPaths, loadBalancerGroups, options, templateName, Collections.<String>emptySet(), Optional.absent());
}

public BaragonService(String serviceId, Collection<String> owners, String serviceBasePath, Set<String> loadBalancerGroups, Map<String, Object> options) {
this(serviceId, owners, serviceBasePath, Collections.<String>emptyList(), loadBalancerGroups, options, Optional.<String>absent(), Collections.<String>emptySet());
this(serviceId, owners, serviceBasePath, Collections.<String>emptyList(), loadBalancerGroups, options, Optional.<String>absent(), Collections.<String>emptySet(), Optional.absent());
}

public String getServiceId() {
Expand All @@ -87,6 +95,9 @@ public Set<String> getLoadBalancerGroups() {
return loadBalancerGroups;
}

/**
* Data from this field is primarily used to populate data in rendered nginx config templates.
*/
public Map<String, Object> getOptions() {
return options;
}
Expand All @@ -99,6 +110,10 @@ public Set<String> getDomains() {
return domains;
}

public Optional<String> getEdgeCacheDNS() {
return edgeCacheDNS;
}

@JsonIgnore
public List<String> getAllPaths() {
List<String> allPaths = new ArrayList<>();
Expand Down Expand Up @@ -131,6 +146,7 @@ public String toString() {
", options=" + options +
", templateName=" + templateName +
", domains=" + domains +
", edgeCacheDNS=" + edgeCacheDNS +
']';
}

Expand Down Expand Up @@ -169,6 +185,9 @@ public boolean equals(Object o) {
if (domains != null ? !domains.equals(service.domains) : service.domains != null) {
return false;
}
if (edgeCacheDNS != null ? !edgeCacheDNS.equals(service.edgeCacheDNS) : service.edgeCacheDNS != null) {
return false;
}

return true;
}
Expand All @@ -183,6 +202,7 @@ public int hashCode() {
result = 31 * result + (options != null ? options.hashCode() : 0);
result = 31 * result + (templateName != null ? templateName.hashCode() : 0);
result = 31 * result + (domains != null ? domains.hashCode() : 0);
result = 31 * result + (edgeCacheDNS != null ? edgeCacheDNS.hashCode() : 0);
return result;
}
}
4 changes: 4 additions & 0 deletions BaragonService/pom.xml
Expand Up @@ -124,6 +124,10 @@
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-guava</artifactId>
</dependency>
<dependency>
<groupId>com.hubspot</groupId>
<artifactId>HorizonCore</artifactId>
</dependency>
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-core</artifactId>
Expand Down
Expand Up @@ -29,8 +29,12 @@
import com.hubspot.baragon.data.BaragonConnectionStateListener;
import com.hubspot.baragon.data.BaragonWorkerDatastore;
import com.hubspot.baragon.service.config.BaragonConfiguration;
import com.hubspot.baragon.service.config.EdgeCacheConfiguration;
import com.hubspot.baragon.service.config.ElbConfiguration;
import com.hubspot.baragon.service.config.SentryConfiguration;
import com.hubspot.baragon.service.edgecache.EdgeCache;
import com.hubspot.baragon.service.edgecache.cloudflare.CloudflareEdgeCache;
import com.hubspot.baragon.service.edgecache.cloudflare.client.CloudflareClient;
import com.hubspot.baragon.service.elb.ApplicationLoadBalancer;
import com.hubspot.baragon.service.elb.ClassicLoadBalancer;
import com.hubspot.baragon.service.exceptions.BaragonExceptionNotifier;
Expand Down Expand Up @@ -99,6 +103,11 @@ public void configure(Binder binder) {
binder.bind(ServiceManager.class).in(Scopes.SINGLETON);
binder.bind(StatusManager.class).in(Scopes.SINGLETON);

// Edge Cache
binder.bind(CloudflareEdgeCache.class);
binder.bind(CloudflareClient.class);
binder.bind(EdgeCache.class).to(getConfiguration().getEdgeCacheConfiguration().getEdgeCache().getEdgeCacheClass());

// Workers
binder.bind(BaragonElbSyncWorker.class).in(Scopes.SINGLETON);
binder.bind(BaragonRequestWorker.class).in(Scopes.SINGLETON);
Expand All @@ -107,7 +116,6 @@ public void configure(Binder binder) {
binder.bind(ClassicLoadBalancer.class);
binder.bind(ApplicationLoadBalancer.class);


Multibinder<AbstractLatchListener> latchBinder = Multibinder.newSetBinder(binder, AbstractLatchListener.class);
latchBinder.addBinding().to(RequestWorkerListener.class).in(Scopes.SINGLETON);
latchBinder.addBinding().to(ElbSyncWorkerListener.class).in(Scopes.SINGLETON);
Expand Down Expand Up @@ -158,6 +166,11 @@ public Optional<ElbConfiguration> providesElbConfiguration(BaragonConfiguration
return configuration.getElbConfiguration();
}

@Provides
public EdgeCacheConfiguration providesEdgeCacheConfiguration(BaragonConfiguration configuration) {
return configuration.getEdgeCacheConfiguration();
}

@Provides
@Singleton
@Named(BARAGON_SERVICE_SCHEDULED_EXECUTOR)
Expand Down
Expand Up @@ -70,6 +70,9 @@ public class BaragonConfiguration extends Configuration {
@JsonProperty("elb")
private Optional<ElbConfiguration> elb = Optional.absent();

@JsonProperty("edgeCache")
private EdgeCacheConfiguration edgeCache = new EdgeCacheConfiguration();

@JsonProperty("ui")
@Valid
private UIConfiguration uiConfiguration = new UIConfiguration();
Expand Down Expand Up @@ -184,6 +187,14 @@ public void setElbConfiguration(Optional<ElbConfiguration> elb) {
this.elb = elb;
}

public EdgeCacheConfiguration getEdgeCacheConfiguration() {
return edgeCache;
}

public void setEdgeCacheConfiguration(EdgeCacheConfiguration edgeCache) {
this.edgeCache = edgeCache;
}

public UIConfiguration getUiConfiguration() {
return uiConfiguration;
}
Expand Down
@@ -0,0 +1,54 @@
package com.hubspot.baragon.service.config;

import java.util.HashMap;
import java.util.Map;

import javax.validation.constraints.NotNull;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.hubspot.baragon.service.edgecache.cloudflare.EdgeCacheClass;

@JsonIgnoreProperties(ignoreUnknown = true)
public class EdgeCacheConfiguration {

@JsonProperty
@NotNull
private boolean enabled = false;

@JsonProperty
@NotNull
private EdgeCacheClass edgeCache = EdgeCacheClass.CLOUDFLARE;

@JsonProperty
@NotNull
private Map<String, String> integrationSettings = new HashMap<>();

public boolean isEnabled() {
return enabled;
}

public EdgeCacheConfiguration setEnabled(boolean enabled) {
this.enabled = enabled;
return this;
}

public EdgeCacheClass getEdgeCache() {
return edgeCache;
}

public EdgeCacheConfiguration setEdgeCache(EdgeCacheClass edgeCache) {
this.edgeCache = edgeCache;
return this;
}

public Map<String, String> getIntegrationSettings() {
return integrationSettings;
}

public EdgeCacheConfiguration setIntegrationSettings(Map<String, String> integrationSettings) {
this.integrationSettings = integrationSettings;
return this;
}

}
@@ -0,0 +1,9 @@
package com.hubspot.baragon.service.edgecache;

import com.hubspot.baragon.models.BaragonRequest;

public interface EdgeCache {

boolean invalidateIfNecessary(BaragonRequest request);

}
@@ -0,0 +1,105 @@
package com.hubspot.baragon.service.edgecache.cloudflare;

import java.util.Collections;
import java.util.Optional;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.inject.Inject;
import com.hubspot.baragon.models.BaragonRequest;
import com.hubspot.baragon.service.config.EdgeCacheConfiguration;
import com.hubspot.baragon.service.edgecache.EdgeCache;
import com.hubspot.baragon.service.edgecache.cloudflare.client.CloudflareClient;
import com.hubspot.baragon.service.edgecache.cloudflare.client.CloudflareClientException;
import com.hubspot.baragon.service.edgecache.cloudflare.client.models.CloudflareDnsRecord;
import com.hubspot.baragon.service.edgecache.cloudflare.client.models.CloudflareZone;

/**
* An implementation of a proxying edge cache backed by Cloudflare.
*
* Config example:
* ...
* edgeCache:
* enabled: true
* edgeCacheClass: CLOUDFLARE
* integrationSettings:
* apiBase: https://api.cloudflare.com/client/v4/
* apiEmail: email@host.net
* apiKey: some-key
* cacheTagFormat: cache-tag-for-%-service
* ...
*/
public class CloudflareEdgeCache implements EdgeCache {
private static final Logger LOG = LoggerFactory.getLogger(CloudflareEdgeCache.class);

private final CloudflareClient cf;
private final EdgeCacheConfiguration edgeCacheConfiguration;

@Inject
public CloudflareEdgeCache(CloudflareClient cf, EdgeCacheConfiguration edgeCacheConfiguration) {
this.cf = cf;
this.edgeCacheConfiguration = edgeCacheConfiguration;
}

/**
* Invalidation will eventually occur when the TTL expires, so it's not a showstopper if this fails.
*/
@Override
public boolean invalidateIfNecessary(BaragonRequest request) {
if (!request.getLoadBalancerService().getEdgeCacheDNS().isPresent()) {
return false;
}

try {
String edgeCacheDNS = request.getLoadBalancerService().getEdgeCacheDNS().get();
Optional<CloudflareZone> matchingZone = getCloudflareZone(edgeCacheDNS);

if (!matchingZone.isPresent()) {
LOG.warn("`edgeCacheDNS` was defined on the request, but no matching Cloudflare Zone was found!");
return false;
}

String zoneId = matchingZone.get().getId();
Optional<CloudflareDnsRecord> matchingDnsRecord = getCloudflareDnsRecord(edgeCacheDNS, zoneId);

if (!matchingDnsRecord.isPresent()) {
LOG.warn("`edgeCacheDNS` was defined on the request, but no matching Cloudflare DNS Record was found!");
return false;
}

if (!matchingDnsRecord.get().isProxied()) {
LOG.warn("`edgeCacheDNS` was defined on the request, but {} is not a proxied DNS record!", edgeCacheDNS);
return false;
}

String cacheTag = String.format(
edgeCacheConfiguration.getIntegrationSettings().get("cacheTagFormat"),
request.getLoadBalancerService().getServiceId()
);

LOG.debug("Sending cache purge request against {} for {} to Cloudflare...", matchingDnsRecord.get().getName(), cacheTag);

return cf.purgeEdgeCache(zoneId, Collections.singletonList(cacheTag));

} catch (CloudflareClientException e) {
LOG.error("Unable to invalidate Cloudflare cache for request {}", request, e);
return false;
}
}

private Optional<CloudflareDnsRecord> getCloudflareDnsRecord(String edgeCacheDNS, String zoneId) throws CloudflareClientException {
return Optional.ofNullable(cf.getDnsRecord(zoneId, edgeCacheDNS));
}

private Optional<CloudflareZone> getCloudflareZone(String edgeCacheDNS) throws CloudflareClientException {
String baseDomain = getBaseDomain(edgeCacheDNS);
return Optional.ofNullable(cf.getZone(baseDomain));
}

private String getBaseDomain(String dns) {
String[] domainTokens = dns.split("\\.");
return String.format("%s.%s", domainTokens[domainTokens.length - 2], domainTokens[domainTokens.length - 1]);
}

}
@@ -0,0 +1,18 @@
package com.hubspot.baragon.service.edgecache.cloudflare;

import com.hubspot.baragon.service.edgecache.EdgeCache;

public enum EdgeCacheClass {
CLOUDFLARE(CloudflareEdgeCache.class);

private final Class<? extends EdgeCache> edgeCacheClass;

EdgeCacheClass(Class<? extends EdgeCache> edgeCacheClass) {
this.edgeCacheClass = edgeCacheClass;
}

public Class<? extends EdgeCache> getEdgeCacheClass() {
return edgeCacheClass;
}

}