Adding integration with key vault
This commit is contained in:
1 parent
8c4377b197
commit
84ed1089ce
21 files changed
+209
-61
No files matched your search
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@superalgos/operations-client",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.13",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@superalgos/operations-client",
|
||||
"version": "0.0.13",
|
||||
"version": "0.0.14",
|
||||
"description": "Superalgos Operations client-side web app",
|
||||
"main": "src/index.js",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import clones from './clones';
|
||||
import teams from './teams';
|
||||
import keys from './keys';
|
||||
|
||||
export {
|
||||
clones,
|
||||
teams
|
||||
teams,
|
||||
keys
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const GET_ALL_KEYS = gql`
|
||||
query {
|
||||
keyVault_AvailableKeys {
|
||||
id
|
||||
key
|
||||
description
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default {
|
||||
GET_ALL_KEYS
|
||||
};
|
||||
@@ -30,5 +30,6 @@ export const clone = gql`
|
||||
botAvatar
|
||||
teamAvatar
|
||||
processName
|
||||
keyId
|
||||
}
|
||||
`;
|
||||
@@ -3,7 +3,6 @@ const availableMonths = ["January", "February", "March", "April", "May", "June",
|
||||
|
||||
const tradingStartModes ={
|
||||
live: "Live",
|
||||
competition: "Competition",
|
||||
backtest: "Backtest"
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -12,10 +12,10 @@ import Home from './views/Home'
|
||||
const App = () => (
|
||||
<MuiPickersUtilsProvider utils={LuxonUtils}>
|
||||
<Switch>
|
||||
<Route exact path='/operations/' component={Home} />
|
||||
<Route path='/operations/browse' component={BrowseClones} />
|
||||
<Route path='/operations/history' component={HistoryClones} />
|
||||
<Route path='/operations/add' component={AddClone} />
|
||||
<Route exact path='/clones/' component={Home} />
|
||||
<Route path='/clones/browse' component={BrowseClones} />
|
||||
<Route path='/clones/history' component={HistoryClones} />
|
||||
<Route path='/clones/add' component={AddClone} />
|
||||
</Switch>
|
||||
</MuiPickersUtilsProvider>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { Component } from 'react'
|
||||
import { graphql, compose, Query } from 'react-apollo'
|
||||
import { clones, teams } from '../../../GraphQL/Calls'
|
||||
import { clones, teams, keys } from '../../../GraphQL/Calls'
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import TopBar from '../../BannerTopBar'
|
||||
@@ -45,6 +45,7 @@ class AddClone extends Component {
|
||||
runAsTeam: false,
|
||||
teams: [],
|
||||
teamId: '',
|
||||
keyId: '',
|
||||
|
||||
// Indicator Bot
|
||||
startYear: 2019,
|
||||
@@ -60,7 +61,8 @@ class AddClone extends Component {
|
||||
isNewCloneConfirmationOpen: false,
|
||||
serverResponse: '',
|
||||
serverError: false,
|
||||
processNameError: false
|
||||
processNameError: false,
|
||||
keyIdError: false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,6 +232,60 @@ class AddClone extends Component {
|
||||
</React.Fragment>
|
||||
}
|
||||
|
||||
{ this.state.mode == "live" &&
|
||||
<React.Fragment>
|
||||
<Typography className={classes.typography} variant='subtitle1' align='justify'>
|
||||
Select one of your available Exchange API Keys to be used by this clone.
|
||||
</Typography>
|
||||
|
||||
<Query
|
||||
query={keys.GET_ALL_KEYS}
|
||||
>
|
||||
{({ loading, error, data }) => {
|
||||
if (loading) return (
|
||||
<TextField
|
||||
select
|
||||
label="Exchange API Key"
|
||||
className={classNames(classes.margin, classes.textField, classes.form)}
|
||||
value={this.state.keyId}
|
||||
onChange={(e)=> this.setState({keyId:e.target.value})}
|
||||
onBlur={(e)=>this.setState({keyIdError:false})}
|
||||
error={this.state.keyIdError}
|
||||
fullWidth
|
||||
>
|
||||
<MenuItem key='1' value='Loading keys...'>
|
||||
'Loading keys...'
|
||||
</MenuItem>
|
||||
</TextField>
|
||||
);
|
||||
if (error) return `Error! ${error.message}`;
|
||||
const list = data.keyVault_AvailableKeys.map((key) => (
|
||||
<MenuItem key={key.id} value={key.id}>
|
||||
{key.key}
|
||||
</MenuItem>
|
||||
));
|
||||
return (
|
||||
<TextField
|
||||
select
|
||||
label="Exchange API Key"
|
||||
className={classNames(classes.margin, classes.textField, classes.form)}
|
||||
value={this.state.keyId}
|
||||
onChange={(e)=> this.setState({keyId:e.target.value})}
|
||||
onBlur={(e)=>this.setState({keyIdError:false})}
|
||||
error={this.state.keyIdError}
|
||||
fullWidth
|
||||
>
|
||||
{ list }
|
||||
</TextField>
|
||||
);
|
||||
}}
|
||||
</Query>
|
||||
|
||||
|
||||
|
||||
</React.Fragment>
|
||||
}
|
||||
|
||||
<Typography className={classes.typography} variant='subtitle1' align='justify'>
|
||||
If Run as Team is selected, the clone will be taken from the
|
||||
team respository instead of your own team member folder.
|
||||
@@ -399,7 +455,8 @@ class AddClone extends Component {
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={this.handleNewCloneConfirmationClose} color="primary" autoFocus>
|
||||
<Button onClick={this.handleNewCloneConfirmationClose}
|
||||
variant='contained' color='secondary' autoFocus>
|
||||
Ok
|
||||
</Button>
|
||||
</DialogActions>
|
||||
@@ -431,7 +488,7 @@ class AddClone extends Component {
|
||||
let serverResponse = await this.createCloneOnServer()
|
||||
error = serverResponse.errors || !isDefined(serverResponse.data.operations_AddClone)
|
||||
if(error){
|
||||
this.state.serverResponse = serverResponse.errors[0].message
|
||||
this.state.serverResponse = serverResponse.errors[0].message || serverResponse.errors[0]
|
||||
this.state.serverError = true
|
||||
}else{
|
||||
this.state.serverResponse = "The new clone was sucessfully created."
|
||||
@@ -453,15 +510,16 @@ class AddClone extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
if(this.state.mode === "backtest"){
|
||||
variables.clone.beginDatetime = this.state.beginDatetime.valueOf() / 1000|0
|
||||
variables.clone.endDatetime = this.state.endDatetime.valueOf() / 1000|0
|
||||
variables.clone.waitTime = this.state.waitTime
|
||||
}
|
||||
|
||||
if(this.state.selectedBot.kind === "TRADER"){
|
||||
variables.clone.runAsTeam = this.state.runAsTeam
|
||||
variables.clone.processName = 'Trading-Process'
|
||||
if(this.state.mode === "backtest"){
|
||||
variables.clone.beginDatetime = this.state.beginDatetime.valueOf() / 1000|0
|
||||
variables.clone.endDatetime = this.state.endDatetime.valueOf() / 1000|0
|
||||
variables.clone.waitTime = this.state.waitTime
|
||||
} else {
|
||||
variables.clone.keyId = this.state.keyId
|
||||
}
|
||||
} else {
|
||||
variables.clone.processName = this.state.processName
|
||||
variables.clone.startYear = this.state.startYear
|
||||
@@ -508,6 +566,7 @@ class AddClone extends Component {
|
||||
processName: '',
|
||||
teams: [],
|
||||
teamId: '',
|
||||
keyId: '',
|
||||
|
||||
//Error handlers
|
||||
nameError: false,
|
||||
@@ -517,7 +576,8 @@ class AddClone extends Component {
|
||||
isNewCloneConfirmationOpen: false,
|
||||
serverResponse: '',
|
||||
serverError: false,
|
||||
processNameError: false
|
||||
processNameError: false,
|
||||
keyIdError: false
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
@@ -2,3 +2,32 @@
|
||||
## AA Operations Server
|
||||
|
||||
Server side functions support the business rules for the module.
|
||||
|
||||
## Available Methods
|
||||
|
||||
# Active clones List
|
||||
- List active personal clones
|
||||
- List active team clones (future release)
|
||||
|
||||
# History clone List
|
||||
- List history personal clones
|
||||
- List history team clones (future release)
|
||||
|
||||
# Get clone by id
|
||||
- Get existing clones corresponding to the clone ids passed by parameter and returns:
|
||||
- Clone id
|
||||
- Team id
|
||||
- Bot id
|
||||
|
||||
# Add new clones:
|
||||
Receive a list of clones, create each of them and return the newly created clone.
|
||||
A clone can be created:
|
||||
- As a personal clone (run from the personal user folder)
|
||||
- As a team clone (on competitions, run from the team folder)
|
||||
Who can create each type:
|
||||
- Personal clones are managed by the user who creates it, not visible for any other user.
|
||||
- Team clones are managed by any ADMIN type team member (on the team member role attribute),
|
||||
and all team admins will see it listed.
|
||||
|
||||
- Events Module:
|
||||
At enroll time, the clone is created and the authId is sent
|
||||
@@ -53,6 +53,5 @@ export class KubernateError extends Error {
|
||||
message = 'OperationsModule Kubernate Error. ' + this.message
|
||||
constructor(message){
|
||||
super(message)
|
||||
logger.error(this.stack)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import logger from '../config/logger'
|
||||
import axios from 'axios'
|
||||
|
||||
const authorizeClon = (authorization, keyId, cloneId, releaseClon) => {
|
||||
logger.debug('authorizeClon -> Entering function.')
|
||||
|
||||
return axios({
|
||||
url: process.env.GATEWAY_ENDPOINT,
|
||||
method: 'post',
|
||||
data: {
|
||||
query: `
|
||||
mutation keyVault_AuthorizeClone($keyId: String!, $cloneId: String!, $releaseClon: Boolean!){
|
||||
keyVault_AuthorizeClone(keyId: $keyId, cloneId: $cloneId, releaseClon:$releaseClon)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
keyId: keyId,
|
||||
cloneId: cloneId,
|
||||
releaseClon: releaseClon
|
||||
}
|
||||
},
|
||||
headers: {
|
||||
authorization: authorization
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default authorizeClon
|
||||
@@ -1,8 +1,8 @@
|
||||
import logger from '../config/logger'
|
||||
import axios from 'axios'
|
||||
|
||||
const teamQuery = async(authorization, teamId) => {
|
||||
logger.debug('GraphQL retrieving team information. %s', teamId)
|
||||
const teamQuery = (authorization, teamId) => {
|
||||
logger.debug('teamQuery -> Entering function')
|
||||
|
||||
return axios({
|
||||
url: process.env.GATEWAY_ENDPOINT,
|
||||
|
||||
@@ -104,6 +104,11 @@ const createClone = async (clone) => {
|
||||
"name": "WAIT_TIME",
|
||||
"value": clone.waitTime.toString()
|
||||
})
|
||||
} else{
|
||||
env.push({
|
||||
"name": "KEY_ID",
|
||||
"value": clone.keyId
|
||||
})
|
||||
}
|
||||
} else if(clone.botType === INDICATOR || clone.botType === EXTRACTION ){
|
||||
if(clone.mode !== NO_TIME){
|
||||
|
||||
@@ -23,6 +23,7 @@ const cloneSchema = new Schema({
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
keyId: String,
|
||||
mode: {
|
||||
type: String,
|
||||
enum: CloneModeEnum,
|
||||
|
||||
@@ -2,8 +2,8 @@ import logger from '../config/logger'
|
||||
import { isDefined } from '../config/utils'
|
||||
import { AutorizationError } from '../errors'
|
||||
|
||||
const cloneDetails = async(authId, team, clone) => {
|
||||
logger.debug('Retrieving clone details.')
|
||||
const cloneDetails = (authId, team, clone) => {
|
||||
logger.debug('cloneDetails -> Entering function.')
|
||||
|
||||
if(!(authId === (process.env.AUTH_CLIENT_ID + '@clients') || isAuthorized(team, authId))){
|
||||
logger.debug('User %s is not authorized to manage team %s.', authId, team.slug )
|
||||
@@ -22,12 +22,12 @@ const cloneDetails = async(authId, team, clone) => {
|
||||
clone.teamAvatar = team.profile.avatar
|
||||
clone.userLoggedIn = team.members[0].member.alias //TODO change by context info if possible
|
||||
|
||||
logger.debug('Retrieving clone details complete.')
|
||||
logger.debug('cloneDetails -> Retrieving clone details complete.')
|
||||
return clone
|
||||
};
|
||||
|
||||
function getSelectedBot(team, selectedBotId){
|
||||
logger.debug('Getting selected bot.')
|
||||
logger.debug('cloneDetails -> Getting selected bot.')
|
||||
if(isDefined(team)){
|
||||
for (var i = 0; i < team.fb.length; i++) {
|
||||
if(team.fb[i].id === selectedBotId){
|
||||
@@ -38,7 +38,7 @@ function getSelectedBot(team, selectedBotId){
|
||||
}
|
||||
|
||||
function isAuthorized(team, authId){
|
||||
logger.debug('Checking if the user is authorized by the teams module.')
|
||||
logger.debug('cloneDetails -> Checking if the user is authorized by the teams module.')
|
||||
let authorized = false
|
||||
if(isDefined(team)){
|
||||
for (var i = 0; i < team.members.length; i++) {
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
import { CloneInputType } from '../types/input';
|
||||
import { CloneType } from '../types';
|
||||
import { Clone } from '../../models';
|
||||
import { CloneModeEnum } from '../../enums/CloneMode';
|
||||
import { BotTypesEnum } from '../../enums/BotTypes';
|
||||
import { CloneInputType } from '../types/input'
|
||||
import { CloneType } from '../types'
|
||||
import { Clone } from '../../models'
|
||||
import { CloneModeEnum } from '../../enums/CloneMode'
|
||||
import { BotTypesEnum } from '../../enums/BotTypes'
|
||||
import teamQuery from '../../graphQLCalls/teamQuery'
|
||||
import authorizeClone from '../../graphQLCalls/authorizeClone'
|
||||
import cloneDetails from '../cloneDetails'
|
||||
|
||||
import {
|
||||
AuthentificationError,
|
||||
WrongArgumentsError,
|
||||
OperationsError
|
||||
OperationsError,
|
||||
AutorizationError
|
||||
} from '../../errors'
|
||||
|
||||
import logger from '../../config/logger'
|
||||
import createKubernetesClone from '../../kubernetes/createClone'
|
||||
import { isDefined } from '../../config/utils'
|
||||
import { LIVE } from '../../enums/CloneMode'
|
||||
|
||||
const args = {
|
||||
clone: {type: CloneInputType }
|
||||
clone: { type: CloneInputType }
|
||||
}
|
||||
|
||||
const resolve = async(parent, { clone }, context) => {
|
||||
@@ -27,40 +30,44 @@ const resolve = async(parent, { clone }, context) => {
|
||||
throw new AuthentificationError()
|
||||
}
|
||||
|
||||
if(!Object.values(CloneModeEnum).includes(clone.mode)){
|
||||
if (!Object.values(CloneModeEnum).includes(clone.mode)) {
|
||||
throw new WrongArgumentsError('The mode selected is not valid.')
|
||||
}
|
||||
|
||||
if(!Object.values(BotTypesEnum).includes(clone.botType)){
|
||||
if (!Object.values(BotTypesEnum).includes(clone.botType)) {
|
||||
throw new WrongArgumentsError('The bot type selected is not valid.')
|
||||
}
|
||||
|
||||
try{
|
||||
if (clone.mode === LIVE && !isDefined(clone.keyId)) {
|
||||
throw new WrongArgumentsError('The key was not provided to run the clone in Live mode.')
|
||||
}
|
||||
|
||||
try {
|
||||
let team = await teamQuery(context.authorization, clone.teamId)
|
||||
clone = await cloneDetails(context.userId, team.data.data.teams_TeamById, clone)
|
||||
clone.createDatetime = new Date().valueOf() / 1000|0
|
||||
clone.createDatetime = new Date().valueOf() / 1000 | 0
|
||||
clone.active = true
|
||||
|
||||
logger.debug('addClone -> Creating a new clone.')
|
||||
let newClone = new Clone(clone)
|
||||
newClone.authId = context.userId
|
||||
clone.id = newClone._id
|
||||
await createKubernetesClone(clone)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
newClone.save((err) => {
|
||||
if (err){
|
||||
logger.error('addClone -> Error: %s', err.stack)
|
||||
reject('There has been an error storing the clone.')
|
||||
}
|
||||
else {
|
||||
//TODO transaction on database
|
||||
logger.debug('addClone -> Save clone sucessful.')
|
||||
resolve(newClone)
|
||||
}
|
||||
})
|
||||
})
|
||||
} catch(err) {
|
||||
logger.debug('addClone -> Creating the clone on the Database.')
|
||||
let savedClone = await newClone.save()
|
||||
|
||||
if (clone.mode === LIVE) {
|
||||
logger.debug('addClone -> Authorizing clone to use the key.')
|
||||
let response = await authorizeClone(context.authorization, clone.keyId, clone.id, false)
|
||||
logger.debug('addClone -> Authorizing clone to use the key.')
|
||||
if (response.data.data.keyVault_AuthorizeClone === clone.keyId) {
|
||||
await createKubernetesClone(clone)
|
||||
} else {
|
||||
throw new AutorizationError()
|
||||
}
|
||||
}
|
||||
logger.debug('addClone -> Clone created sucessfully.')
|
||||
return savedClone
|
||||
} catch (err) {
|
||||
logger.error('addClone -> Error: %s', err.stack)
|
||||
throw new OperationsError('There has been an error creating the clone.')
|
||||
}
|
||||
|
||||
@@ -40,6 +40,9 @@ const resolve = async (parent, { id }, context) => {
|
||||
let team = await teamQuery(context.authorization, clone.teamId)
|
||||
clone = await cloneDetails(context.userId, team.data.data.teams_TeamById, clone)
|
||||
|
||||
logger.debug('removeClone -> Release the clone key.')
|
||||
await authorizeClone(context.authorization, clone.keyId, clone.id, true)
|
||||
|
||||
logger.debug('removeClone -> Removing Clone from Kubernates.')
|
||||
await removeKuberneteClone(clone)
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { Clone } from '../../models'
|
||||
import logger from '../../config/logger'
|
||||
import teamQuery from '../../graphQLCalls/teamQuery'
|
||||
import authorizeClone from '../../graphQLCalls/authorizeClone'
|
||||
import cloneDetails from '../cloneDetails'
|
||||
import removeKuberneteClone from '../../kubernetes/removeClone'
|
||||
import { isDefined } from '../../config/utils'
|
||||
@@ -55,9 +56,6 @@ const resolve = async (parent,
|
||||
let team = await teamQuery(context.authorization, clone.teamId)
|
||||
clone = await cloneDetails(context.userId, team.data.data.teams_TeamById, clone)
|
||||
|
||||
logger.debug('UpdateExecutionSummary -> Removing Clone from Kubernates.')
|
||||
await removeKuberneteClone(clone)
|
||||
|
||||
const query = { _id: id }
|
||||
const options = { new: true }
|
||||
const update = {
|
||||
|
||||
@@ -18,7 +18,7 @@ const args = {
|
||||
}
|
||||
|
||||
const resolve = async(parent, { cloneIdList }, context) => {
|
||||
logger.debug('Get Clones -> Entering Fuction. %j', cloneIdList)
|
||||
logger.debug('Get Clones -> Entering Fuction.')
|
||||
try{
|
||||
let clones = []
|
||||
for (var i = 0; i < cloneIdList.length; i++) {
|
||||
@@ -44,7 +44,6 @@ const resolve = async(parent, { cloneIdList }, context) => {
|
||||
id: cloneIdList[i]
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return clones
|
||||
|
||||
@@ -37,7 +37,8 @@ const CloneType = new GraphQLObjectType({
|
||||
teamName: {type: GraphQLString},
|
||||
botName: {type: GraphQLString},
|
||||
botAvatar: {type: GraphQLString},
|
||||
teamAvatar: {type: GraphQLString}
|
||||
teamAvatar: {type: GraphQLString},
|
||||
keyId: {type: GraphQLString}
|
||||
})
|
||||
})
|
||||
export default CloneType
|
||||
@@ -23,7 +23,8 @@ const Type = new GraphQLInputObjectType({
|
||||
month: { type: GraphQLInt },
|
||||
interval: { type: GraphQLInt },
|
||||
processName: { type: new GraphQLNonNull(GraphQLString) },
|
||||
botType: { type: new GraphQLNonNull(GraphQLString) }
|
||||
botType: { type: new GraphQLNonNull(GraphQLString) },
|
||||
keyId: { type: GraphQLString }
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user