blob: 77569095536f058efc3fae20c59c9eca20bdead3 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.brooklyn.entity.proxy.nginx;
import java.net.URI;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.entity.Group;
import org.apache.brooklyn.api.mgmt.SubscriptionHandle;
import org.apache.brooklyn.api.policy.PolicySpec;
import org.apache.brooklyn.api.sensor.SensorEvent;
import org.apache.brooklyn.api.sensor.SensorEventListener;
import org.apache.brooklyn.core.annotation.Effector;
import org.apache.brooklyn.core.entity.Attributes;
import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
import org.apache.brooklyn.core.entity.lifecycle.ServiceStateLogic.ServiceNotUpLogic;
import org.apache.brooklyn.core.feed.ConfigToAttributes;
import org.apache.brooklyn.enricher.stock.Enrichers;
import org.apache.brooklyn.entity.group.AbstractMembershipTrackingPolicy;
import org.apache.brooklyn.entity.proxy.AbstractControllerImpl;
import org.apache.brooklyn.entity.proxy.ProxySslConfig;
import org.apache.brooklyn.entity.proxy.nginx.NginxController.NginxControllerInternal;
import org.apache.brooklyn.feed.http.HttpFeed;
import org.apache.brooklyn.feed.http.HttpPollConfig;
import org.apache.brooklyn.feed.http.HttpValueFunctions;
import org.apache.brooklyn.util.core.ResourceUtils;
import org.apache.brooklyn.util.core.file.ArchiveUtils;
import org.apache.brooklyn.util.http.HttpTool;
import org.apache.brooklyn.util.http.HttpToolResponse;
import org.apache.brooklyn.util.guava.Functionals;
import org.apache.brooklyn.util.stream.Streams;
import org.apache.brooklyn.util.text.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Function;
import com.google.common.base.Predicates;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
/**
* Implementation of the {@link NginxController} entity.
*/
public class NginxControllerImpl extends AbstractControllerImpl implements NginxController, NginxControllerInternal {
private static final Logger LOG = LoggerFactory.getLogger(NginxControllerImpl.class);
private volatile HttpFeed httpFeed;
private final Set<String> installedKeysCache = Sets.newLinkedHashSet();
protected UrlMappingsMemberTrackerPolicy urlMappingsMemberTrackerPolicy;
protected SubscriptionHandle targetAddressesHandler;
@Override
public void reload() {
NginxSshDriver driver = (NginxSshDriver)getDriver();
if (driver==null) {
Lifecycle state = getAttribute(NginxController.SERVICE_STATE_ACTUAL);
throw new IllegalStateException("Cannot reload (no driver instance; stopped? (state="+state+")");
}
driver.reload();
}
@Override
public boolean isSticky() {
return getConfig(STICKY);
}
private class UrlInferencer implements Supplier<URI> {
private Map<String, String> parameters;
private UrlInferencer(Map<String,String> parameters) {
this.parameters = parameters;
}
@Override public URI get() {
String baseUrl = inferUrl(true);
if (parameters==null || parameters.isEmpty())
return URI.create(baseUrl);
return URI.create(baseUrl+"?"+HttpTool.encodeUrlParams(parameters));
}
}
@Override
public void connectSensors() {
super.connectSensors();
ConfigToAttributes.apply(this);
// "up" is defined as returning a valid HTTP response from nginx (including a 404 etc)
httpFeed = addFeed(HttpFeed.builder()
.uniqueTag("nginx-poll")
.entity(this)
.period(getConfig(HTTP_POLL_PERIOD))
.baseUri(new UrlInferencer(null))
.poll(new HttpPollConfig<Boolean>(NGINX_URL_ANSWERS_NICELY)
// Any response from Nginx is good.
.checkSuccess(Predicates.alwaysTrue())
// Accept any nginx response (don't assert specific version), so that sub-classing
// for a custom nginx build is not strict about custom version numbers in headers
.onResult(HttpValueFunctions.containsHeader("Server"))
.setOnException(false)
.suppressDuplicates(true))
.build());
// TODO PERSISTENCE WORKAROUND kept anonymous function in case referenced in persisted state
new Function<HttpToolResponse, Boolean>() {
@Override
public Boolean apply(HttpToolResponse input) {
// Accept any nginx response (don't assert specific version), so that sub-classing
// for a custom nginx build is not strict about custom version numbers in headers
List<String> actual = input.getHeaderLists().get("Server");
return actual != null && actual.size() == 1;
}
};
if (!Lifecycle.RUNNING.equals(getAttribute(SERVICE_STATE_ACTUAL))) {
// TODO when updating the map, if it would change from empty to empty on a successful run
// gate with the above check to prevent flashing on ON_FIRE during rebind (this is invoked on rebind as well as during start)
ServiceNotUpLogic.updateNotUpIndicator(this, NGINX_URL_ANSWERS_NICELY, "No response from nginx yet");
}
enrichers().add(Enrichers.builder().updatingMap(Attributes.SERVICE_NOT_UP_INDICATORS)
.uniqueTag("not-up-unless-url-answers")
.from(NGINX_URL_ANSWERS_NICELY)
.computing(Functionals.ifNotEquals(true).value("URL where nginx listens is not answering correctly (with expected header)") )
.build());
connectServiceUpIsRunning();
// Can guarantee that parent/managementContext has been set
Group urlMappings = getConfig(URL_MAPPINGS);
if (urlMappings!=null && urlMappingsMemberTrackerPolicy==null) {
// Listen to the targets of each url-mapping changing
targetAddressesHandler = subscriptions().subscribeToMembers(urlMappings, UrlMapping.TARGET_ADDRESSES, new SensorEventListener<Collection<String>>() {
@Override public void onEvent(SensorEvent<Collection<String>> event) {
updateNeeded();
}
});
// Listen to url-mappings being added and removed
urlMappingsMemberTrackerPolicy = policies().add(PolicySpec.create(UrlMappingsMemberTrackerPolicy.class)
.configure("group", urlMappings));
}
}
protected void removeUrlMappingsMemberTrackerPolicy() {
if (urlMappingsMemberTrackerPolicy != null) {
policies().remove(urlMappingsMemberTrackerPolicy);
urlMappingsMemberTrackerPolicy = null;
}
Group urlMappings = getConfig(URL_MAPPINGS);
if (urlMappings!=null && targetAddressesHandler!=null) {
subscriptions().unsubscribe(urlMappings, targetAddressesHandler);
targetAddressesHandler = null;
}
}
public static class UrlMappingsMemberTrackerPolicy extends AbstractMembershipTrackingPolicy {
@Override
protected void onEntityEvent(EventType type, Entity entity) {
// relies on policy-rebind injecting the implementation rather than the dynamic-proxy
((NginxControllerImpl)super.entity).updateNeeded();
}
}
@Override
protected void preStop() {
super.preStop();
removeUrlMappingsMemberTrackerPolicy();
}
@Override
protected void postStop() {
// TODO don't want stop to race with the last poll.
super.postStop();
sensors().set(SERVICE_UP, false);
}
@Override
protected void disconnectSensors() {
if (httpFeed != null) httpFeed.stop();
disconnectServiceUpIsRunning();
super.disconnectSensors();
}
@Override
public Class<?> getDriverInterface() {
return NginxDriver.class;
}
@Override
public NginxDriver getDriver() {
return (NginxDriver) super.getDriver();
}
@Override
public void doExtraConfigurationDuringStart() {
computePortsAndUrls();
reconfigureService();
// reconnect sensors if ports have changed
connectSensors();
}
@Override
@Effector(description="Gets the current server configuration (by brooklyn recalculating what the config should be); does not affect the server")
public String getCurrentConfiguration() {
return getConfigFile();
}
@Override
@Effector(description="Deploys an archive of static content to the server")
public void deploy(String archiveUrl) {
NginxSshDriver driver = (NginxSshDriver) getDriver();
if (driver==null) {
if (LOG.isDebugEnabled())
LOG.debug("No driver for {}, so not deploying archive (is entity stopping? state={})",
this, getAttribute(NginxController.SERVICE_STATE_ACTUAL));
return;
}
// Copy to the destination machine and extract contents
ArchiveUtils.deploy(archiveUrl, driver.getMachine(), driver.getRunDir());
}
@Override
public void reconfigureService() {
String cfg = getConfigFile();
if (cfg == null) return;
if (LOG.isDebugEnabled()) LOG.debug("Reconfiguring {}, targetting {} and {}", new Object[] {this, getServerPoolAddresses(), getUrlMappings()});
if (LOG.isTraceEnabled()) LOG.trace("Reconfiguring {}, config file:\n{}", this, cfg);
NginxSshDriver driver = (NginxSshDriver) getDriver();
if (!driver.isCustomizationCompleted()) {
if (LOG.isDebugEnabled()) LOG.debug("Reconfiguring {}, but driver's customization not yet complete so aborting", this);
return;
}
driver.getMachine().copyTo(Streams.newInputStreamWithContents(cfg), driver.getRunDir()+"/conf/server.conf");
installSslKeys("global", getSslConfig());
for (UrlMapping mapping : getUrlMappings()) {
//cache ensures only the first is installed, which is what is assumed below
installSslKeys(mapping.getDomain(), mapping.getConfig(UrlMapping.SSL_CONFIG));
}
}
/**
* Installs SSL keys named as {@code id.crt} and {@code id.key} where nginx can find them.
* <p>
* Currently skips re-installs (does not support changing)
*/
public void installSslKeys(String id, ProxySslConfig ssl) {
if (ssl == null) return;
if (installedKeysCache.contains(id)) return;
NginxSshDriver driver = (NginxSshDriver) getDriver();
if (!Strings.isEmpty(ssl.getCertificateSourceUrl())) {
String certificateDestination = Strings.isEmpty(ssl.getCertificateDestination()) ? driver.getRunDir() + "/conf/" + id + ".crt" : ssl.getCertificateDestination();
driver.getMachine().copyTo(ImmutableMap.of("permissions", "0600"),
ResourceUtils.create(this).getResourceFromUrl(ssl.getCertificateSourceUrl()),
certificateDestination);
}
if (!Strings.isEmpty(ssl.getKeySourceUrl())) {
String keyDestination = Strings.isEmpty(ssl.getKeyDestination()) ? driver.getRunDir() + "/conf/" + id + ".key" : ssl.getKeyDestination();
driver.getMachine().copyTo(ImmutableMap.of("permissions", "0600"),
ResourceUtils.create(this).getResourceFromUrl(ssl.getKeySourceUrl()),
keyDestination);
}
installedKeysCache.add(id);
}
@Override
public String getConfigFile() {
NginxSshDriver driver = (NginxSshDriver) getDriver();
if (driver==null) {
LOG.debug("No driver for {}, so not generating config file (is entity stopping? state={})",
this, getAttribute(NginxController.SERVICE_STATE_ACTUAL));
return null;
}
NginxConfigFileGenerator templateGenerator = getConfig(NginxController.SERVER_CONF_GENERATOR);
return templateGenerator.generateConfigFile(driver, this);
}
@Override
public Iterable<UrlMapping> getUrlMappings() {
// For mapping by URL
Group urlMappingGroup = getConfig(NginxController.URL_MAPPINGS);
if (urlMappingGroup != null) {
return Iterables.filter(urlMappingGroup.getMembers(), UrlMapping.class);
} else {
return Collections.<UrlMapping>emptyList();
}
}
@Override
public String getShortName() {
return "Nginx";
}
@Override
public boolean appendSslConfig(String id,
StringBuilder out,
String prefix,
ProxySslConfig ssl,
boolean sslBlock,
boolean certificateBlock) {
if (ssl == null)
return false;
if (sslBlock) {
out.append(prefix);
out.append("ssl on;\n");
}
if (ssl.getReuseSessions()) {
out.append(prefix);
out.append("proxy_ssl_session_reuse on;");
}
if (certificateBlock) {
String cert;
if (Strings.isEmpty(ssl.getCertificateDestination())) {
cert = "" + id + ".crt";
} else {
cert = ssl.getCertificateDestination();
}
out.append(prefix);
out.append("ssl_certificate " + cert + ";\n");
String key;
if (!Strings.isEmpty(ssl.getKeyDestination())) {
key = ssl.getKeyDestination();
} else if (!Strings.isEmpty(ssl.getKeySourceUrl())) {
key = "" + id + ".key";
} else {
key = null;
}
if (key != null) {
out.append(prefix);
out.append("ssl_certificate_key " + key + ";\n");
}
}
return true;
}
}