blob: 254f875268871a22c6c74f4c8a8ba5c84e05d600 [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.ambari.server.topology;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Configuration for a topology entity such as a blueprint, hostgroup or cluster.
*/
public class Configuration {
private static final Logger LOG = LoggerFactory.getLogger(Configuration.class);
/**
* properties for this configuration instance
*/
private Map<String, Map<String, String>> properties;
/**
* attributes for this configuration instance
*/
private Map<String, Map<String, Map<String, String>>> attributes;
/**
* parent configuration
*/
private Configuration parentConfiguration;
public static Configuration newEmpty() {
return new Configuration(new HashMap<>(), new HashMap<>());
}
/**
* Apply configuration changes from {@code updatedConfigs} to {@code config}, but only
* change properties that are either absent in the existing config, or have values that
* match the stack default config.
*
* @return config types that had any updates applied
*/
public Set<String> applyUpdatesToStackDefaultProperties(Configuration stackDefaultConfig, Map<String, Map<String, String>> existingConfigurations, Map<String, Map<String, String>> updatedConfigs) {
Set<String> updatedConfigTypes = new HashSet<>();
Map<String, Map<String, String>> stackDefaults = stackDefaultConfig.getProperties();
for (Map.Entry<String, Map<String, String>> configEntry : updatedConfigs.entrySet()) {
String configType = configEntry.getKey();
Map<String, String> propertyMap = configEntry.getValue();
Map<String, String> clusterConfigProperties = existingConfigurations.get(configType);
Map<String, String> stackDefaultConfigProperties = stackDefaults.get(configType);
for (Map.Entry<String, String> propertyEntry : propertyMap.entrySet()) {
String property = propertyEntry.getKey();
String newValue = propertyEntry.getValue();
String currentValue = getPropertyValue(configType, property);
if (!propertyHasCustomValue(clusterConfigProperties, stackDefaultConfigProperties, property) && !Objects.equals(currentValue, newValue)) {
LOG.debug("Update config property {}/{}: {} -> {}", configType, property, currentValue, newValue);
setProperty(configType, property, newValue);
updatedConfigTypes.add(configType);
}
}
}
return updatedConfigTypes;
}
/**
* Returns true if the property exists in clusterConfigProperties and has a custom user defined value. Property has
* custom value in case we there's no stack default value for it or it's not equal to stack default value.
*/
private static boolean propertyHasCustomValue(Map<String, String> clusterConfigProperties, Map<String, String> stackDefaultConfigProperties, String property) {
boolean propertyHasCustomValue = false;
if (clusterConfigProperties != null) {
String propertyValue = clusterConfigProperties.get(property);
if (propertyValue != null) {
if (stackDefaultConfigProperties != null) {
String stackDefaultValue = stackDefaultConfigProperties.get(property);
if (stackDefaultValue != null) {
propertyHasCustomValue = !propertyValue.equals(stackDefaultValue);
} else {
propertyHasCustomValue = true;
}
} else {
propertyHasCustomValue = true;
}
}
}
return propertyHasCustomValue;
}
public Configuration copy() {
Configuration parent = parentConfiguration;
parentConfiguration = null;
Configuration copy = new Configuration(getFullProperties(), getFullAttributes());
parentConfiguration = parent;
return copy;
}
/**
* Constructor.
*
* @param properties properties
* @param attributes attributes
* @param parentConfiguration parent configuration
*/
public Configuration(Map<String, Map<String, String>> properties,
Map<String, Map<String, Map<String, String>>> attributes,
Configuration parentConfiguration) {
this.properties = properties;
this.attributes = attributes;
this.parentConfiguration = parentConfiguration;
//todo: warning for deprecated global properties
// String message = null;
// for (BlueprintConfigEntity blueprintConfig: blueprint.getConfigurations()){
// if(blueprintConfig.getType().equals("global")){
// message = "WARNING: Global configurations are deprecated, please use *-env";
// break;
// }
// }
}
/**
* Configuration.
*
* @param properties properties in configType -> propertyName -> value format
* @param attributes attributes in configType -> attributeName -> propertyName -> attributeValue format
*/
public Configuration(Map<String, Map<String, String>> properties,
Map<String, Map<String, Map<String, String>>> attributes) {
this.properties = properties;
this.attributes = attributes;
}
/**
* Get the properties for this instance only; parent properties are not included.
*
* @return map of properties for this configuration instance keyed by config type
*/
public Map<String, Map<String, String>> getProperties() {
return properties;
}
/**
* Get a complete merged map of properties including this instance and the entire parent hierarchy.
* Properties are merged so that children override the same property specified in it's parent hierarchy.
* This result is re-calculated for each request in case a parent value has changed so the result
* should be cached if possible.
*
* @return complete map of merged properties keyed by config type
*/
public Map<String, Map<String, String>> getFullProperties() {
return getFullProperties(Integer.MAX_VALUE);
}
/**
* Get a merged map of properties including this instance and n levels of the parent hierarchy.
* Properties are merged so that children override the same property specified in it's parent hierarchy.
* This result is re-calculated for each request in case a parent value has changed so the result
* should be cached if possible.
*
* @param depthLimit the number of parent levels to include in the results. Specifying 0 is the same
* as calling {@link #getProperties()}
*
* @return map of merged properties keyed by config type
*/
public Map<String, Map<String, String>> getFullProperties(int depthLimit) {
if (depthLimit == 0) {
HashMap<String, Map<String, String>> propertiesCopy = new HashMap<>();
for (Map.Entry<String, Map<String, String>> typeProperties : properties.entrySet()) {
propertiesCopy.put(typeProperties.getKey(), new HashMap<>(typeProperties.getValue()));
}
return propertiesCopy;
}
Map<String, Map<String, String>> mergedProperties = parentConfiguration == null ?
new HashMap<>() :
new HashMap<>(parentConfiguration.getFullProperties(--depthLimit));
for (Map.Entry<String, Map<String, String>> entry : properties.entrySet()) {
String configType = entry.getKey();
Map<String, String> typeProps = new HashMap<>(entry.getValue());
if (mergedProperties.containsKey(configType)) {
mergedProperties.get(configType).putAll(typeProps);
} else {
mergedProperties.put(configType, typeProps);
}
}
return mergedProperties;
}
/**
* Get the attributes for this instance only; parent attributes aren't included.
*
* @return map of attributes {configType -> {attributeName -> {propName, attributeValue}}}
*/
public Map<String, Map<String, Map<String, String>>> getAttributes() {
return attributes;
}
/**
* Get a complete merged map of attributes including this instance and the entire parent hierarchy.
* Attributes are merged so that children override the same attribute specified in it's parent hierarchy.
* This result is re-calculated for each request in case a parent value has changed so the result
* should be cached if possible.
*
* @return complete map of merged attributes {configType -> {attributeName -> {propName, attributeValue}}}
*/
public Map<String, Map<String, Map<String, String>>> getFullAttributes() {
Map<String, Map<String, Map<String, String>>> mergedAttributeMap = parentConfiguration == null ?
new HashMap<>() :
new HashMap<>(parentConfiguration.getFullAttributes());
for (Map.Entry<String, Map<String, Map<String, String>>> typeEntry : attributes.entrySet()) {
String type = typeEntry.getKey();
Map<String, Map<String, String>> typeAttributes = new HashMap<>();
for (Map.Entry<String, Map<String, String>> attributeEntry : typeEntry.getValue().entrySet()) {
typeAttributes.put(attributeEntry.getKey(), new HashMap<>(attributeEntry.getValue()));
}
if (! mergedAttributeMap.containsKey(type)) {
mergedAttributeMap.put(type, typeAttributes);
} else {
Map<String, Map<String, String>> mergedAttributes = mergedAttributeMap.get(type);
for (Map.Entry<String, Map<String, String>> attributeEntry : typeAttributes.entrySet()) {
String attribute = attributeEntry.getKey();
if (! mergedAttributes.containsKey(attribute)) {
mergedAttributes.put(attribute, attributeEntry.getValue());
} else {
Map<String, String> mergedAttributeProps = mergedAttributes.get(attribute);
for (Map.Entry<String, String> propEntry : attributeEntry.getValue().entrySet()) {
mergedAttributeProps.put(propEntry.getKey(), propEntry.getValue());
}
}
}
}
}
return mergedAttributeMap;
}
/**
* Get the requested property value from the full merged set of properties.
*
* @param configType configuration type
* @param propertyName property name
*
* @return requested property value or null if property isn't set in configuration hierarchy
*/
public String getPropertyValue(String configType, String propertyName) {
String value = null;
if (properties.containsKey(configType) && properties.get(configType).containsKey(propertyName)) {
value = properties.get(configType).get(propertyName);
} else if (parentConfiguration != null) {
value = parentConfiguration.getPropertyValue(configType, propertyName);
}
return value;
}
/**
* Get the requested attribute value from the full merged set of attributes.
*
* @param configType configuration type
* @param propertyName attribute property name
* @param attributeName attribute name
*
* @return requested attribute value or null if the attribute isn't set in configuration hierarchy
*/
public String getAttributeValue(String configType, String propertyName, String attributeName) {
String value = null;
if (attributes.containsKey(configType) &&
attributes.get(configType).containsKey(attributeName) &&
attributes.get(configType).get(attributeName).containsKey(propertyName)) {
value = attributes.get(configType).get(attributeName).get(propertyName);
} else if (parentConfiguration != null) {
value = parentConfiguration.getAttributeValue(configType, propertyName, attributeName);
}
return value;
}
/**
* Set a property on the configuration.
* The property will be set on this instance so it will override any value specified in
* the parent hierarchy.
*
* @param configType configuration type
* @param propertyName property name
* @param value property value
*
* @return the previous value of the property or null if it didn't exist
*/
public String setProperty(String configType, String propertyName, String value) {
String previousValue = getPropertyValue(configType, propertyName);
Map<String, String> typeProperties = properties.get(configType);
if (typeProperties == null) {
typeProperties = new HashMap<>();
properties.put(configType, typeProperties);
}
typeProperties.put(propertyName, value);
return previousValue;
}
/**
* Remove a property from the configuration hierarchy.
* All occurrences of the property are removed from the config hierarchy such that
* a subsequent call to getPropertyValue() for the removed property will return null.
*
* @param configType configuration type
* @param propertyName property name
*
* @return the previous value of the removed property or null if it didn't exist in the
* configuration hierarchy
*/
public String removeProperty(String configType, String propertyName) {
String previousValue = null;
if (properties.containsKey(configType)) {
previousValue = properties.get(configType).remove(propertyName);
}
if (parentConfiguration != null) {
String parentPreviousValue = parentConfiguration.removeProperty(configType, propertyName);
if (previousValue == null) {
previousValue = parentPreviousValue;
}
}
return previousValue;
}
/**
* Moves the given properties from {@code sourceConfigType} to {@code targetConfigType}.
* If a property is already present in the target, it will be removed from the source, but not overwritten in the target.
*
* @param sourceConfigType the config type to move properties from
* @param targetConfigType the config type to move properties to
* @param propertiesToMove names of properties to be moved
* @return property names that were removed from the source
*/
public Set<String> moveProperties(String sourceConfigType, String targetConfigType, Set<String> propertiesToMove) {
Set<String> moved = new HashSet<>();
for (String property : propertiesToMove) {
if (isPropertySet(sourceConfigType, property)) {
String value = removeProperty(sourceConfigType, property);
if (!isPropertySet(targetConfigType, property)) {
setProperty(targetConfigType, property, value);
}
moved.add(property);
}
}
return moved;
}
/**
* General convenience method to determine if a given property has been set in the cluster configuration
*
* @param configType the config type to check
* @param propertyName the property name to check
* @return true if the named property has been set
* false if the named property has not been set
*/
public boolean isPropertySet(String configType, String propertyName) {
return properties.containsKey(configType) && properties.get(configType).containsKey(propertyName) ||
parentConfiguration != null && parentConfiguration.isPropertySet(configType, propertyName);
}
/**
* Set an attribute on the hierarchy.
* The attribute will be set on this instance so it will override any value specified in
* the parent hierarchy.
*
* @param configType configuration type
* @param propertyName attribute property name
* @param attributeName attribute name
* @param attributeValue attribute property value
*
* @return the previous value of the attribute or null if it didn't exist
*/
public String setAttribute(String configType, String propertyName, String attributeName, String attributeValue) {
String previousValue = getAttributeValue(configType, propertyName, attributeName);
Map<String, Map<String, String>> typeAttributes = attributes.get(configType);
if (typeAttributes == null) {
typeAttributes = new HashMap<>();
attributes.put(configType, typeAttributes);
}
Map<String, String> attributes = typeAttributes.get(attributeName);
if (attributes == null) {
attributes = new HashMap<>();
typeAttributes.put(attributeName, attributes);
}
attributes.put(propertyName, attributeValue);
return previousValue;
}
/**
* Get the complete set of configuration types represented in both full properties and full attributes.
*
* @return collection of all represented configuration types
*/
public Collection<String> getAllConfigTypes() {
Collection<String> allTypes = new HashSet<>();
allTypes.addAll(getFullProperties().keySet());
allTypes.addAll(getFullAttributes().keySet());
return allTypes;
}
public boolean containsConfigType(String configType) {
return properties.containsKey(configType) || attributes.containsKey(configType) ||
(parentConfiguration != null && parentConfiguration.containsConfigType(configType));
}
public boolean containsConfig(String configType, String propertyName) {
return (properties.containsKey(configType) && properties.get(configType).containsKey(propertyName))
|| (attributes.containsKey(configType) && attributes.get(configType).values().stream().filter(map -> map.containsKey(propertyName)).findAny().isPresent())
|| (parentConfiguration != null && parentConfiguration.containsConfig(configType, propertyName));
}
/**
* Get the parent configuration.
*
* @return the parent configuration or null if no parent is set
*/
public Configuration getParentConfiguration() {
return parentConfiguration;
}
/**
* Set the parent configuration.
*
* @param parent parent configuration to set
*/
public void setParentConfiguration(Configuration parent) {
parentConfiguration = parent;
}
/**
* Remove all occurrences of a config type
*/
public void removeConfigType(String configType) {
if (properties != null) {
properties.remove(configType);
}
if (attributes != null) {
attributes.remove(configType);
}
if (parentConfiguration != null) {
parentConfiguration.removeConfigType(configType);
}
}
/**
* Create a new {@code Configuration} based on a pair of maps.
* (This is just a convenience method to be able to avoid local variables in a few places.)
*/
public static Configuration of(Pair<Map<String, Map<String, String>>, Map<String, Map<String, Map<String, String>>>> propertiesAndAttributes) {
return new Configuration(propertiesAndAttributes.getLeft(), propertiesAndAttributes.getRight());
}
/**
* @return this configuration's properties and attributes as a pair of maps,
* in order to be able to pass around more easily without polluting non-topology code with the Configuration object
*/
public Pair<Map<String, Map<String, String>>, Map<String, Map<String, Map<String, String>>>> asPair() {
return Pair.of(properties, attributes);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
Configuration other = (Configuration) obj;
return Objects.equals(properties, other.properties) &&
Objects.equals(attributes, other.attributes) &&
Objects.equals(parentConfiguration, other.parentConfiguration);
}
}