import { fromJS } from 'immutable'; import { normalize, arrayOf } from 'normalizr'; import when from 'when'; import CurrentPlanActions from '../actions/CurrentPlanActions'; import { browserHistory } from 'react-router'; import logger from '../services/logger'; import MistralApiService from '../services/MistralApiService'; import MistralApiErrorHandler from '../services/MistralApiErrorHandler'; import NotificationActions from '../actions/NotificationActions'; import PlansConstants from '../constants/PlansConstants'; import { planFileSchema } from '../normalizrSchemas/plans'; import StackActions from '../actions/StacksActions'; import SwiftApiErrorHandler from '../services/SwiftApiErrorHandler'; import SwiftApiService from '../services/SwiftApiService'; import MistralConstants from '../constants/MistralConstants'; export default { requestPlans() { return { type: PlansConstants.REQUEST_PLANS }; }, receivePlans(plans) { return { type: PlansConstants.RECEIVE_PLANS, payload: plans }; }, fetchPlans() { return dispatch => { dispatch(this.requestPlans()); MistralApiService.runAction(MistralConstants.PLAN_LIST).then((response) => { let plans = JSON.parse(response.output).result || []; dispatch(this.receivePlans(plans)); dispatch(CurrentPlanActions.detectPlan()); }).catch((error) => { logger.error('Error in PlansActions.fetchPlans', error.stack || error); let errorHandler = new MistralApiErrorHandler(error); errorHandler.errors.forEach((error) => { dispatch(NotificationActions.notify(error)); }); }); }; }, requestPlan() { return { type: PlansConstants.REQUEST_PLAN }; }, receivePlan(planName, planFiles) { return { type: PlansConstants.RECEIVE_PLAN, payload: { planName: planName, planFiles: planFiles } }; }, fetchPlan(planName) { return dispatch => { dispatch(this.requestPlan()); SwiftApiService.getContainer(planName).then(response => { dispatch(this.receivePlan(planName, normalize(response, arrayOf(planFileSchema)).entities.planFiles)); }).catch(error => { logger.error('Error retrieving plan PlansActions.fetchPlan', error.stack || error); let errorHandler = new SwiftApiErrorHandler(error); errorHandler.errors.forEach((error) => { dispatch(NotificationActions.notify(error)); }); }); }; }, updatePlanPending(planName) { return { type: PlansConstants.UPDATE_PLAN_PENDING, payload: planName }; }, updatePlanSuccess(planName) { return { type: PlansConstants.UPDATE_PLAN_SUCCESS, payload: planName }; }, updatePlanFailed(planName) { return { type: PlansConstants.UPDATE_PLAN_FAILED, payload: planName }; }, updatePlan(planName, planFiles) { return dispatch => { dispatch(this.updatePlanPending(planName)); this._uploadFilesToContainer(planName, fromJS(planFiles), dispatch).then(() => { dispatch(this.updatePlanSuccess(planName)); browserHistory.push('/plans/list'); dispatch(NotificationActions.notify({ title: 'Plan Updated', message: `The plan ${planName} was successfully updated.`, type: 'success' })); dispatch(this.fetchPlans()); }).catch((errors) => { logger.error('Error in PlansActions.updatePlan', errors); errors.forEach((error) => { dispatch(NotificationActions.notify(error)); }); dispatch(this.updatePlanFailed(planName)); }); }; }, updatePlanFromTarball(planName, file) { return (dispatch) => { dispatch(this.updatePlanPending(planName)); SwiftApiService.uploadTarball(planName, file).then((response) => { dispatch(this.updatePlanSuccess(planName)); browserHistory.push('/plans/list'); dispatch(NotificationActions.notify({ title: 'Plan Updated', message: `The plan ${planName} was successfully updated.`, type: 'success' })); dispatch(this.fetchPlans()); }).catch((error) => { logger.error('Error in PlansActions.updatePlanFromTarball', error); let errorHandler = new SwiftApiErrorHandler(error); errorHandler.errors.forEach((error) => { dispatch(NotificationActions.notify(error)); }); dispatch(this.updatePlanFailed(planName)); }); }; }, cancelCreatePlan () { return { type: PlansConstants.CANCEL_CREATE_PLAN }; }, createPlanPending() { return { type: PlansConstants.CREATE_PLAN_PENDING }; }, createPlanSuccess() { return { type: PlansConstants.CREATE_PLAN_SUCCESS }; }, createPlanFailed(errors) { return { type: PlansConstants.CREATE_PLAN_FAILED, payload: errors }; }, /* * Uploads a number of files to a container. * Returns a promise which gets resolved when all files are uploaded * or rejected if >= 1 objects fail. * @container: String * @files: Immutable Map */ _uploadFilesToContainer(container, files, dispatch) { let uploadedFiles = 0; return when.promise((resolve, reject) => { files.forEach((value, key) => { SwiftApiService.createObject(container, key, value.get('contents')).then((response) => { // On success increase nr of uploaded files. // If this is the last file in the map, resolve the promise. if(uploadedFiles === files.size - 1) { resolve(); } uploadedFiles += 1; }).catch((error) => { // Reject the promise on the first file that fails. logger.error('Error in PlansActions.createPlan', error); let errorHandler = new SwiftApiErrorHandler(error); reject(errorHandler.errors); }); }); }); }, createPlan(planName, planFiles) { return (dispatch, getState) => { dispatch(this.createPlanPending()); SwiftApiService.createContainer(planName).then((response) => { // Upload all files to container first. this._uploadFilesToContainer(planName, fromJS(planFiles), dispatch).then(() => { // Once all files are uploaded, start plan creation workflow. MistralApiService.runWorkflow( MistralConstants.PLAN_CREATE, { container: planName } ).then((response) => { if(response.state === 'ERROR') { logger.error('Error in PlansActions.createPlan', response); dispatch(NotificationActions.notify({ title: 'Error', message: response.state_info })); dispatch(this.createPlanFailed()); } }).catch((error) => { logger.error('Error in PlansActions.createPlan', error); let errorHandler = new MistralApiErrorHandler(error); errorHandler.errors.forEach((error) => { dispatch(NotificationActions.notify(error)); }); dispatch(this.createPlanFailed()); }); }).catch((errors) => { logger.error('Error in PlansActions.createPlan', errors); // If the file upload fails, just notify the user errors.forEach((error) => { dispatch(NotificationActions.notify(error)); }); dispatch(this.createPlanFailed()); }); }).catch((error) => { logger.error('Error in PlansActions.createPlan', error); let errorHandler = new SwiftApiErrorHandler(error); errorHandler.errors.forEach((error) => { dispatch(NotificationActions.notify(error)); }); dispatch(this.createPlanFailed()); }); }; }, createPlanFinished(payload) { return (dispatch) => { if(payload.status === 'SUCCESS') { dispatch(this.createPlanSuccess()); dispatch(NotificationActions.notify({ type: 'success', title: 'Plan was created', message: `The plan ${payload.execution.input.container} was successfully created` })); dispatch(this.fetchPlans()); browserHistory.push('/plans/list'); } else { dispatch(this.createPlanFailed([{ title: 'Error', message: payload.message }])); } }; }, createPlanFromTarball(planName, file) { return (dispatch) => { dispatch(this.createPlanPending()); SwiftApiService.uploadTarball(planName, file).then((response) => { MistralApiService.runWorkflow( MistralConstants.PLAN_CREATE, { container: planName } ).then((response) => { if(response.state === 'ERROR') { logger.error('Error in PlansActions.createPlanFromTarball', response); dispatch(this.createPlanFailed([{ title: 'Error', message: response.state_info }])); } }).catch((error) => { logger.error('Error in workflow in PlansActions.createPlanFromTarball', error); let errorHandler = new MistralApiErrorHandler(error); dispatch(this.createPlanFailed(errorHandler.errors)); }); }).catch((error) => { logger.error('Error in Swift in PlansActions.createPlanFromTarball', error); let errorHandler = new SwiftApiErrorHandler(error); dispatch(this.createPlanFailed(errorHandler.errors)); }); }; }, deletePlanPending(planName) { return { type: PlansConstants.DELETE_PLAN_PENDING, payload: planName }; }, deletePlanSuccess(planName) { return { type: PlansConstants.DELETE_PLAN_SUCCESS, payload: planName }; }, deletePlanFailed(planName) { return { type: PlansConstants.DELETE_PLAN_FAILED, payload: planName }; }, deletePlan(planName) { return dispatch => { dispatch(this.deletePlanPending(planName)); browserHistory.push('/plans/list'); MistralApiService.runAction(MistralConstants.PLAN_DELETE, { container: planName }) .then(response => { dispatch(this.deletePlanSuccess(planName)); dispatch(NotificationActions.notify({ title: 'Plan Deleted', message: `The plan ${planName} was successfully deleted.`, type: 'success' })); dispatch(CurrentPlanActions.detectPlan()); }).catch(error => { logger.error('Error deleting plan MistralApiService.runAction', error); dispatch(this.planDeleted(planName)); let errorHandler = new MistralApiErrorHandler(error); errorHandler.errors.forEach((error) => { dispatch(NotificationActions.notify(error)); }); }); }; }, deployPlanPending(planName) { return { type: PlansConstants.START_DEPLOYMENT_PENDING, payload: planName }; }, deployPlanSuccess(planName) { return { type: PlansConstants.START_DEPLOYMENT_SUCCESS, payload: planName }; }, deployPlanFailed(planName) { return { type: PlansConstants.START_DEPLOYMENT_FAILED, payload: planName }; }, deployPlanFinished(payload) { return (dispatch) => { if(payload.status === 'FAILED') { dispatch(this.deployPlanFailed(payload.execution.input.container)); dispatch(NotificationActions.notify({ title: 'Deployment failed', message: payload.message, type: 'error' })); } else { dispatch(this.deployPlanSuccess(payload.execution.input.container)); dispatch(StackActions.fetchStacks()); } }; }, deployPlan(planName) { return dispatch => { dispatch(this.deployPlanPending(planName)); MistralApiService.runWorkflow( MistralConstants.DEPLOYMENT_DEPLOY_PLAN, { container: planName, timeout: 240 }).then((response) => { dispatch(StackActions.fetchStacks()); }).catch(error => { dispatch(this.deployPlanFailed(planName)); let errorHandler = new MistralApiErrorHandler(error); errorHandler.errors.forEach((error) => { dispatch(NotificationActions.notify(error)); }); }); }; } };