/* * 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 gobblin.service; import java.net.URI; import java.net.URISyntaxException; import java.util.Properties; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.Maps; import com.google.inject.Inject; import com.google.inject.name.Named; import com.linkedin.data.template.StringMap; import com.linkedin.restli.common.ComplexResourceKey; import com.linkedin.restli.common.EmptyRecord; import com.linkedin.restli.common.HttpStatus; import com.linkedin.restli.server.CreateResponse; import com.linkedin.restli.server.RestLiServiceException; import com.linkedin.restli.server.UpdateResponse; import com.linkedin.restli.server.annotations.RestLiCollection; import com.linkedin.restli.server.resources.ComplexKeyResourceTemplate; import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; import gobblin.config.ConfigBuilder; import gobblin.configuration.ConfigurationKeys; import gobblin.runtime.api.FlowSpec; import gobblin.runtime.api.SpecNotFoundException; import gobblin.runtime.spec_catalog.FlowCatalog; /** * Resource for handling flow configuration requests */ @RestLiCollection(name = "flowconfigs", namespace = "gobblin.service", keyName = "id") public class FlowConfigsResource extends ComplexKeyResourceTemplate<FlowId, EmptyRecord, FlowConfig> { private static final Logger LOG = LoggerFactory.getLogger(FlowConfigsResource.class); @edu.umd.cs.findbugs.annotations.SuppressWarnings("MS_SHOULD_BE_FINAL") public static FlowCatalog _globalFlowCatalog; @Inject @Named("flowCatalog") private FlowCatalog _flowCatalog; // For blocking use of this resource until it is ready @Inject @Named("readyToUse") private Boolean readyToUse = Boolean.FALSE; public FlowConfigsResource() {} /** * Logs message and throws Rest.li exception * @param status HTTP status code * @param msg error message * @param e exception */ public void logAndThrowRestLiServiceException(HttpStatus status, String msg, Exception e) { if (e != null) { LOG.error(msg, e); throw new RestLiServiceException(status, msg + " cause = " + e.getMessage()); } else { LOG.error(msg); throw new RestLiServiceException(status, msg); } } /** * Retrieve the flow configuration with the given key * @param key flow config id key containing group name and flow name * @return {@link FlowConfig} with flow configuration */ @Override public FlowConfig get(ComplexResourceKey<FlowId, EmptyRecord> key) { String flowGroup = key.getKey().getFlowGroup(); String flowName = key.getKey().getFlowName(); LOG.info("Get called with flowGroup " + flowGroup + " flowName " + flowName); try { URI flowCatalogURI = new URI("gobblin-flow", null, "/", null, null); URI flowUri = new URI(flowCatalogURI.getScheme(), flowCatalogURI.getAuthority(), "/" + flowGroup + "/" + flowName, null, null); FlowSpec spec = (FlowSpec) getFlowCatalog().getSpec(flowUri); FlowConfig flowConfig = new FlowConfig(); Properties flowProps = spec.getConfigAsProperties(); Schedule schedule = null; if (flowProps.containsKey(ConfigurationKeys.JOB_SCHEDULE_KEY)) { schedule = new Schedule(); schedule.setCronSchedule(flowProps.getProperty(ConfigurationKeys.JOB_SCHEDULE_KEY)); } if (flowProps.containsKey(ConfigurationKeys.JOB_TEMPLATE_PATH)) { flowConfig.setTemplateUris(flowProps.getProperty(ConfigurationKeys.JOB_TEMPLATE_PATH)); } else if (spec.getTemplateURIs().isPresent()) { flowConfig.setTemplateUris(StringUtils.join(spec.getTemplateURIs().get(), ",")); } else { flowConfig.setTemplateUris("NA"); } if (schedule != null) { if (flowProps.containsKey(ConfigurationKeys.FLOW_RUN_IMMEDIATELY)) { schedule.setRunImmediately(Boolean.valueOf(flowProps.getProperty(ConfigurationKeys.FLOW_RUN_IMMEDIATELY))); } flowConfig.setSchedule(schedule); } // remove keys that were injected as part of flowSpec creation flowProps.remove(ConfigurationKeys.JOB_SCHEDULE_KEY); flowProps.remove(ConfigurationKeys.JOB_TEMPLATE_PATH); StringMap flowPropsAsStringMap = new StringMap(); flowPropsAsStringMap.putAll(Maps.fromProperties(flowProps)); return flowConfig.setId(new FlowId().setFlowGroup(flowGroup).setFlowName(flowName)) .setProperties(flowPropsAsStringMap); } catch (URISyntaxException e) { logAndThrowRestLiServiceException(HttpStatus.S_400_BAD_REQUEST, "bad URI " + flowName, e); } catch (SpecNotFoundException e) { logAndThrowRestLiServiceException(HttpStatus.S_404_NOT_FOUND, "Flow requested does not exist: " + flowName, null); } return null; } /** * Build a {@link FlowSpec} from a {@link FlowConfig} * @param flowConfig flow configuration * @return {@link FlowSpec} created with attributes from flowConfig */ private FlowSpec createFlowSpecForConfig(FlowConfig flowConfig) { ConfigBuilder configBuilder = ConfigBuilder.create() .addPrimitive(ConfigurationKeys.FLOW_GROUP_KEY, flowConfig.getId().getFlowGroup()) .addPrimitive(ConfigurationKeys.FLOW_NAME_KEY, flowConfig.getId().getFlowName()); if (flowConfig.hasSchedule()) { Schedule schedule = flowConfig.getSchedule(); configBuilder.addPrimitive(ConfigurationKeys.JOB_SCHEDULE_KEY, schedule.getCronSchedule()); configBuilder.addPrimitive(ConfigurationKeys.FLOW_RUN_IMMEDIATELY, schedule.isRunImmediately()); } Config config = configBuilder.build(); Config configWithFallback = config.withFallback(ConfigFactory.parseMap(flowConfig.getProperties())); URI templateURI = null; try { templateURI = new URI(flowConfig.getTemplateUris()); } catch (URISyntaxException e) { logAndThrowRestLiServiceException(HttpStatus.S_400_BAD_REQUEST, "bad URI " + flowConfig.getTemplateUris(), e); } return FlowSpec.builder().withConfig(configWithFallback).withTemplate(templateURI).build(); } /** * Create a flow configuration that the service will forward to execution instances for execution * @param flowConfig flow configuration * @return {@link CreateResponse} */ @Override public CreateResponse create(FlowConfig flowConfig) { LOG.info("Create called with flowName " + flowConfig.getId().getFlowName()); LOG.info("ReadyToUse is: " + readyToUse); LOG.info("FlowCatalog is: " + getFlowCatalog()); if (!readyToUse && getFlowCatalog() == null) { throw new RuntimeException("Not ready for use."); } try { URI flowCatalogURI = new URI("gobblin-flow", null, "/", null, null); URI flowUri = new URI(flowCatalogURI.getScheme(), flowCatalogURI.getAuthority(), "/" + flowConfig.getId().getFlowGroup() + "/" + flowConfig.getId().getFlowName(), null, null); if (getFlowCatalog().getSpec(flowUri) != null) { logAndThrowRestLiServiceException(HttpStatus.S_409_CONFLICT, "Flow with the same name already exists: " + flowUri, null); } } catch (URISyntaxException e) { logAndThrowRestLiServiceException(HttpStatus.S_400_BAD_REQUEST, "bad URI " + flowConfig.getId().getFlowName(), e); } catch (SpecNotFoundException e) { // okay if flow does not exist } getFlowCatalog().put(createFlowSpecForConfig(flowConfig)); return new CreateResponse(flowConfig.getId().getFlowName(), HttpStatus.S_201_CREATED); } /** * Update the flow configuration with the specified key. Running flows are not affected. * An error is raised if the flow configuration does not exist. * @param key composite key containing group name and flow name that identifies the flow to update * @param flowConfig new flow configuration * @return {@link UpdateResponse} */ @Override public UpdateResponse update(ComplexResourceKey<FlowId, EmptyRecord> key, FlowConfig flowConfig) { String flowGroup = key.getKey().getFlowGroup(); String flowName = key.getKey().getFlowName(); URI flowUri = null; LOG.info("Update called with flowGroup " + flowGroup + " flowName " + flowName); if (!flowGroup.equals(flowConfig.getId().getFlowGroup()) || !flowName.equals(flowConfig.getId().getFlowName())) { logAndThrowRestLiServiceException(HttpStatus.S_400_BAD_REQUEST, "flowName and flowGroup cannot be changed in update", null); } try { URI flowCatalogURI = new URI("gobblin-flow", null, "/", null, null); flowUri = new URI(flowCatalogURI.getScheme(), flowCatalogURI.getAuthority(), "/" + flowGroup + "/" + flowName, null, null); FlowSpec oldFlowSpec = (FlowSpec) getFlowCatalog().getSpec(flowUri); FlowSpec newFlowSpec = createFlowSpecForConfig(flowConfig); getFlowCatalog().put(newFlowSpec); return new UpdateResponse(HttpStatus.S_200_OK); } catch (URISyntaxException e) { logAndThrowRestLiServiceException(HttpStatus.S_400_BAD_REQUEST, "bad URI " + flowUri, e); } catch (SpecNotFoundException e) { logAndThrowRestLiServiceException(HttpStatus.S_404_NOT_FOUND, "Flow does not exist: flowGroup " + flowGroup + " flowName " + flowName, null); } return null; } /** * Delete a configured flow. Running flows are not affected. The schedule will be removed for scheduled flows. * @param key composite key containing flow group and flow name that identifies the flow to remove from the * {@link FlowCatalog} * @return {@link UpdateResponse} */ @Override public UpdateResponse delete(ComplexResourceKey<FlowId, EmptyRecord> key) { String flowGroup = key.getKey().getFlowGroup(); String flowName = key.getKey().getFlowName(); URI flowUri = null; LOG.info("Delete called with flowGroup " + flowGroup + " flowName " + flowName); try { URI flowCatalogURI = new URI("gobblin-flow", null, "/", null, null); flowUri = new URI(flowCatalogURI.getScheme(), flowCatalogURI.getAuthority(), "/" + flowGroup + "/" + flowName, null, null); FlowSpec flowSpec = (FlowSpec) getFlowCatalog().getSpec(flowUri); getFlowCatalog().remove(flowUri); return new UpdateResponse(HttpStatus.S_200_OK); } catch (URISyntaxException e) { logAndThrowRestLiServiceException(HttpStatus.S_400_BAD_REQUEST, "bad URI " + flowUri, e); } catch (SpecNotFoundException e) { logAndThrowRestLiServiceException(HttpStatus.S_404_NOT_FOUND, "Flow does not exist: flowGroup " + flowGroup + " flowName " + flowName, null); } return null; } /*** * This method is to workaround injection issues where Service has only one active global FlowCatalog * .. and is not able to inject it via RestLI bootstrap. We should remove this and make injected * .. FlowCatalog standard after injection works and recipe is documented here. * @return FlowCatalog in use. */ private FlowCatalog getFlowCatalog() { if (null != _globalFlowCatalog) { return _globalFlowCatalog; } return this._flowCatalog; } }