package io.cattle.platform.api.resource; import io.cattle.platform.api.action.ActionHandler; import io.cattle.platform.api.auth.Policy; import io.cattle.platform.api.link.LinkHandler; import io.cattle.platform.api.utils.ApiUtils; import io.cattle.platform.archaius.util.ArchaiusUtil; import io.cattle.platform.engine.manager.ProcessNotFoundException; import io.cattle.platform.engine.process.ExitReason; import io.cattle.platform.engine.process.ProcessInstanceException; import io.cattle.platform.engine.process.impl.ProcessCancelException; import io.cattle.platform.object.ObjectManager; import io.cattle.platform.object.meta.ActionDefinition; import io.cattle.platform.object.meta.MapRelationship; import io.cattle.platform.object.meta.ObjectMetaDataManager; import io.cattle.platform.object.meta.Relationship; import io.cattle.platform.object.process.ObjectProcessManager; import io.cattle.platform.object.process.StandardProcess; import io.cattle.platform.object.util.DataAccessor; import io.cattle.platform.util.type.CollectionUtils; import io.cattle.platform.util.type.InitializationTask; import io.cattle.platform.util.type.NamedUtils; import io.github.ibuildthecloud.gdapi.condition.Condition; import io.github.ibuildthecloud.gdapi.condition.ConditionType; import io.github.ibuildthecloud.gdapi.context.ApiContext; import io.github.ibuildthecloud.gdapi.exception.ClientVisibleException; import io.github.ibuildthecloud.gdapi.factory.SchemaFactory; import io.github.ibuildthecloud.gdapi.id.IdFormatter; import io.github.ibuildthecloud.gdapi.id.IdentityFormatter; import io.github.ibuildthecloud.gdapi.model.Action; import io.github.ibuildthecloud.gdapi.model.Field; import io.github.ibuildthecloud.gdapi.model.Include; import io.github.ibuildthecloud.gdapi.model.ListOptions; import io.github.ibuildthecloud.gdapi.model.Resource; import io.github.ibuildthecloud.gdapi.model.Schema; import io.github.ibuildthecloud.gdapi.model.Schema.Method; import io.github.ibuildthecloud.gdapi.request.ApiRequest; import io.github.ibuildthecloud.gdapi.request.resource.ResourceManager; import io.github.ibuildthecloud.gdapi.request.resource.impl.AbstractBaseResourceManager; import io.github.ibuildthecloud.gdapi.url.UrlBuilder; import io.github.ibuildthecloud.gdapi.util.ResponseCodes; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.inject.Inject; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.netflix.config.DynamicIntProperty; public abstract class AbstractObjectResourceManager extends AbstractBaseResourceManager implements InitializationTask { private static final Logger log = LoggerFactory.getLogger(AbstractObjectResourceManager.class); public static final String SCHEDULE_UPDATE = "scheduleUpdate"; private static final DynamicIntProperty REMOVE_DELAY = ArchaiusUtil.getInt("api.show.removed.for.seconds"); private static final IdFormatter IDENTITY_FORMATTER = new IdentityFormatter(); ObjectManager objectManager; ObjectProcessManager objectProcessManager; ObjectMetaDataManager metaDataManager; Map<String, ActionHandler> actionHandlersMap; List<ActionHandler> actionHandlers; Map<String, List<LinkHandler>> linkHandlersMap; List<LinkHandler> linkHandlers; @Override protected Object authorize(Object object) { return ApiUtils.authorize(object); } @Override protected Object createInternal(String type, ApiRequest request) { Class<?> clz = getClassForType(request.getSchemaFactory(), type); if (clz == null) { return null; } return doCreate(type, clz, CollectionUtils.toMap(request.getRequestObject())); } protected <T> T doCreate(String type, Class<T> clz, Map<Object, Object> data) { Map<String, Object> properties = getObjectManager().convertToPropertiesFor(clz, data); if (!properties.containsKey(ObjectMetaDataManager.KIND_FIELD)) { properties.put(ObjectMetaDataManager.KIND_FIELD, type); } return createAndScheduleObject(clz, properties); } @SuppressWarnings("unchecked") protected <T> T createAndScheduleObject(Class<T> clz, Map<String, Object> properties) { Object result = objectManager.create(clz, properties); try { scheduleProcess(StandardProcess.CREATE, result, properties); result = objectManager.reload(result); } catch (ProcessNotFoundException e) { } return (T) result; } protected Class<?> getClassForType(SchemaFactory schemaFactory, String type) { Class<?> clz = schemaFactory.getSchemaClass(type); if (clz == null) { Schema schema = schemaFactory.getSchema(type); if (schema != null && schema.getParent() != null) { return getClassForType(schemaFactory, schema.getParent()); } } return clz; } @Override protected Object deleteInternal(String type, String id, Object obj, ApiRequest request) { try { scheduleProcess(StandardProcess.REMOVE, obj, null); return objectManager.reload(obj); } catch (ProcessCancelException e) { throw new ClientVisibleException(ResponseCodes.METHOD_NOT_ALLOWED); } catch (ProcessNotFoundException e) { return removeFromStore(type, id, obj, request); } } protected abstract Object removeFromStore(String type, String id, Object obj, ApiRequest request); @Override protected Object updateInternal(String type, String id, Object obj, ApiRequest request) { Map<String, Object> updates = CollectionUtils.toMap(request.getRequestObject()); Map<String, Object> existingValues = new HashMap<>(); Map<String, Object> filteredUpdates = new HashMap<String, Object>(); Map<String, Object> existing = createResource(obj, IDENTITY_FORMATTER, request).getFields(); Schema schema = request.getSchemaFactory().getSchema(type); Map<String, Field> fields = schema.getResourceFields(); boolean schedule = false; for (Map.Entry<String, Object> entry : updates.entrySet()) { String key = entry.getKey(); Object existingValue = existing.get(key); if (!ObjectUtils.equals(existingValue, entry.getValue())) { filteredUpdates.put(key, entry.getValue()); existingValues.put(key, existingValue); Field field = fields.get(key); if (field != null) { schedule |= Boolean.TRUE.equals(field.getAttributes().get(SCHEDULE_UPDATE)); } } } Object result = objectManager.setFields(schema, obj, filteredUpdates); if (schedule) { filteredUpdates.put("old", existingValues); objectProcessManager.scheduleStandardProcess(StandardProcess.UPDATE, obj, filteredUpdates); result = objectManager.reload(result); } return result; } @Override protected Object getLinkInternal(String type, String id, String link, ApiRequest request) { List<LinkHandler> linkHandlers = linkHandlersMap.get(type); if (linkHandlers != null) { for (LinkHandler linkHandler : linkHandlers) { if (linkHandler.handles(type, id, link, request)) { Object currentObject = getById(type, id, new ListOptions(request)); if (currentObject == null) { return null; } try { return linkHandler.link(link, currentObject, request); } catch (IOException e) { log.error("Failed to process link [{}] for [{}:{}]", link, type, id, e); return null; } } } } Class<?> clz = request.getSchemaFactory().getSchemaClass(type, true); Relationship relationship; if (clz != null) { relationship = getRelationship(clz, link); } else { relationship = getRelationship(type, link); } if (relationship == null) { return null; } switch (relationship.getRelationshipType()) { case CHILD: return getChildLink(type, id, relationship, request); case REFERENCE: return getReferenceLink(type, id, relationship, request); case MAP: return getMapLink(type, id, (MapRelationship) relationship, request); } return null; } protected Object getMapLink(String type, String id, MapRelationship relationship, ApiRequest request) { return null; } protected Object getChildLink(String type, String id, Relationship relationship, ApiRequest request) { Schema otherSchema = request.getSchemaFactory().getSchema(relationship.getObjectType()); if (otherSchema == null) { return Collections.EMPTY_LIST; } Object currentObject = getById(type, id, new ListOptions(request)); if (currentObject == null) { return null; } String otherType = otherSchema.getId(); Field field = otherSchema.getResourceFields().get(relationship.getPropertyName()); if (field == null) { return Collections.EMPTY_LIST; } if (otherSchema.getCollectionMethods().contains(Method.POST.toString())) { Map<String, Object> createDefaults = new HashMap<String, Object>(); IdFormatter idFormatter = ApiContext.getContext().getIdFormatter(); createDefaults.put(relationship.getPropertyName(), idFormatter.formatId(type, id)); request.setCreateDefaults(createDefaults); } Map<Object, Object> criteria = getDefaultCriteria(false, true, otherType); criteria.put(relationship.getPropertyName(), id); ResourceManager resourceManager = locator.getResourceManagerByType(otherType); return resourceManager.list(otherType, criteria, null); } protected Object getReferenceLink(String type, String id, Relationship relationship, ApiRequest request) { SchemaFactory schemaFactory = request.getSchemaFactory(); Schema schema = schemaFactory.getSchema(type); Schema otherSchema = schemaFactory.getSchema(relationship.getObjectType()); Field field = schema.getResourceFields().get(relationship.getPropertyName()); if (field == null || otherSchema == null) { return null; } ListOptions options = new ListOptions(request); Object currentObject = getById(type, id, options); if (currentObject == null) { return null; } Object fieldValue = field.getValue(currentObject); if (fieldValue == null) { return null; } Map<Object, Object> criteria = getDefaultCriteria(false, true, otherSchema.getId()); criteria.put(ObjectMetaDataManager.ID_FIELD, fieldValue); ResourceManager resourceManager = locator.getResourceManagerByType(otherSchema.getId()); return ApiUtils.getFirstFromList(resourceManager.list(otherSchema.getId(), criteria, options)); } protected Map<String, Relationship> getLinkRelationships(SchemaFactory schemaFactory, String type, Include include) { if (include == null) return Collections.emptyMap(); Map<String, Relationship> result = new HashMap<String, Relationship>(); Map<String, Relationship> links = metaDataManager.getLinkRelationships(schemaFactory, type); for (String link : include.getLinks()) { link = link.toLowerCase(); if (links.containsKey(link)) { result.put(link, links.get(link)); } } return result; } @Override protected Map<Object, Object> getDefaultCriteria(boolean byId, boolean byLink, String type) { Map<Object, Object> criteria = super.getDefaultCriteria(byId, byLink, type); Policy policy = ApiUtils.getPolicy(); addAccountAuthorization(byId, byLink, type, criteria, policy); if (!showRemoved() && !byId) { /* removed is null or removed >= (NOW() - delay) */ Condition or = new Condition(new Condition(ConditionType.NULL), new Condition(ConditionType.GTE, removedTime())); criteria.put(ObjectMetaDataManager.REMOVED_FIELD, or); /* remove_time is null or remove_time > NOW() */ or = new Condition(new Condition(ConditionType.NULL), new Condition(ConditionType.GT, new Date())); criteria.put(ObjectMetaDataManager.REMOVE_TIME_FIELD, or); } return criteria; } protected boolean showRemoved() { ApiRequest request = ApiContext.getContext().getApiRequest(); if (request == null) { return false; } return request.getOptions().containsKey("_removed"); } protected Date removedTime() { return new Date(System.currentTimeMillis() - REMOVE_DELAY.get() * 1000); } protected void addAccountAuthorization(boolean byId, boolean byLink, String type, Map<Object, Object> criteria, Policy policy) { if (!policy.isOption(Policy.LIST_ALL_ACCOUNTS)) { if (policy.isOption(Policy.AUTHORIZED_FOR_ALL_ACCOUNTS) && (byId || byLink)) { return; } if ("account".equals(type)) { criteria.put(ObjectMetaDataManager.ID_FIELD, policy.getAccountId()); } else { criteria.put(ObjectMetaDataManager.ACCOUNT_FIELD, policy.getAccountId()); } } } @Override protected String getCollectionType(List<?> list, ApiRequest request) { String link = request.getLink(); if (link == null) { return request.getType(); } else { Relationship relationship = getRelationship(request.getSchemaFactory() .getSchemaClass(request.getType(), true), link); return request.getSchemaFactory().getSchemaName(relationship.getObjectType()); } } @Override protected Schema getSchemaForDisplay(SchemaFactory schemaFactory, Object obj) { String schemaId = ApiUtils.getSchemaIdForDisplay(schemaFactory, obj); Schema schema = schemaFactory.getSchema(schemaId); if (schema == null) { /* Check core schema because the parent might not be authorized */ schemaId = ApiUtils.getSchemaIdForDisplay(getObjectManager().getSchemaFactory(), obj); /* Still get schema from request's schemaFactory */ schema = schemaFactory.getSchema(schemaId); } return schema; } @Override protected Resource constructResource(IdFormatter idFormatter, SchemaFactory schemaFactory, Schema schema, Object obj, ApiRequest apiRequest) { Map<String, Object> transitioningFields = metaDataManager.getTransitionFields(schema, obj); return ApiUtils.createResourceWithAttachments(this, apiRequest, idFormatter, schemaFactory, schema, obj, transitioningFields); } @Override protected Object resourceActionInternal(Object obj, ApiRequest request) { String processName = getProcessName(obj, request); ActionHandler handler = actionHandlersMap.get(processName); if (handler != null) { return handler.perform(processName, obj, request); } Map<String, Object> data = CollectionUtils.toMap(request.getRequestObject()); try { scheduleProcess(getProcessName(obj, request), obj, data); } catch (ProcessNotFoundException e) { throw new ClientVisibleException(ResponseCodes.NOT_FOUND); } request.setResponseCode(ResponseCodes.ACCEPTED); return objectManager.reload(obj); } protected String getProcessName(Object obj, ApiRequest request) { String baseType = request.getSchemaFactory().getBaseType(request.getType()); return String.format("%s.%s", baseType == null ? request.getType() : baseType, request.getAction()).toLowerCase(); } protected void scheduleProcess(final String processName, final Object resource, final Map<String, Object> data) { scheduleProcess(new Runnable() { @Override public void run() { objectProcessManager.scheduleProcessInstance(processName, resource, data); } }); } protected void scheduleProcess(final StandardProcess process, final Object resource, final Map<String, Object> data) { scheduleProcess(new Runnable() { @Override public void run() { objectProcessManager.scheduleStandardProcess(process, resource, data); } }); } protected void scheduleProcess(Runnable run) { try { run.run(); } catch (ProcessInstanceException e) { if (e.getExitReason() == ExitReason.RESOURCE_BUSY || e.getExitReason() == ExitReason.CANCELED) { throw new ClientVisibleException(ResponseCodes.CONFLICT); } else { throw e; } } } @Override protected void addActions(Object obj, SchemaFactory schemaFactory, Schema schema, Resource resource) { Object state = resource.getFields().get(ObjectMetaDataManager.STATE_FIELD); Map<String, ActionDefinition> defs = metaDataManager.getActionDefinitions(obj); if (state == null || defs == null) { super.addActions(obj, schemaFactory, schema, resource); return; } Map<String, Action> actions = schema.getResourceActions(); if (actions == null || actions.size() == 0) { return; } UrlBuilder urlBuilder = ApiContext.getUrlBuilder(); for (Map.Entry<String, Action> entry : actions.entrySet()) { String name = entry.getKey(); Action action = entry.getValue(); if ("restore".equals(name) || !isValidAction(obj, action)) { continue; } ActionDefinition def = defs.get(name); if (def == null || def.getValidStates().contains(state)) { resource.getActions().put(name, urlBuilder.actionLink(resource, name)); } } } @SuppressWarnings("unchecked") protected boolean isValidAction(Object obj, Action action) { Map<String, Object> attributes = action.getAttributes(); if (attributes == null || attributes.size() == 0) { return true; } String capability = ObjectUtils.toString(attributes.get("capability"), null); String state = ObjectUtils.toString(attributes.get(ObjectMetaDataManager.STATE_FIELD), null); String currentState = io.cattle.platform.object.util.ObjectUtils.getState(obj); if (!StringUtils.isBlank(capability) && !(ApiContext.getContext().getCapabilities(obj) != null ? ApiContext.getContext().getCapabilities(obj).contains(capability) : DataAccessor.fieldStringList(obj, ObjectMetaDataManager.CAPABILITIES_FIELD).contains(capability))) { return false; } if (!StringUtils.isBlank(state) && !state.equals(currentState)) { return false; } List<String> states = ((List<String>) attributes.get(ObjectMetaDataManager.STATES_FIELD)); if (states != null && !states.contains(currentState)){ return false; } return true; } @Override public void start() { actionHandlersMap = NamedUtils.createMapByName(actionHandlers); linkHandlersMap = new HashMap<String, List<LinkHandler>>(); for (LinkHandler handler : linkHandlers) { for (String type : handler.getTypes()) { CollectionUtils.addToMap(linkHandlersMap, type, handler, ArrayList.class); } } } protected Relationship getRelationship(String type, String linkName) { return metaDataManager.getRelationship(type, linkName); } protected Relationship getRelationship(Class<?> clz, String linkName) { return metaDataManager.getRelationship(clz, linkName); } @Override protected Object collectionActionInternal(Object resources, ApiRequest request) { return null; } @Override protected Map<String, String> getLinks(SchemaFactory schemaFactory, Resource resource) { return metaDataManager.getLinks(schemaFactory, resource.getType()); } public ObjectManager getObjectManager() { return objectManager; } @Inject public void setObjectManager(ObjectManager objectManager) { this.objectManager = objectManager; } public ObjectMetaDataManager getMetaDataManager() { return metaDataManager; } @Inject public void setMetaDataManager(ObjectMetaDataManager metaDataManager) { this.metaDataManager = metaDataManager; } public ObjectProcessManager getObjectProcessManager() { return objectProcessManager; } @Inject public void setObjectProcessManager(ObjectProcessManager objectProcessManager) { this.objectProcessManager = objectProcessManager; } public List<ActionHandler> getActionHandlers() { return actionHandlers; } @Inject public void setActionHandlers(List<ActionHandler> actionHandlers) { this.actionHandlers = actionHandlers; } public List<LinkHandler> getLinkHandlers() { return linkHandlers; } @Inject public void setLinkHandlers(List<LinkHandler> linkHandlers) { this.linkHandlers = linkHandlers; } }