/* * 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 static org.apache.brooklyn.util.JavaGroovyEquivalents.groovyTruth; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Set; import org.apache.brooklyn.api.entity.Entity; import org.apache.brooklyn.api.mgmt.SubscriptionHandle; import org.apache.brooklyn.api.sensor.SensorEvent; import org.apache.brooklyn.api.sensor.SensorEventListener; import org.apache.brooklyn.core.entity.Attributes; import org.apache.brooklyn.core.entity.Entities; import org.apache.brooklyn.core.entity.EntityPredicates; import org.apache.brooklyn.core.entity.trait.Changeable; import org.apache.brooklyn.core.entity.trait.Startable; import org.apache.brooklyn.entity.group.AbstractGroupImpl; import org.apache.brooklyn.entity.proxy.ProxySslConfig; import org.apache.brooklyn.entity.webapp.WebAppServiceConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; /** * This is a group whose members will be made available to a load-balancer / URL forwarding service (such as nginx). * <p> * Configuration requires a <b>domain</b> and some mechanism for finding members. * The easiest way to find members is using a <b>target</b> whose children will be tracked, * but alternative membership policies can also be used. */ public class UrlMappingImpl extends AbstractGroupImpl implements UrlMapping { private static final Logger log = LoggerFactory.getLogger(UrlMapping.class); public UrlMappingImpl() { super(); } @Override public String getUniqueLabel() { String l = getConfig(LABEL); if (groovyTruth(l)) return getId()+"-"+l; else return getId(); } /** adds a rewrite rule, must be called at config time. see {@link UrlRewriteRule} for more info. */ @Override public synchronized UrlMapping addRewrite(String from, String to) { return addRewrite(new UrlRewriteRule(from, to)); } /** adds a rewrite rule, must be called at config time. see {@link UrlRewriteRule} for more info. */ @Override public synchronized UrlMapping addRewrite(UrlRewriteRule rule) { Collection<UrlRewriteRule> rewrites = getConfig(REWRITES); if (rewrites==null) { rewrites = new ArrayList<UrlRewriteRule>(); } rewrites.add(rule); config().set(REWRITES, rewrites); return this; } @Override public String getDomain() { return Preconditions.checkNotNull( getConfig(DOMAIN), "domain config argument required"); } @Override public String getPath() { return getConfig(PATH); } @Override public Entity getTarget() { return getConfig(TARGET_PARENT); } @Override public void setTarget(Entity target) { config().set(TARGET_PARENT, target); recompute(); } @Override public void onManagementStarting() { super.onManagementStarting(); if (getConfig(TARGET_PARENT) != null) { recompute(); // following line could be more efficient (just modify the addresses set, not clearing it each time; // but since addresses is lazy loaded not that big a deal) // subscribe(this, Changeable.GROUP_SIZE, { resetAddresses(true) } as SensorEventListener); // above not needed since our target tracking figures this out } } /** defines how address string, ie hostname:port, is constructed from a given entity. * returns null if not possible. * <p> * the default is to look at HOSTNAME and HTTPS_PORT or HTTP_PORT attribute sensors (depending on SSL_CONFIG being set with targetIsSsl). * <p> * this method is suitable (intended) for overriding if needed. */ protected String getAddressOfEntity(Entity s) { String h = s.getAttribute(Attributes.HOSTNAME); Integer p = null; Set<String> protos = s.getAttribute(WebAppServiceConstants.ENABLED_PROTOCOLS); ProxySslConfig sslConfig = getConfig(SSL_CONFIG); if (sslConfig != null && sslConfig.getTargetIsSsl()) { // use ssl if (protos != null && hasProtocol(protos, "https")) { // proto configured correctly } else { // proto not defined; use https anyway, but it might fail log.warn("Misconfiguration for "+this+": ENABLED_PROTOCOLS='"+protos+"' for "+s+" but sslConfig="+sslConfig); } p = s.getAttribute(Attributes.HTTPS_PORT); if (p == null) log.warn("Misconfiguration for "+this+": sslConfig="+sslConfig+" but no HTTPS_PORT on "+s); } if (p == null) { // default to http p = s.getAttribute(Attributes.HTTP_PORT); } if (groovyTruth(h) && p != null) return h+":"+p; log.error("Unable to construct hostname:port representation for "+s+"; skipping in "+this); return null; } protected synchronized void recomputeAddresses() { Set<String> resultM = Sets.newLinkedHashSet(); for (Entity s: getMembers()) { String hp = getAddressOfEntity(s); if (hp != null) resultM.add(hp); } Set<String> result = Collections.unmodifiableSet(resultM); Collection<String> oldAddresses = getAttribute(TARGET_ADDRESSES); if (oldAddresses == null || !result.equals(ImmutableSet.copyOf(oldAddresses))) { sensors().set(TARGET_ADDRESSES, result); } } public Collection<String> getTargetAddresses() { return getAttribute(TARGET_ADDRESSES); } public ProxySslConfig getSsl() { return getConfig(SSL_CONFIG); } // FIXME Do we really need this?! protected SubscriptionHandle getSubscriptionHandle() { return subscriptionHandle; } private SubscriptionHandle subscriptionHandle; private SubscriptionHandle subscriptionHandle2; @Override public synchronized void recompute() { if (subscriptionHandle != null) subscriptions().unsubscribe(subscriptionHandle); if (subscriptionHandle2 != null) subscriptions().unsubscribe(subscriptionHandle2); Entity t = getTarget(); if (t != null) { subscriptionHandle = subscriptions().subscribeToChildren(t, Startable.SERVICE_UP, new SensorEventListener<Boolean>() { @Override public void onEvent(SensorEvent<Boolean> event) { boolean changed = (event.getValue()) ? addMember(event.getSource()) : removeMember(event.getSource()); if (changed) { recomputeAddresses(); } }}); subscriptionHandle2 = subscriptions().subscribe(t, Changeable.MEMBER_REMOVED, new SensorEventListener<Entity>() { @Override public void onEvent(SensorEvent<Entity> event) { removeMember(event.getValue()); // recompute, irrespective of change, because framework may have already invoked the removeMember call recomputeAddresses(); }}); setMembers(t.getChildren(), EntityPredicates.attributeEqualTo(Startable.SERVICE_UP, true)); } recomputeAddresses(); } @Override public void discard() { Entities.unmanage(this); } private boolean hasProtocol(Collection<String> protocols, String desired) { for (String contender : protocols) { if ("https".equals(contender.toLowerCase())) return true; } return false; } }