Răsfoiți Sursa

Dynamically detect if a provider has env options

Dynamically detect if a provider can make `destination-options` or
`source-options` API calls to fill the destination / source options
schema. For this, we use Coriolis API's `constants.py` info.

Previously, the list of providers which support these calls was stored
in `config.js`. Note that we still need to store the providers which can
make additional destination / source API calls with a set of field
values, in the config file.
Sergiu Miclea 6 ani în urmă
părinte
comite
fa135777dc

+ 4 - 12
config.js

@@ -37,24 +37,16 @@ const conf: Config = {
   // - `Infinity` value means no `limit` will be used, i.e. all VMs will be listed.
   instancesListBackgroundLoading: { default: 10, ovm: Infinity },
 
-  // A list of providers for which `source-options` API call(s) will be made
-  // If the item is just a string with the provider name, only one API call will be made
-  // If the item has `envRequiredFields`, an additional API call will be made once the specified fields are filled
-  sourceProvidersWithExtraOptions: ['aws'],
-
-  // A list of providers for which `destination-options` API call(s) will be made
-  // If the item is just a string with the provider name, only one API call will be made
-  // If the item has `envRequiredFields`, an additional API call will be made once the specified fields are filled
-  destinationProvidersWithExtraOptions: [
-    'openstack',
-    'oracle_vm',
-    'aws',
+  // The providers for which an extra `source` or `destination options` call can be made with a set of field values
+  providersWithEnvOptions: [
     {
       name: 'azure',
+      type: 'destination',
       envRequiredFields: ['location', 'resource_group'],
     },
     {
       name: 'oci',
+      type: 'destination',
       envRequiredFields: ['compartment', 'availability_domain'],
     },
   ],

+ 24 - 24
src/components/organisms/EditReplica/EditReplica.jsx

@@ -18,7 +18,7 @@ import React from 'react'
 import { observer } from 'mobx-react'
 import styled from 'styled-components'
 
-import providerStore, { getFieldChangeDestOptions } from '../../../stores/ProviderStore'
+import providerStore, { getFieldChangeOptions } from '../../../stores/ProviderStore'
 import replicaStore from '../../../stores/ReplicaStore'
 import migrationStore from '../../../stores/MigrationStore'
 import endpointStore from '../../../stores/EndpointStore'
@@ -115,28 +115,27 @@ class EditReplica extends React.Component<Props, State> {
       if (this.hasStorageMap()) {
         endpointStore.loadStorage(this.props.destinationEndpoint.id, {})
       }
-    })
-
-    providerStore.loadDestinationSchema(this.props.destinationEndpoint.type, this.props.type || 'replica', useCache)
-      .then(() => providerStore.getOptionsValues({
-        optionsType: 'destination',
-        endpointId: this.props.destinationEndpoint.id,
-        provider: this.props.destinationEndpoint.type,
-        useCache,
-      })).then(() => {
-        this.loadEnvDestinationOptions()
-      })
+      providerStore.loadDestinationSchema(this.props.destinationEndpoint.type, this.props.type || 'replica', useCache)
+        .then(() => providerStore.getOptionsValues({
+          optionsType: 'destination',
+          endpointId: this.props.destinationEndpoint.id,
+          provider: this.props.destinationEndpoint.type,
+          useCache,
+        })).then(() => {
+          this.loadEnvDestinationOptions()
+        })
 
-    if (!this.hasSourceOptions()) {
-      return
-    }
-    providerStore.loadSourceSchema(this.props.sourceEndpoint.type, this.props.type || 'replica', useCache)
-      .then(() => providerStore.getOptionsValues({
-        optionsType: 'source',
-        endpointId: this.props.sourceEndpoint.id,
-        provider: this.props.sourceEndpoint.type,
-        useCache,
-      }))
+      if (!this.hasSourceOptions()) {
+        return
+      }
+      providerStore.loadSourceSchema(this.props.sourceEndpoint.type, this.props.type || 'replica', useCache)
+        .then(() => providerStore.getOptionsValues({
+          optionsType: 'source',
+          endpointId: this.props.sourceEndpoint.id,
+          provider: this.props.sourceEndpoint.type,
+          useCache,
+        }))
+    })
   }
 
   hasStorageMap(): boolean {
@@ -182,14 +181,15 @@ class EditReplica extends React.Component<Props, State> {
   }
 
   loadEnvDestinationOptions(field?: Field) {
-    let envData = getFieldChangeDestOptions({
+    let envData = getFieldChangeOptions({
       provider: this.props.destinationEndpoint.type,
-      destSchema: providerStore.destinationSchema,
+      schema: providerStore.destinationSchema,
       data: {
         ...this.parseReplicaData(this.props.replica.destination_environment),
         ...this.state.destinationData,
       },
       field,
+      type: 'destination',
     })
 
     if (envData) {

+ 14 - 5
src/components/pages/WizardPage/WizardPage.jsx

@@ -26,7 +26,7 @@ import Modal from '../../molecules/Modal'
 import Endpoint from '../../organisms/Endpoint'
 
 import userStore from '../../../stores/UserStore'
-import providerStore, { getFieldChangeDestOptions } from '../../../stores/ProviderStore'
+import providerStore, { getFieldChangeOptions } from '../../../stores/ProviderStore'
 import endpointStore from '../../../stores/EndpointStore'
 import wizardStore from '../../../stores/WizardStore'
 import instanceStore from '../../../stores/InstanceStore'
@@ -248,7 +248,11 @@ class WizardPage extends React.Component<Props, State> {
     // Preload destination options schema
     providerStore.loadDestinationSchema(target.type, this.state.type).then(() => {
       // Preload destination options values
-      providerStore.getOptionsValues({ optionsType: 'destination', endpointId: target.id, provider: target.type })
+      providerStore.getOptionsValues({
+        optionsType: 'destination',
+        endpointId: target.id,
+        provider: target.type,
+      })
     })
     if (this.pages.find(p => p.id === 'storage')) {
       endpointStore.loadStorage(target.id, {})
@@ -349,11 +353,12 @@ class WizardPage extends React.Component<Props, State> {
 
   loadEnvDestinationOptions(field?: Field) {
     let provider = wizardStore.data.target && wizardStore.data.target.type
-    let envData = getFieldChangeDestOptions({
+    let envData = getFieldChangeOptions({
       provider: wizardStore.data.target && wizardStore.data.target.type,
-      destSchema: providerStore.destinationSchema,
+      schema: providerStore.destinationSchema,
       data: wizardStore.data.destOptions,
       field,
+      type: 'destination',
     })
 
     if (provider && envData && wizardStore.data.target) {
@@ -381,7 +386,11 @@ class WizardPage extends React.Component<Props, State> {
           providerStore.loadSourceSchema(source.type, this.state.type).then(() => {
             // Preload source options if data is set from 'Permalink'
             if (providerStore.sourceOptions.length === 0 && source) {
-              providerStore.getOptionsValues({ optionsType: 'source', endpointId: source.id, provider: source.type })
+              providerStore.getOptionsValues({
+                optionsType: 'source',
+                endpointId: source.id,
+                provider: source.type,
+              })
             }
           })
         }

+ 3 - 8
src/constants.js

@@ -43,20 +43,15 @@ export const navigationMenu = [
   { label: 'Users', value: 'users', requiresAdmin: true },
 ]
 
-/* https://github.com/cloudbase/coriolis/blob/master/coriolis/constants.py
-PROVIDER_TYPE_IMPORT = 1 // migration target schema
-PROVIDER_TYPE_EXPORT = 2 // migration source schema
-PROVIDER_TYPE_REPLICA_IMPORT = 4 // replica target schema
-PROVIDER_TYPE_REPLICA_EXPORT = 8 // replica source schema
-PROVIDER_TYPE_ENDPOINT_STORAGE = 32768
-PROVIDER_TYPE_SOURCE_REPLICA_UPDATE = 65536 // the replica can be updated if provider is source
-PROVIDER_TYPE_DESTINATION_REPLICA_UPDATE = 262144 // the replica can be updated if provider is source */
+// https://github.com/cloudbase/coriolis/blob/master/coriolis/constants.py
 export const providerTypes = {
   TARGET_MIGRATION: 1,
   SOURCE_MIGRATION: 2,
   TARGET_REPLICA: 4,
   SOURCE_REPLICA: 8,
   CONNECTION: 16,
+  DESTINATION_OPTIONS: 512,
+  SOURCE_OPTIONS: 131072,
   STORAGE: 32768,
   SOURCE_UPDATE: 65536,
   TARGET_UPDATE: 262144,

+ 61 - 52
src/stores/ProviderStore.js

@@ -18,27 +18,30 @@ import { observable, action, computed } from 'mobx'
 
 import ProviderSource from '../sources/ProviderSource'
 import configLoader from '../utils/Config'
+import { providerTypes } from '../constants'
 import { OptionsSchemaPlugin } from '../plugins/endpoint'
 import type { OptionValues } from '../types/Endpoint'
 import type { Field } from '../types/Field'
 import type { Providers } from '../types/Providers'
 
-export const getFieldChangeDestOptions = (config: {
+export const getFieldChangeOptions = (config: {
   provider: ?string,
-  destSchema: Field[],
+  schema: Field[],
   data: any,
   field: ?Field,
+  type: 'source' | 'destination',
 }) => {
-  let { provider, destSchema, data, field } = config
-  let providerWithExtraOptions = configLoader.config.destinationProvidersWithExtraOptions
-    .find(p => typeof p !== 'string' && p.name === provider)
-  if (!provider || !providerWithExtraOptions || typeof providerWithExtraOptions === 'string' || !providerWithExtraOptions.envRequiredFields) {
+  let { provider, schema, data, field, type } = config
+  let providerWithEnvOptions = configLoader.config.providersWithEnvOptions.find(p => p.name === provider && p.type === type)
+
+  if (!provider || !providerWithEnvOptions) {
     return null
   }
+  let envRequiredFields = providerWithEnvOptions.envRequiredFields
 
-  let findFieldInSchema = (name: string) => destSchema.find(f => f.name === name)
+  let findFieldInSchema = (name: string) => schema.find(f => f.name === name)
 
-  let validFields = providerWithExtraOptions.envRequiredFields.filter(fn => {
+  let validFields = envRequiredFields.filter(fn => {
     let schemaField = findFieldInSchema(fn)
     if (data) {
       if (data[fn] === null) {
@@ -53,7 +56,7 @@ export const getFieldChangeDestOptions = (config: {
   })
 
   let isCurrentFieldValid = field ? validFields.find(fn => field ? fn === field.name : false) : true
-  if (validFields.length !== providerWithExtraOptions.envRequiredFields.length || !isCurrentFieldValid) {
+  if (validFields.length !== envRequiredFields.length || !isCurrentFieldValid) {
     return null
   }
 
@@ -123,7 +126,10 @@ class ProviderStore {
   }
 
   @action loadProviders(): Promise<void> {
-    this.providers = null
+    if (this.providers) {
+      return Promise.resolve()
+    }
+
     this.providersLoading = true
 
     return ProviderSource.loadProviders().then((providers: Providers) => {
@@ -189,56 +195,59 @@ class ProviderStore {
     envData?: { [string]: mixed },
     useCache?: boolean,
   }): Promise<OptionValues[]> {
-    let { provider, optionsType, endpointId, envData, useCache } = config
-    let providers = optionsType === 'source' ?
-      configLoader.config.sourceProvidersWithExtraOptions :
-      configLoader.config.destinationProvidersWithExtraOptions
-    let providerWithExtraOptions = providers.find(p => typeof p === 'string' ? p === provider : p.name === provider)
-    if (!providerWithExtraOptions) {
-      return Promise.resolve([])
-    }
-
-    if (useCache) {
-      let key = `${endpointId}-${provider}-${optionsType}-${JSON.stringify(envData)}`
-      let cacheItem = this.cache.find(c => c.key === key)
-      if (cacheItem) {
-        this.getOptionsValuesSuccess(optionsType, provider, cacheItem.data)
-        this.getOptionsValuesDone(optionsType)
-        return Promise.resolve(cacheItem.data)
+    return this.loadProviders().then(() => {
+      let { provider, optionsType, endpointId, envData, useCache } = config
+      let providerType = optionsType === 'source' ? providerTypes.SOURCE_OPTIONS : providerTypes.DESTINATION_OPTIONS
+      if (!this.providers) {
+        return Promise.resolve([])
+      }
+      let providerWithExtraOptions = this.providers[provider].types.find(t => t === providerType)
+      if (!providerWithExtraOptions) {
+        return Promise.resolve([])
       }
-    }
-
-    if (optionsType === 'source') {
-      this.sourceOptionsLoading = true
-      this.sourceOptions = []
-    } else {
-      this.destinationOptionsLoading = true
-      this.destinationOptions = []
-    }
-
-    let optionsValues = []
 
-    return ProviderSource.getOptionsValues(optionsType, endpointId, envData).then(options => {
-      this.getOptionsValuesSuccess(optionsType, provider, options)
-      optionsValues = options
       if (useCache) {
         let key = `${endpointId}-${provider}-${optionsType}-${JSON.stringify(envData)}`
-        if (this.cache.length > 20) {
-          this.cache.splice(0)
+        let cacheItem = this.cache.find(c => c.key === key)
+        if (cacheItem) {
+          this.getOptionsValuesSuccess(optionsType, provider, cacheItem.data)
+          this.getOptionsValuesDone(optionsType)
+          return Promise.resolve(cacheItem.data)
         }
-        this.cache.push({ key, data: options })
       }
-    }).catch(err => {
-      console.error(err)
+
       if (optionsType === 'source') {
-        return this.loadSourceSchema(provider, this.lastSourceSchemaType)
-          .then(() => envData ? this.getOptionsValues({ endpointId, provider, optionsType }) : null)
+        this.sourceOptionsLoading = true
+        this.sourceOptions = []
+      } else {
+        this.destinationOptionsLoading = true
+        this.destinationOptions = []
       }
-      return this.loadDestinationSchema(provider, this.lastDestinationSchemaType)
-        .then(() => envData ? this.getOptionsValues({ endpointId, provider, optionsType }) : null)
-    }).then(() => {
-      this.getOptionsValuesDone(optionsType)
-      return optionsValues
+
+      let optionsValues = []
+
+      return ProviderSource.getOptionsValues(optionsType, endpointId, envData).then(options => {
+        this.getOptionsValuesSuccess(optionsType, provider, options)
+        optionsValues = options
+        if (useCache) {
+          let key = `${endpointId}-${provider}-${optionsType}-${JSON.stringify(envData)}`
+          if (this.cache.length > 20) {
+            this.cache.splice(0)
+          }
+          this.cache.push({ key, data: options })
+        }
+      }).catch(err => {
+        console.error(err)
+        if (optionsType === 'source') {
+          return this.loadSourceSchema(provider, this.lastSourceSchemaType)
+            .then(() => envData ? this.getOptionsValues({ endpointId, provider, optionsType }) : null)
+        }
+        return this.loadDestinationSchema(provider, this.lastDestinationSchemaType)
+          .then(() => envData ? this.getOptionsValues({ endpointId, provider, optionsType }) : null)
+      }).then(() => {
+        this.getOptionsValuesDone(optionsType)
+        return optionsValues
+      })
     })
   }
 

+ 1 - 2
src/types/Config.js

@@ -9,8 +9,7 @@ export type Config = {
   requestPollTimeout: number,
   sourceOptionsProviders: string[],
   instancesListBackgroundLoading: { default: number, [string]: number },
-  sourceProvidersWithExtraOptions: Array<string | { name: string, envRequiredFields: string[] }>,
-  destinationProvidersWithExtraOptions: Array<string | { name: string, envRequiredFields: string[] }>,
+  providersWithEnvOptions: Array<{ name: string, type: 'source' | 'destination', envRequiredFields: string[] }>,
   providerSortPriority: { [providerName: string]: number },
   hiddenUsers: string[],
 }