UNPKG

@grucloud/core

Version:

GruCloud core, generate infrastructure code

1,081 lines (1,051 loc) 29.4 kB
const assert = require("assert"); const { pipe, tap, map, filter, tryCatch, switchCase, get, assign, any, reduce, not, and, or, transform, fork, } = require("rubico"); const { first, append, isEmpty, isString, isObject, isFunction, callProp, pluck, forEach, size, identity, unless, when, values, } = require("rubico/x"); const generator = require("generate-password"); const mergeWith = require("lodash/fp/mergeWith"); const memoize = require("lodash.memoize"); const logger = require("./logger")({ prefix: "CoreResources" }); const { tos } = require("./tos"); const { retryCall } = require("./Retry"); const { convertError } = require("./Common"); const { decorateLive } = require("./Client"); const { deepDefaults } = require("./utils/deepDefault"); const { multiline } = require("./multiline"); exports.ResourceMaker = ({ name: resourceName, namespace = "", meta, dependencies = () => ({}), readOnly, isDefault, spec, filterLives = switchCase([ () => readOnly, () => spec.findResource, () => isDefault, () => spec.findDefault, () => undefined, ])(), properties = () => ({}), attributes = () => ({}), provider, programOptions, }) => { assert(programOptions); assert(spec); const { type, group, groupType } = spec; assert(groupType); assert(provider); const config = provider.getConfig(); const getId = ({ group, type, name, path = "id", suffix = "" }) => pipe([ tap(() => { assert(group); assert(type); assert(name); assert(path); }), () => name, provider.lives.getByName({ group, type, //TODO //providerName: config.providerName, }), tap((params) => { assert(true); }), get( path, `<< ${path} of ${group}::${type}::${name} not available yet >>` ), append(suffix), tap((params) => { assert(true); }), ])(); const resourceNameToString = (resourceName) => switchCase([ () => isFunction(resourceName), pipe([() => resourceName({ config: provider.getConfig() })]), () => resourceName, ])(); const getResourceName = memoize( pipe([ tap((params) => { assert(true); }), switchCase([ () => spec.inferName, pipe([ tap((params) => { assert( spec.inferName, `resource ${spec.type} without name must implement 'inferName'` ); assert( isFunction( spec.inferName({ resourceName: resourceNameToString(resourceName), dependenciesSpec: dependencies({ config }), dependencies: getDependencies(), }) ), `spec.inferName should return a function for ${spec.groupType}` ); }), () => resourceName, resourceNameToString, (resourceNameString) => pipe([ () => properties({ config, getId, generatePassword: generator.generate, multiline, }), tap((params) => { assert(true); }), spec.inferName({ resourceName: resourceNameString, dependenciesSpec: dependencies({ config }), dependencies: getDependencies(), }), ])(), tap((name) => { assert(name, `empty inferName for ${spec.groupType}`); }), ]), () => isFunction(resourceName), () => resourceName({ config: provider.getConfig() }), () => isString(resourceName), () => resourceName, (resourceName) => { throw Error( `resource name ${JSON.stringify( resourceName )} is neither empty, nor a string, nor a function, ${ spec.groupType }` ); }, ]), tap((name) => { assert(name, `resource name is empty for ${groupType}`); }), ]) ); // logger.debug( // `ResourceMaker: ${JSON.stringify({ // type, // group, // //resourceName: getResourceName(), // namespace, // meta, // // programOptions, // })}` // ); const getDependencies = pipe([ () => dependencies, tap((params) => { assert(true); }), unless(isFunction, (dependencies) => () => ({ ...dependencies })), (dep) => () => dep({ config, resources: provider.resources() }), (dep) => () => spec.transformDependencies({ provider })(dep()), (dep) => () => mapDependenciesNameToResource( dep({ config, resources: provider.resources() }) ), ]); const getClient = () => provider.getClient(spec); const getLive = ({ deep = true, options = {}, resolvedDependencies } = {}) => pipe([ tap((params) => { //assert(resolvedDependencies); }), tap(() => { //logger.info(`getLive ${toString()}, deep: ${deep}`); }), getResourceName, (name) => getClient().getByName({ provider, name, namespace, meta, dependencies: getDependencies(), resolvedDependencies, properties, resolveConfig, deep, resources: provider.getResourcesByType(spec), lives: provider.lives, config: provider.config, isDefault, }), unless( or([isEmpty, () => isEmpty(provider.lives)]), tap( pipe([ decorateLive({ client: getClient(), lives: provider.lives, config, options, }), tap((resource) => { provider.lives.addResource({ groupType, resource, }); }), ]) ) ), tap((live) => { //logger.info(`getLive ${toString()} hasLive: ${!!live}`); //logger.debug(`getLive ${toString()} live: ${tos(live)}`); }), ])(); const findLive = ({}) => pipe([ getResourceName, tap((params) => { assert(true); }), provider.lives.getByName({ providerName: provider.name, type, group, }), tap((xxx) => { assert(true); }), get("live"), tap.if(isEmpty, (live) => { logger.debug( `findLive no live for ${JSON.stringify({ type, group, resourceName: getResourceName(), })}` ); }), ])(); const planUpdate = ({ resource, target, live, targetResources }) => pipe([ tap(() => { // logger.debug( // `planUpdate resource: ${tos(resource.toJSON())}, target: ${tos( // target // )}, live: ${tos(live)}` // ); assert(Array.isArray(targetResources)); assert(spec.compare, `no compare for ${spec.groupType}`); assert(Array.isArray(spec.omitProperties)); assert(Array.isArray(spec.omitPropertiesExtra)); }), () => spec.compare({ ...spec, target, live, dependencies: resource.dependencies(), lives: provider.lives, config, programOptions, targetResources, }), tap((diff) => { assert(diff); //logger.debug(`planUpdate diff ${tos(diff)}`); }), (diff) => pipe([ () => diff, switchCase([ get("hasDiff"), () => pipe([ () => [ { action: "UPDATE", resource: resource.toJSON(), target: resource.spec.displayResource()(target), live: resource.spec.displayResource()(live), id: getClient().findId({ lives: provider.lives, config: provider.getConfig(), })(live), diff, providerName: resource.toJSON().providerName, }, ], tap((updateItem) => { //logger.debug(`updateItem ${tos(updateItem)}`); }), ])(), () => { logger.info(`planUpdate diff no update`); }, ]), ])(), ])(); const getDependencyList = () => pipe([ tap(() => { // logger.info(`getDependencyList ${type} `); }), getDependencies(), filter(and([not(isString), not(isEmpty)])), transform(map(identity), () => []), //TODO filter(not(isEmpty)) tap((result) => { //logger.info(`getDependencyList ${type} `); }), tap( forEach((dep) => { assert(dep, `dep ${type}`); assert(dep.type, "dep.type"); }) ), ])(); const dependencyNameToResource = ({ key: depKey, value, providerName }) => pipe([ tap(() => { assert(depKey); assert(value); }), () => spec.dependencies, tap((dependencies) => { //assert(dependencies, "missing dependency"); }), map.entries(([key, value]) => [ key, // TODO rubico eq when( () => key === depKey, () => value )(), ]), filter(not(isEmpty)), values, first, switchCase([ isEmpty, () => { throw Error( `Cannot find the dependency ${depKey}, ${value}, groupType: ${spec.groupType} ` ); }, pipe([ tap((params) => { assert(true); }), assign({ name: () => value }), switchCase([ () => providerName, assign({ providerName: () => providerName, }), assign({ providerName: () => spec.providerName, }), ]), provider.getResource, tap.if(isEmpty, () => { logger.info(`no resource`); // logger.info( // `no resource for ${uri}, available resources:\n${[ // ...mapGloblalNameToResource.keys(), // ].join("\n")} )}` // ); }), ]), ]), ])(); const mapDependenciesNameToResource = pipe([ tap((params) => { assert(true); }), map.entries(([key, value]) => [ key, pipe([ () => value, switchCase([ isString, pipe([() => ({ key, value }), dependencyNameToResource]), Array.isArray, pipe([ map( pipe([ switchCase([ isString, (name) => dependencyNameToResource({ key, value: name }), isObject, pipe([ tap((params) => { assert(true); }), ({ name, provider: providerName }) => dependencyNameToResource({ key, value: name, providerName, }), ]), (elem) => { assert( false, "dependency array item should be a string or an object" ); }, ]), ]) ), ]), isObject, pipe([ tap(({ name, provider }) => { assert(name); assert(provider); assert(isString(provider)); }), ({ name, provider }) => dependencyNameToResource({ key, value: name, providerName: provider, }), tap((params) => { assert(true); }), ]), identity, ]), ])(), ]), filter(not(isEmpty)), tap((params) => { assert(true); }), ]); const resolveDependencies = ({ dependencies, dependenciesMustBeUp = false, }) => pipe([ tap(() => { assert(isFunction(dependencies)); }), dependencies, tap((params) => { assert(true); }), //TODO filter(or([not(isEmpty) /*, not(isString)*/])), tap((params) => { assert(true); }), map( switchCase([ isEmpty, () => { logger.error(`${toString()} has undefined dependencies`); }, isString, identity, // Recursive not(get("getLive")), tryCatch( (dependency) => resolveDependencies({ dependencies: () => dependency, dependenciesMustBeUp, }), (error, dependency) => { logger.error( `resolveDependencies: ${toString()}, dep ${dependency.toString()}, error: ${tos( error )}` ); return { error: convertError({ error }), }; } ), (dependency) => tryCatch( pipe([ tap(() => { // logger.debug( // `resolveDependencies ${toString()}, dep ${dependency.toString()}` // ); }), () => dependency, switchCase([ // TODO readOnly : spec.findResource, useDefault: spec.findDefault () => dependency.filterLives, () => dependency.resolveConfig({ deep: true }), pipe([() => dependency.findLive({})]), ]), tap.if( switchCase([ (live) => dependenciesMustBeUp && !dependency.isUp({ live }), () => true, () => false, ]), () => { throw { message: `${toString()} dependency ${dependency.toString()} is not up`, }; } ), async (live) => ({ resource: dependency, config: await dependency.resolveConfig({ deep: true, live, }), live, }), ]), (error) => { logger.error(`resolveDependencies: ${tos(error)}`); return { item: { resource: dependency.toString() }, error: convertError({ error }), }; } )(), ]) ), tap((result) => { /*logger.debug( `resolveDependencies for ${()}, result: ${tos(result)}` );*/ }), tap.if(any(get("error")), (resolvedDependencies) => { logger.error( `resolveDependencies ${toString()} error in resolveDependencies` ); const results = filter(get("error"))(resolvedDependencies); throw { message: pipe([ () => results, pluck("error"), reduce((acc, value) => [...acc, value.message], []), callProp("join", "\n"), tap((message) => { logger.debug( `resolveDependencies ${toString()}, error message: ${message}` ); }), ])(), errorClass: "Dependency", results, }; }), tap((result) => { // logger.debug( // `resolveDependencies for ${toString()}, result: ${tos(result)}` // ); }), ])(); const customizerMergeArray = (objValue, srcValue) => pipe([ tap((result) => { assert(true); }), when( () => Array.isArray(objValue), () => srcValue ), ])(); const resolveConfig = ({ live, resolvedDependencies, deep = false } = {}) => pipe([ tap(() => { // logger.debug( // `resolveConfig ${toString()}, ${JSON.stringify({ // deep, // hasLive: !!live, // })}` // ); if (!live) { assert(true); } assert(getClient().configDefault); }), switchCase([ () => !deep, () => ({}), () => !isEmpty(resolvedDependencies), () => resolvedDependencies, () => resolveDependencies({ resourceName: getResourceName(), dependencies: getDependencies(), }), ]), (resolvedDependencies) => switchCase([ () => filterLives, pipe([ provider.lives.getByType({ type, group, providerName: provider.name, }), // tap((resources) => { // logger.debug( // `resolveConfig ${type} #resources ${size(resources)}` // ); // }), (resources) => filterLives({ name: getResourceName(), dependencies: resolvedDependencies, resources, configProvider: provider.config, live, lives: provider.lives, }), get("live"), tap((live) => { // logger.debug( // `resolveConfig filterLives ${getResourceName()}: ${tos(live)}` // ); }), ]), pipe([ () => properties({ config: provider.getConfig(), dependencies: resolvedDependencies, getId, generatePassword: generator.generate, multiline, }), (properties = {}) => getClient().configDefault({ name: getResourceName(), resourceName: resourceNameToString(resourceName), meta, namespace, properties: pipe([ tap((params) => { assert(true); }), () => properties, mergeWith(customizerMergeArray, spec.propertiesDefault), tap((params) => { assert(true); }), deepDefaults(spec.propertiesDefaultArray), tap((params) => { assert(true); }), ])(), dependencies: resolvedDependencies, spec, live, lives: provider.lives, programOptions, config, }), tap((result) => { // logger.debug( // `resolveConfig configDefault ${resourceName}: ${tos(result)}` // ); }), ]), ])(), ])(); const create = ({ payload, resolvedDependencies }) => pipe([ tap(() => { //logger.info(`create ${tos({ resourceName: getResourceName(), type })}`); //logger.debug(`create ${tos({ payload })}`); assert(payload); assert(resolvedDependencies); }), //TODO /*tap.if( () => getLive({ deep: false }), () => { throw Error(`Resource ${toString()} already exists`); } ),*/ tap((params) => { assert(true); }), () => getDependencies(), (dependencies) => getClient().create({ meta, name: getResourceName(), payload, namespace, dependencies, attributes, resolvedDependencies, lives: provider.lives, programOptions, }), () => retryCall({ name: `getLive ${toString()}`, fn: async () => getLive({ deep: true, resolvedDependencies }), config: { retryCount: 10, retryDelay: 5e3 }, isExpectedResult: not(isEmpty), }), tap((live) => { if (!live) { assert(false, `no live after create ${getResourceName()}`); } //logger.info(`created: ${toString()}`); //logger.debug(`created: live: ${tos(live)}`); }), ])(); const updateTags = ({ diff, live }) => pipe([ tap((params) => { assert(true); }), tap.if( () => diff.hasTagsDiff, pipe([ getClient, tap((client) => { assert( client.tagResource, `missing client.tagResource ${client.spec.groupType}` ); assert( client.untagResource, `missing client.untagResource ${client.spec.groupType}` ); }), (client) => pipe([ () => live, client.findId({ lives: provider.lives, config: provider.getConfig(), }), (id) => pipe([ //Tag () => diff, get("tags.targetTags"), client.tagResource({ live: diff.liveIn, id }), //Untag () => diff, get("tags.removedKeys"), unless( isEmpty, pipe([client.untagResource({ live: diff.liveIn, id })]) ), ])(), ])(), ]) ), ]); const update = ({ payload, diff, live, resolvedDependencies }) => pipe([ tap((params) => { assert(true); }), () => getLive({ resolvedDependencies }), tap.if(isEmpty, () => { throw Error(`Resource ${toString()} does not exist`); }), tap.if( () => diff.hasDataDiff, pipe([ getClient, tap((client) => { assert( client.update, `client ${client.spec.groupType} has no update function` ); }), (client) => retryCall({ name: `update ${toString()}`, fn: tryCatch( pipe([ () => ({ name: getResourceName(), payload, dependencies: getDependencies(), resolvedDependencies, diff, live, lives: provider.lives, config: provider.getConfig(), //TODO do we need that id ? id: client.findId({ lives: provider.lives, config: provider.getConfig(), })(live), programOptions, compare: spec.compare, }), client.update, tap((params) => { assert(true); }), ]), (error) => { logger.error(`error updating: ${toString()}`); throw error; } ), isExpectedResult: () => true, shouldRetryOnException: client.shouldRetryOnException, config: provider.config, }), tap((params) => { logger.info(`updated: ${toString()}`); }), ]) ), updateTags({ diff, live }), ])(); const planUpsert = ({ resource, lives, targetResources }) => pipe([ tap((params) => { //logger.info(`planUpsert resource: ${resource.toString()}`); assert(lives); assert(targetResources); }), () => resource, switchCase([ or([get("readOnly"), get("isDefault")]), pipe([ () => resource.resolveConfig({ deep: true }), tap((params) => { assert(true); }), switchCase([ isEmpty, () => [ { action: "WAIT_CREATION", resource: resource.toJSON(), providerName: resource.toJSON().providerName, }, ], () => [], ]), tap((params) => { assert(true); }), ]), // readOnly false pipe([ () => ({}), assign({ live: () => resource.findLive({}), }), assign({ target: pipe([ ({ live }) => resource.resolveConfig({ live, deep: true }), ]), }), tap((params) => { assert(true); }), switchCase([ pipe([get("live"), isEmpty]), ({ target, live }) => [ { action: "CREATE", resource: resource.toJSON(), target: resource.spec.displayResource()(target), live: resource.spec.displayResource()(live), providerName: resource.toJSON().providerName, }, ], ({ live, target }) => planUpdate({ live, target, resource, targetResources }), ]), ]), ]), ])(); const waitForResourceUp = ({ lives }) => pipe([ tap(() => { assert(filterLives); assert(lives); }), getClient, (client) => retryCall({ name: `waitForResourceUp ${toString()}`, fn: tryCatch( pipe([ fork({ resources: pipe([ () => ({ lives }), client.getLives, get("resources"), tap((resources) => { logger.debug( `waitForResourceUp: ${toString()}, #resources: ${size( resources )}` ); }), ]), resolveDependencies: () => resolveDependencies({ resourceName: getResourceName(), dependencies: getDependencies(), }), }), ({ resources, resolveDependencies }) => filterLives({ name: getResourceName(), resources, lives, dependencies: resolveDependencies, }), tap((params) => { logger.debug( `waitForResourceUp: ${toString()}, filterLives: ${tos( params )}` ); }), ]), (error) => { logger.error( `error waitForResourceUp: ${toString()}, error: ${tos(error)}` ); throw error; } ), isExpectedResult: not(isEmpty), config: client.retryConfigs.isUp, }), tap((params) => { logger.info(`waitForResourceUp: ${toString()}, done`); }), ])(); const toString = () => spec.resourceKey({ providerName: provider.name, type, group, name: getResourceName(), meta, dependencies: getDependencies(), properties, }); const toJSON = pipe([ () => ({ providerName: provider.name, groupType, type, group, namespace: getClient().findNamespaceFromTarget({ namespace, properties }), name: getResourceName(), meta, readOnly, displayName: getClient().displayNameResource({ name: getResourceName(), meta, properties, dependencies: getDependencies(), }), uri: toString(), }), tap((json) => { assert(json); }), ]); return { type, group, groupType: `${group}::${type}`, provider, get name() { return getResourceName(); }, namespace, meta, readOnly, isDefault, dependencies: getDependencies(), spec, getClient, toJSON, toString, attributes, properties, resolveConfig, create, update, planUpsert, waitForResourceUp, filterLives, getLive, findLive, getDependencyList, resolveDependencies: ({ dependenciesMustBeUp }) => resolveDependencies({ resourceName, dependencies: getDependencies(), dependenciesMustBeUp, }), isUp: ({ live }) => pipe([ tap(() => { assert(getClient().isInstanceUp); }), () => getClient().isInstanceUp(live), tap((isUp) => { logger.debug( `isUp ${type}/${getResourceName()}: ${!!isUp}, hasLive: ${!!live}` ); }), ])(), }; };