package io.cattle.platform.allocator.service;
import static io.cattle.platform.core.model.tables.ServiceTable.*;
import io.cattle.platform.allocator.constraint.AffinityConstraintDefinition;
import io.cattle.platform.allocator.constraint.AffinityConstraintDefinition.AffinityOps;
import io.cattle.platform.allocator.constraint.Constraint;
import io.cattle.platform.allocator.constraint.ContainerAffinityConstraint;
import io.cattle.platform.allocator.constraint.ContainerLabelAffinityConstraint;
import io.cattle.platform.allocator.constraint.HostAffinityConstraint;
import io.cattle.platform.allocator.dao.AllocatorDao;
import io.cattle.platform.allocator.lock.AllocateConstraintLock;
import io.cattle.platform.core.constants.InstanceConstants;
import io.cattle.platform.core.dao.InstanceDao;
import io.cattle.platform.core.dao.LabelsDao;
import io.cattle.platform.core.model.Host;
import io.cattle.platform.core.model.Instance;
import io.cattle.platform.core.model.Service;
import io.cattle.platform.json.JsonMapper;
import io.cattle.platform.lock.definition.LockDefinition;
import io.cattle.platform.object.ObjectManager;
import io.cattle.platform.object.util.DataAccessor;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.inject.Inject;
import org.jooq.tools.StringUtils;
public class AllocationHelperImpl implements AllocationHelper {
private static final String SERVICE_NAME_MACRO = "${service_name}";
private static final String STACK_NAME_MACRO = "${stack_name}";
// LEGACY: Temporarily support ${project_name} but this has become ${stack_name} now
private static final String PROJECT_NAME_MACRO = "${project_name}";
// TODO: We should refactor since these are defined in ServiceDiscoveryConstants too
private static final String LABEL_STACK_NAME = "io.rancher.stack.name";
private static final String LABEL_STACK_SERVICE_NAME = "io.rancher.stack_service.name";
private static final String LABEL_PROJECT_SERVICE_NAME = "io.rancher.project_service.name";
private static final String LABEL_SERVICE_LAUNCH_CONFIG = "io.rancher.service.launch.config";
private static final String PRIMARY_LAUNCH_CONFIG_NAME = "io.rancher.service.primary.launch.config";
@Inject
LabelsDao labelsDao;
@Inject
AllocatorDao allocatorDao;
@Inject
InstanceDao instanceDao;
@Inject
ObjectManager objectManager;
@Inject
JsonMapper jsonMapper;
@Inject
AllocatorService allocatorService;
@Override
public List<Long> getAllHostsSatisfyingHostAffinity(Long accountId, Map<String, String> labelConstraints) {
return getHostsSatisfyingHostAffinityInternal(true, accountId, labelConstraints);
}
@Override
public List<Long> getHostsSatisfyingHostAffinity(Long accountId, Map<String, String> labelConstraints) {
return getHostsSatisfyingHostAffinityInternal(false, accountId, labelConstraints);
}
protected List<Long> getHostsSatisfyingHostAffinityInternal(boolean includeRemoved, Long accountId, Map<String, String> labelConstraints) {
List<? extends Host> hosts = includeRemoved ? allocatorDao.getNonPurgedHosts(accountId) : allocatorDao.getActiveHosts(accountId);
List<Constraint> hostAffinityConstraints = getHostAffinityConstraintsFromLabels(labelConstraints);
List<Long> acceptableHostIds = new ArrayList<Long>();
for (Host host : hosts) {
if (hostSatisfiesHostAffinity(host.getId(), hostAffinityConstraints)) {
acceptableHostIds.add(host.getId());
}
}
return acceptableHostIds;
}
private List<Constraint> getHostAffinityConstraintsFromLabels(Map<String, String> labelConstraints) {
List<Constraint> constraints = extractConstraintsFromLabels(labelConstraints, null);
List<Constraint> hostConstraints = new ArrayList<Constraint>();
for (Constraint constraint : constraints) {
if (constraint instanceof HostAffinityConstraint) {
hostConstraints.add(constraint);
}
}
return hostConstraints;
}
private boolean hostSatisfiesHostAffinity(long hostId, List<Constraint> hostAffinityConstraints) {
for (Constraint constraint: hostAffinityConstraints) {
AllocationCandidate candidate = new AllocationCandidate();
candidate.setHost(hostId);
if (!constraint.matches(candidate)) {
return false;
}
}
return true;
}
@Override
public void normalizeLabels(long stackId, Map<String, String> systemLabels, Map<String, String> serviceUserLabels) {
String stackName = systemLabels.get(LABEL_STACK_NAME);
String stackServiceNameWithLaunchConfig = systemLabels.get(LABEL_STACK_SERVICE_NAME);
String launchConfig = systemLabels.get(LABEL_SERVICE_LAUNCH_CONFIG);
Set<String> serviceNamesInStack = getServiceNamesInStack(stackId);
for (Map.Entry<String, String> entry : serviceUserLabels.entrySet()) {
String labelValue = entry.getValue();
if (entry.getKey().startsWith(ContainerLabelAffinityConstraint.LABEL_HEADER_AFFINITY_CONTAINER_LABEL) &&
labelValue != null) {
String userEnteredServiceName = null;
if (labelValue.startsWith(LABEL_STACK_SERVICE_NAME)) {
userEnteredServiceName = labelValue.substring(LABEL_STACK_SERVICE_NAME.length() + 1);
} else if (labelValue.startsWith(LABEL_PROJECT_SERVICE_NAME)) {
userEnteredServiceName = labelValue.substring(LABEL_PROJECT_SERVICE_NAME.length() + 1);
}
if (userEnteredServiceName != null) {
String[] components = userEnteredServiceName.split("/");
if (components.length == 1 &&
stackServiceNameWithLaunchConfig != null) {
if (serviceNamesInStack.contains(userEnteredServiceName.toLowerCase())) {
// prepend stack name
userEnteredServiceName = stackName + "/" + userEnteredServiceName;
}
}
if (!PRIMARY_LAUNCH_CONFIG_NAME.equals(launchConfig) &&
stackServiceNameWithLaunchConfig.startsWith(userEnteredServiceName)) {
// automatically append secondary launchConfig
userEnteredServiceName = userEnteredServiceName + "/" + launchConfig;
}
entry.setValue(LABEL_STACK_SERVICE_NAME + "=" + userEnteredServiceName);
}
}
}
}
// TODO: Fix repeated DB call even if DB's cache no longer hits the disk
private Set<String> getServiceNamesInStack(long stackId) {
Set<String> servicesInEnv = new HashSet<String>();
List<? extends Service> services = objectManager.find(Service.class, SERVICE.STACK_ID, stackId, SERVICE.REMOVED,
null);
for (Service service : services) {
servicesInEnv.add(service.getName().toLowerCase());
}
return servicesInEnv;
}
@Override
public void mergeLabels(Map<String, String> srcMap, Map<String, String> destMap) {
if (srcMap == null || destMap == null) {
return;
}
for (Map.Entry<String, String> entry : srcMap.entrySet()) {
String key = entry.getKey();
if (key.toLowerCase().startsWith("io.rancher")) {
key = key.toLowerCase();
}
String value = entry.getValue();
if (key.startsWith("io.rancher.scheduler.affinity")) {
// merge labels
String destValue = destMap.get(key);
if (StringUtils.isEmpty(destValue)) {
destMap.put(key, value);
} else if (StringUtils.isEmpty(value)) {
continue;
} else if (!destValue.toLowerCase().contains(value.toLowerCase())) {
destMap.put(key, destValue + "," + value);
}
} else {
// overwrite label value
destMap.put(key, value);
}
}
}
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public List<Constraint> extractConstraintsFromEnv(Map env) {
List<Constraint> constraints = new ArrayList<Constraint>();
if (env != null) {
Set<String> affinityDefinitions = env.keySet();
for (String affinityDef : affinityDefinitions) {
if (affinityDef == null) {
continue;
}
if (affinityDef.startsWith(ContainerAffinityConstraint.ENV_HEADER_AFFINITY_CONTAINER)) {
affinityDef = affinityDef.substring(ContainerAffinityConstraint.ENV_HEADER_AFFINITY_CONTAINER.length());
AffinityConstraintDefinition def = extractAffinitionConstraintDefinitionFromEnv(affinityDef);
if (def != null && !StringUtils.isEmpty(def.getValue())) {
constraints.add(new ContainerAffinityConstraint(def, objectManager, instanceDao));
}
} else if (affinityDef.startsWith(HostAffinityConstraint.ENV_HEADER_AFFINITY_HOST_LABEL)) {
affinityDef = affinityDef.substring(HostAffinityConstraint.ENV_HEADER_AFFINITY_HOST_LABEL.length());
AffinityConstraintDefinition def = extractAffinitionConstraintDefinitionFromEnv(affinityDef);
if (def != null && !StringUtils.isEmpty(def.getKey())) {
constraints.add(new HostAffinityConstraint(def, allocatorDao));
}
} else if (affinityDef.startsWith(ContainerLabelAffinityConstraint.ENV_HEADER_AFFINITY_CONTAINER_LABEL)) {
affinityDef = affinityDef.substring(ContainerLabelAffinityConstraint.ENV_HEADER_AFFINITY_CONTAINER_LABEL.length());
AffinityConstraintDefinition def = extractAffinitionConstraintDefinitionFromEnv(affinityDef);
if (def != null && !StringUtils.isEmpty(def.getKey())) {
constraints.add(new ContainerLabelAffinityConstraint(def, allocatorDao));
}
}
}
}
return constraints;
}
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public List<Constraint> extractConstraintsFromLabels(Map labels, Instance instance) {
List<Constraint> constraints = new ArrayList<Constraint>();
if (labels == null) {
return constraints;
}
Iterator<Map.Entry> iter = labels.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry affinityDef = iter.next();
String key = ((String)affinityDef.getKey()).toLowerCase();
String valueStr = (String)affinityDef.getValue();
valueStr = valueStr == null ? "" : valueStr.toLowerCase();
if (instance != null) {
// TODO: Possibly memoize the macros so we don't need to redo the queries for Service and Environment
valueStr = evaluateMacros(valueStr, instance);
}
String opStr = "";
if (key.startsWith(ContainerLabelAffinityConstraint.LABEL_HEADER_AFFINITY_CONTAINER_LABEL)) {
opStr = key.substring(ContainerLabelAffinityConstraint.LABEL_HEADER_AFFINITY_CONTAINER_LABEL.length());
List<AffinityConstraintDefinition> defs = extractAffinityConstraintDefinitionFromLabel(opStr, valueStr, true);
for (AffinityConstraintDefinition def: defs) {
constraints.add(new ContainerLabelAffinityConstraint(def, allocatorDao));
}
} else if (key.startsWith(ContainerAffinityConstraint.LABEL_HEADER_AFFINITY_CONTAINER)) {
opStr = key.substring(ContainerAffinityConstraint.LABEL_HEADER_AFFINITY_CONTAINER.length());
List<AffinityConstraintDefinition> defs = extractAffinityConstraintDefinitionFromLabel(opStr, valueStr, false);
for (AffinityConstraintDefinition def: defs) {
constraints.add(new ContainerAffinityConstraint(def, objectManager, instanceDao));
}
} else if (key.startsWith(HostAffinityConstraint.LABEL_HEADER_AFFINITY_HOST_LABEL)) {
opStr = key.substring(HostAffinityConstraint.LABEL_HEADER_AFFINITY_HOST_LABEL.length());
List<AffinityConstraintDefinition> defs = extractAffinityConstraintDefinitionFromLabel(opStr, valueStr, true);
for (AffinityConstraintDefinition def: defs) {
constraints.add(new HostAffinityConstraint(def, allocatorDao));
}
}
}
return constraints;
}
/**
* Supported macros
* ${service_name}
* ${stack_name}
* LEGACY:
* ${project_name}
*
* @param valueStr
* @param instance
* @return
*/
@SuppressWarnings("unchecked")
private String evaluateMacros(String valueStr, Instance instance) {
if (valueStr.indexOf(SERVICE_NAME_MACRO) != -1 ||
valueStr.indexOf(STACK_NAME_MACRO) != -1 ||
valueStr.indexOf(PROJECT_NAME_MACRO) != -1) {
Map<String, String> labels = DataAccessor.fields(instance).withKey(InstanceConstants.FIELD_LABELS).as(Map.class);
String serviceLaunchConfigName = "";
String stackName = "";
if (labels != null && !labels.isEmpty()) {
for (Map.Entry<String, String> label : labels.entrySet()) {
if (LABEL_STACK_NAME.equals(label.getKey())) {
stackName = label.getValue();
} else if (LABEL_STACK_SERVICE_NAME.equals(label.getKey())) {
if (label.getValue() != null) {
int i = label.getValue().indexOf('/');
if (i != -1) {
serviceLaunchConfigName = label.getValue().substring(i + 1);
}
}
}
}
}
if (!StringUtils.isBlank(stackName)) {
valueStr = valueStr.replace(STACK_NAME_MACRO, stackName);
// LEGACY: ${project_name} rename ${stack_name}
valueStr = valueStr.replace(PROJECT_NAME_MACRO, stackName);
}
if (!StringUtils.isBlank(serviceLaunchConfigName)) {
valueStr = valueStr.replace(SERVICE_NAME_MACRO, serviceLaunchConfigName);
}
}
return valueStr;
}
private AffinityConstraintDefinition extractAffinitionConstraintDefinitionFromEnv(String definitionString) {
for (AffinityOps op : AffinityOps.values()) {
int i = definitionString.indexOf(op.getEnvSymbol());
if (i != -1) {
String key = definitionString.substring(0, i);
String value = definitionString.substring(i + op.getEnvSymbol().length());
return new AffinityConstraintDefinition(op, key, value);
}
}
return null;
}
private List<AffinityConstraintDefinition> extractAffinityConstraintDefinitionFromLabel(String opStr, String valueStr, boolean keyValuePairs) {
List<AffinityConstraintDefinition> defs = new ArrayList<AffinityConstraintDefinition>();
AffinityOps affinityOp = null;
for (AffinityOps op : AffinityOps.values()) {
if (op.getLabelSymbol().equals(opStr)) {
affinityOp = op;
break;
}
}
if (affinityOp == null) {
return defs;
}
if (StringUtils.isEmpty(valueStr)) {
return defs;
}
String[] values = valueStr.split(",");
for (String value : values) {
if (StringUtils.isEmpty(value)) {
continue;
}
if (keyValuePairs && value.indexOf('=') != -1) {
String[] pair = value.split("=");
defs.add(new AffinityConstraintDefinition(affinityOp, pair[0], pair[1]));
} else {
defs.add(new AffinityConstraintDefinition(affinityOp, null, value));
}
}
return defs;
}
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public List<LockDefinition> extractAllocationLockDefinitions(Instance instance, List<Instance> instances) {
Map env = DataAccessor.fields(instance).withKey(InstanceConstants.FIELD_ENVIRONMENT).as(jsonMapper, Map.class);
List<LockDefinition> lockDefs = extractAllocationLockDefinitionsFromEnv(env);
Map labels = DataAccessor.fields(instance).withKey(InstanceConstants.FIELD_LABELS).as(jsonMapper, Map.class);
if (labels == null) {
return lockDefs;
}
// we need to merge all the affinity labels from primary containers and sickkicks
for (Instance inst: instances) {
Map lbs = DataAccessor.fields(inst).withKey(InstanceConstants.FIELD_LABELS).as(jsonMapper, Map.class);
Iterator<Map.Entry> it = lbs.entrySet().iterator();
while (it.hasNext()) {
Map.Entry affinityDef = it.next();
String key = ((String)affinityDef.getKey()).toLowerCase();
String valueStr = (String)affinityDef.getValue();
if (key.startsWith(ContainerLabelAffinityConstraint.LABEL_HEADER_AFFINITY_CONTAINER_LABEL) ||
key.startsWith(ContainerAffinityConstraint.LABEL_HEADER_AFFINITY_CONTAINER)) {
labels.put(key, valueStr);
}
}
}
Iterator<Map.Entry> iter = labels.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry affinityDef = iter.next();
String key = ((String)affinityDef.getKey()).toLowerCase();
String valueStr = (String)affinityDef.getValue();
valueStr = valueStr == null ? "" : valueStr.toLowerCase();
if (instance != null) {
valueStr = evaluateMacros(valueStr, instance);
}
String opStr = "";
if (key.startsWith(ContainerLabelAffinityConstraint.LABEL_HEADER_AFFINITY_CONTAINER_LABEL)) {
opStr = key.substring(ContainerLabelAffinityConstraint.LABEL_HEADER_AFFINITY_CONTAINER_LABEL.length());
List<AffinityConstraintDefinition> defs = extractAffinityConstraintDefinitionFromLabel(opStr, valueStr, true);
for (AffinityConstraintDefinition def: defs) {
lockDefs.add(new AllocateConstraintLock(AllocateConstraintLock.Type.AFFINITY, def.getValue()));
}
} else if (key.startsWith(ContainerAffinityConstraint.LABEL_HEADER_AFFINITY_CONTAINER)) {
opStr = key.substring(ContainerAffinityConstraint.LABEL_HEADER_AFFINITY_CONTAINER.length());
List<AffinityConstraintDefinition> defs = extractAffinityConstraintDefinitionFromLabel(opStr, valueStr, false);
for (AffinityConstraintDefinition def: defs) {
lockDefs.add(new AllocateConstraintLock(AllocateConstraintLock.Type.AFFINITY, def.getValue()));
}
}
}
return lockDefs;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private List<LockDefinition> extractAllocationLockDefinitionsFromEnv(Map env) {
List<LockDefinition> constraints = new ArrayList<LockDefinition>();
if (env != null) {
Set<String> affinityDefinitions = env.keySet();
for (String affinityDef : affinityDefinitions) {
if (affinityDef == null) {
continue;
}
if (affinityDef.startsWith(ContainerAffinityConstraint.ENV_HEADER_AFFINITY_CONTAINER)) {
affinityDef = affinityDef.substring(ContainerAffinityConstraint.ENV_HEADER_AFFINITY_CONTAINER.length());
AffinityConstraintDefinition def = extractAffinitionConstraintDefinitionFromEnv(affinityDef);
if (def != null && !StringUtils.isEmpty(def.getValue())) {
constraints.add(new AllocateConstraintLock(AllocateConstraintLock.Type.AFFINITY, def.getValue()));
}
} else if (affinityDef.startsWith(ContainerLabelAffinityConstraint.ENV_HEADER_AFFINITY_CONTAINER_LABEL)) {
affinityDef = affinityDef.substring(ContainerLabelAffinityConstraint.ENV_HEADER_AFFINITY_CONTAINER_LABEL.length());
AffinityConstraintDefinition def = extractAffinitionConstraintDefinitionFromEnv(affinityDef);
if (def != null && !StringUtils.isEmpty(def.getKey())) {
constraints.add(new AllocateConstraintLock(AllocateConstraintLock.Type.AFFINITY, def.getValue()));
}
}
}
}
return constraints;
}
}