| /* |
| * 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; |
| } |
| } |