package io.cattle.platform.servicediscovery.api.filter;
import io.cattle.platform.core.addon.InServiceUpgradeStrategy;
import io.cattle.platform.core.addon.ServiceUpgrade;
import io.cattle.platform.core.addon.ServiceUpgradeStrategy;
import io.cattle.platform.core.constants.InstanceConstants;
import io.cattle.platform.core.constants.ServiceConstants;
import io.cattle.platform.core.model.Service;
import io.cattle.platform.iaas.api.filter.common.AbstractDefaultResourceManagerFilter;
import io.cattle.platform.json.JsonMapper;
import io.cattle.platform.object.ObjectManager;
import io.cattle.platform.object.util.DataAccessor;
import io.cattle.platform.servicediscovery.api.util.ServiceDiscoveryUtil;
import io.cattle.platform.storage.api.filter.ExternalTemplateInstanceFilter;
import io.cattle.platform.storage.service.StorageService;
import io.cattle.platform.util.type.CollectionUtils;
import io.github.ibuildthecloud.gdapi.request.ApiRequest;
import io.github.ibuildthecloud.gdapi.request.resource.ResourceManager;
import io.github.ibuildthecloud.gdapi.validation.ValidationErrorCodes;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import javax.inject.Named;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
@Named
public class ServiceUpgradeValidationFilter extends AbstractDefaultResourceManagerFilter {
@Inject
ObjectManager objectManager;
@Inject
JsonMapper jsonMapper;
@Inject
StorageService storageService;
@Override
public Class<?>[] getTypeClasses() {
return new Class<?>[] { Service.class };
}
@Override
public String[] getTypes() {
List<String> supportedTypes = new ArrayList<>();
supportedTypes.addAll(ServiceConstants.SERVICE_LIKE);
supportedTypes.add(ServiceConstants.KIND_DNS_SERVICE);
supportedTypes.add(ServiceConstants.KIND_EXTERNAL_SERVICE);
return supportedTypes.toArray(new String[supportedTypes.size()]);
}
@Override
public Object resourceAction(String type, ApiRequest request, ResourceManager next) {
if (request.getAction().equals(ServiceConstants.ACTION_SERVICE_UPGRADE)) {
Service service = objectManager.loadResource(Service.class, request.getId());
ServiceUpgrade upgrade = jsonMapper.convertValue(request.getRequestObject(),
ServiceUpgrade.class);
ServiceUpgradeStrategy strategy = upgrade.getStrategy();
if (strategy == null) {
ValidationErrorCodes.throwValidationError(ValidationErrorCodes.MISSING_REQUIRED,
"Upgrade strategy needs to be set");
}
processInServiceUpgradeStrategy(request, service, upgrade, strategy);
}
return super.resourceAction(type, request, next);
}
@SuppressWarnings("unchecked")
protected void processInServiceUpgradeStrategy(ApiRequest request, Service service, ServiceUpgrade upgrade,
ServiceUpgradeStrategy strategy) {
if (strategy instanceof InServiceUpgradeStrategy) {
InServiceUpgradeStrategy inServiceStrategy = (InServiceUpgradeStrategy) strategy;
inServiceStrategy = finalizeUpgradeStrategy(service, inServiceStrategy);
Object launchConfig = DataAccessor.field(service, ServiceConstants.FIELD_LAUNCH_CONFIG,
Object.class);
Object newLaunchConfig = inServiceStrategy.getLaunchConfig();
if (newLaunchConfig != null) {
ServiceDiscoveryUtil.validateScaleSwitch(newLaunchConfig, launchConfig);
}
List<Object> secondaryLaunchConfigs = DataAccessor.fields(service)
.withKey(ServiceConstants.FIELD_SECONDARY_LAUNCH_CONFIGS)
.withDefault(Collections.EMPTY_LIST).as(
List.class);
inServiceStrategy.setPreviousLaunchConfig(launchConfig);
inServiceStrategy.setPreviousSecondaryLaunchConfigs(secondaryLaunchConfigs);
upgrade.setInServiceStrategy(inServiceStrategy);
request.setRequestObject(jsonMapper.writeValueAsMap(upgrade));
ServiceDiscoveryUtil.upgradeServiceConfigs(service, inServiceStrategy, false);
}
objectManager.persist(service);
}
protected void setVersion(InServiceUpgradeStrategy upgrade) {
String version = io.cattle.platform.util.resource.UUID.randomUUID().toString();
if (upgrade.getSecondaryLaunchConfigs() != null) {
for (Object launchConfigObj : upgrade.getSecondaryLaunchConfigs()) {
setLaunchConfigVersion(version, launchConfigObj);
}
}
if (upgrade.getLaunchConfig() != null) {
setLaunchConfigVersion(version, upgrade.getLaunchConfig());
}
}
@SuppressWarnings("unchecked")
protected void setLaunchConfigVersion(String version, Object launchConfigObj) {
Map<String, Object> launchConfig = (Map<String, Object>) launchConfigObj;
launchConfig.put(ServiceConstants.FIELD_VERSION, version);
}
@SuppressWarnings("unchecked")
protected InServiceUpgradeStrategy finalizeUpgradeStrategy(Service service, InServiceUpgradeStrategy strategy) {
if (strategy.getLaunchConfig() == null && strategy.getSecondaryLaunchConfigs() == null) {
ValidationErrorCodes.throwValidationError(ValidationErrorCodes.INVALID_OPTION,
"LaunchConfig/secondaryLaunchConfigs need to be specified for inService strategy");
}
if (DataAccessor.fieldBool(service, ServiceConstants.FIELD_SERVICE_RETAIN_IP)) {
if (strategy.getStartFirst()) {
ValidationErrorCodes.throwValidationError(ValidationErrorCodes.INVALID_OPTION,
"StartFirst option can't be used for service with "
+ ServiceConstants.FIELD_SERVICE_RETAIN_IP + " field set");
}
}
if (service.getKind().equalsIgnoreCase(ServiceConstants.KIND_LOAD_BALANCER_SERVICE)) {
if (strategy.getLaunchConfig() == null) {
ValidationErrorCodes.throwValidationError(ValidationErrorCodes.INVALID_OPTION,
"LaunchConfig is required for load balancer service");
}
ServiceDiscoveryUtil.injectBalancerLabelsAndHealthcheck((Map<Object, Object>) strategy.getLaunchConfig());
}
Map<String, Map<Object, Object>> serviceLCs = getExistingLaunchConfigs(service);
Map<String, Map<Object, Object>> lCsToUpdateInitial = getLaunchConfigsToUpdateInitial(service, strategy,
serviceLCs);
Map<String, Map<Object, Object>> lCsToUpdateFinal = getLaunchConfigsToUpdateFinal(serviceLCs,
lCsToUpdateInitial);
for (String name : lCsToUpdateFinal.keySet()) {
if (!lCsToUpdateInitial.containsKey(name)) {
Object launchConfig = lCsToUpdateFinal.get(name);
if (name.equalsIgnoreCase(service.getName())) {
strategy.setLaunchConfig(launchConfig);
} else {
List<Object> secondaryLCs = strategy.getSecondaryLaunchConfigs();
if (secondaryLCs == null) {
secondaryLCs = new ArrayList<>();
}
secondaryLCs.add(launchConfig);
strategy.setSecondaryLaunchConfigs(secondaryLCs);
}
}
}
// set new version on the configs-to-upgrade
setVersion(strategy);
// add launch configs that don't need to be upgraded
// they will get saved with the old version value
List<Object> finalizedSecondary = new ArrayList<>();
if (strategy.getSecondaryLaunchConfigs() != null) {
finalizedSecondary.addAll(strategy.getSecondaryLaunchConfigs());
}
Object finalizedPrimary = strategy.getLaunchConfig();
Map<String, Map<Object, Object>> existingLCs = getExistingLaunchConfigs(service);
for (String scName : existingLCs.keySet()) {
if (!lCsToUpdateFinal.containsKey(scName)) {
if (StringUtils.equals(scName, service.getName())) {
finalizedPrimary = existingLCs.get(scName);
} else {
finalizedSecondary.add(existingLCs.get(scName));
}
}
}
// remove secondary launch configs marked with labels
Iterator<Object> it = finalizedSecondary.iterator();
while (it.hasNext()) {
Object lc = it.next();
Map<String, Object> mapped = CollectionUtils.toMap(lc);
Object imageUuid = mapped.get(InstanceConstants.FIELD_IMAGE_UUID);
if (imageUuid == null) {
continue;
}
if (service.getSelectorContainer() == null
&& StringUtils.equalsIgnoreCase(ServiceConstants.IMAGE_NONE, imageUuid.toString())) {
it.remove();
}
else {
String fullImageName = ExternalTemplateInstanceFilter.getImageUuid(imageUuid.toString(), storageService);
((Map<String, Object>) lc).put(InstanceConstants.FIELD_IMAGE_UUID, fullImageName);
}
}
Map<String, Object> mapped = CollectionUtils.toMap(finalizedPrimary);
Object imageUuid = mapped.get(InstanceConstants.FIELD_IMAGE_UUID);
if (imageUuid != null && !imageUuid.toString().equalsIgnoreCase(ServiceConstants.IMAGE_NONE)) {
String fullImageName = ExternalTemplateInstanceFilter.getImageUuid(imageUuid.toString(), storageService);
((Map<String, Object>) finalizedPrimary).put(InstanceConstants.FIELD_IMAGE_UUID, fullImageName);
}
strategy.setLaunchConfig(finalizedPrimary);
strategy.setSecondaryLaunchConfigs(finalizedSecondary);
return strategy;
}
protected Map<String, Map<Object, Object>> getLaunchConfigsToUpdateFinal(
Map<String, Map<Object, Object>> serviceLCs,
Map<String, Map<Object, Object>> lCsToUpdateInitial) {
Map<String, Map<Object, Object>> lCsToUpdateFinal = new HashMap<>();
for (String lcNameToUpdate : lCsToUpdateInitial.keySet()) {
finalizeLCNamesToUpdate(serviceLCs, lCsToUpdateFinal,
Pair.of(lcNameToUpdate, lCsToUpdateInitial.get(lcNameToUpdate)));
}
return lCsToUpdateFinal;
}
@SuppressWarnings("unchecked")
protected Map<String, Map<Object, Object>> getLaunchConfigsToUpdateInitial(Service service,
InServiceUpgradeStrategy strategy,
Map<String, Map<Object, Object>> serviceLCs) {
Map<String, Map<Object, Object>> lCsToUpdateInitial = new HashMap<>();
if (strategy.getLaunchConfig() != null) {
lCsToUpdateInitial.put(service.getName(), (Map<Object, Object>) strategy.getLaunchConfig());
}
if (strategy.getSecondaryLaunchConfigs() != null) {
for (Object secondaryLC : strategy.getSecondaryLaunchConfigs()) {
String lcName = CollectionUtils.toMap(secondaryLC).get("name").toString();
lCsToUpdateInitial.put(lcName, (Map<Object, Object>) secondaryLC);
}
}
return lCsToUpdateInitial;
}
protected Map<String, Map<Object, Object>> getExistingLaunchConfigs(Service service) {
Map<String, Map<Object, Object>> serviceLCs = ServiceDiscoveryUtil.getServiceLaunchConfigsWithNames(service);
Map<Object, Object> primaryLC = serviceLCs.get(ServiceConstants.PRIMARY_LAUNCH_CONFIG_NAME);
serviceLCs.remove(ServiceConstants.PRIMARY_LAUNCH_CONFIG_NAME);
serviceLCs.put(service.getName(), primaryLC);
return serviceLCs;
}
@SuppressWarnings("unchecked")
// this method finalizes volumeFrom/networkFrom dependencies that need to be updated as well
protected void finalizeLCNamesToUpdate(Map<String, Map<Object, Object>> serviceLCs,
Map<String, Map<Object, Object>> lCToUpdateFinal,
Pair<String, Map<Object, Object>> lcToUpdate) {
Map<Object, Object> finalConfig = new HashMap<>();
finalConfig.putAll(lcToUpdate.getRight());
lCToUpdateFinal.put(lcToUpdate.getLeft(), finalConfig);
for (String serviceLCName : serviceLCs.keySet()) {
Map<Object, Object> serviceLC = serviceLCs.get(serviceLCName);
List<String> refs = new ArrayList<>();
Object networkFromLaunchConfig = serviceLC
.get(ServiceConstants.FIELD_NETWORK_LAUNCH_CONFIG);
if (networkFromLaunchConfig != null) {
refs.add((String) networkFromLaunchConfig);
}
Object volumesFromLaunchConfigs = serviceLC
.get(ServiceConstants.FIELD_DATA_VOLUMES_LAUNCH_CONFIG);
if (volumesFromLaunchConfigs != null) {
refs.addAll((List<String>) volumesFromLaunchConfigs);
}
for (String ref : refs) {
if (lcToUpdate.getLeft().equalsIgnoreCase(ref)) {
finalizeLCNamesToUpdate(serviceLCs, lCToUpdateFinal,
Pair.of(serviceLCName, serviceLC));
}
}
}
}
}