@grucloud/core
Version:
GruCloud core, generate infrastructure code
1,835 lines (1,751 loc) • 48 kB
JavaScript
const assert = require("assert");
const {
pipe,
tap,
map,
flatMap,
filter,
tryCatch,
switchCase,
set,
get,
assign,
any,
or,
reduce,
eq,
not,
pick,
fork,
and,
} = require("rubico");
const {
isString,
isEmpty,
when,
callProp,
flatten,
pluck,
forEach,
find,
defaultsDeep,
isFunction,
identity,
size,
append,
unless,
groupBy,
prepend,
values,
filterOut,
uniq,
} = require("rubico/x");
const memoize = require("lodash.memoize");
const { EOL } = require("os");
const logger = require("./logger")({ prefix: "CoreProvider" });
const { tos } = require("./tos");
const { createSpec } = require("./SpecDefault");
const { retryCall } = require("./Retry");
const {
mapPoolSize,
convertError,
HookType,
TitleListing,
TitleQuery,
TitleDeploying,
TitleDestroying,
typeFromResources,
groupFromResources,
planToResourcesPerType,
configProviderDefault,
} = require("./Common");
const { Lister } = require("./Lister");
const { Planner, mapToGraph } = require("./Planner");
const {
nextStateOnError,
hasResultError,
PlanDirection,
isTypeMatch,
isValidPlan,
filterReadClient,
filterReadWriteClient,
contextFromClient,
contextFromProvider,
contextFromProviderInit,
contextFromResourceType,
contextFromPlanner,
contextFromResource,
contextFromHook,
contextFromHookAction,
providerRunning,
createGetResource,
} = require("./ProviderCommon");
const { ResourceMaker } = require("./CoreResource");
const { createClient, buildGroupType } = require("./Client");
const { createLives } = require("./Lives");
const groupName = switchCase([isEmpty, () => "", pipe([append(".")])]);
const createResourceMakers = ({
specs,
provider,
filterResource = identity,
prefix,
programOptions,
}) =>
pipe([
tap((params) => {
assert(true);
}),
() => specs,
filter(filterResource),
reduce(
(acc, spec) =>
set(
`${groupName(spec.group)}${prefix}${spec.type}`,
pipe([
() =>
spec[`${prefix}Resource`]({
provider,
spec,
programOptions,
}),
])
)(acc),
{}
),
tap((params) => {
assert(true);
}),
])();
const buildProviderConfig = ({ config = {}, providerName, programOptions }) =>
pipe([
() => config,
defaultsDeep({ providerName }),
defaultsDeep(programOptions),
defaultsDeep(configProviderDefault),
])();
exports.buildProviderConfig = buildProviderConfig;
const getListHofDefault = ({ getList, spec }) =>
tryCatch(
pipe([
tap(() => {
logger.info(`getList ${spec.groupType}`);
assert(getList);
}),
getList,
tap((items) => {
if (!Array.isArray(items)) {
assert(Array.isArray(items), JSON.stringify(spec));
}
}),
(items) => ({ items, total: size(items) }),
tap(({ total }) => {
logger.info(`getList ${spec.groupType} ${total}`);
}),
]),
(error, params) =>
pipe([
tap(() => {
logger.error(
`getList ${spec.groupType}, error: ${JSON.stringify({
params,
error,
})}`
);
}),
() => {
throw error;
},
])()
);
function CoreProvider({
name: providerName,
displayName = () => providerName,
directory = "",
dependencies = {},
type,
programOptions = {},
fnSpecs,
makeConfig,
info = () => ({}),
init = () => {},
unInit = () => {},
start = () => {},
generateCode = () => {},
getListHof = getListHofDefault,
filterClient = () => true,
createResources,
mapGloblalNameToResource = new Map(),
}) {
assert(makeConfig);
let context = {};
const getContext = () => context;
let _lives;
const setLives = (livesToSet) => {
_lives = livesToSet;
};
const getProgramOptions = () =>
pipe([
() => programOptions,
defaultsDeep({ workingDirectory: process.cwd() }),
])();
const getLives = pipe([
() => _lives,
when(isEmpty, pipe([createLives, tap(setLives)])),
]);
const getProviderConfig = () =>
buildProviderConfig({ config: makeConfig(), providerName, programOptions });
//logger.debug(`CoreProvider name: ${providerName}, type ${type}`);
const hookMap = new Map();
const hookAdd = ({ name, hookInstance }) =>
pipe([
tap(() => {
assert(name);
assert(hookInstance);
logger.info(`hookAdd ${name}`);
}),
() => hookInstance,
defaultsDeep({
name,
onDeployed: {
init: () => {},
cleanUp: () => {},
actions: [],
},
onDestroyed: {
init: () => {},
cleanUp: () => {},
actions: [],
},
}),
tap.if(
() => hookMap.has(name),
() => {
logger.info(`hook ${name} already added`);
}
),
(newHook) => hookMap.set(name, newHook),
])();
// Target Resources
const mapTypeToResources = new Map();
let resourcesObj = {};
const resourcesSet = new Set();
const mapNameToResource = new Map();
const getMapNameToResource = () => mapNameToResource;
const getResourceFromLive = ({ uri }) =>
pipe([
tap(() => {
assert(uri);
}),
() => mapNameToResource.get(uri),
tap((resource) => {
// logger.debug(
// `getResourceFromLive uri: ${uri}, hasResource: ${!!resource}`
// );
}),
])();
const getTargetGroupTypes = pipe([() => [...mapTypeToResources.keys()]]);
const targetResourcesAdd = (resource) =>
pipe([
tap(() => {
resourcesSet.add(resource);
}),
])();
const targetResourceAddToMap = (resource) => {
const { type, group, name, spec } = resource;
assert(name);
assert(isString(name));
assert(type);
assert(spec.groupType);
assert(spec.providerName);
const resourceKey = resource.toString();
logger.debug(`targetResourceAddToMap ${resourceKey}`);
if (mapNameToResource.has(resourceKey)) {
if (spec.listOnly) {
return;
}
// logger.debug(`resource '${resourceKey}' already exists`);
}
resourcesObj = set(
filter(not(isEmpty))([group, type, name]),
resource
)(resourcesObj);
mapNameToResource.set(resourceKey, resource);
mapGloblalNameToResource.set(
`${spec.providerName}::${group}::${type}::${name}`,
resource
);
const resourcesByType = getResourcesByType(resource);
assert(resourcesByType);
mapTypeToResources.set(resource.groupType, [
...filter(not(eq(get("name"), name)))(resourcesByType),
resource,
]);
tap.if(get("hook"), (client) =>
hookAdd({
name: client.spec.type,
hookInstance: client.hook({ resource }),
})
)(resource.client);
};
const targetResourceAdd = (resource) =>
pipe([
tap((params) => {
assert(true);
}),
() => targetResourceAddToMap(resource),
])();
const findSpecByGroupType = ({ type, group }) =>
pipe([
tap((params) => {
assert(true);
}),
find(and([eq(get("type"), type), eq(get("group"), group)])),
tap((spec) => {
assert(spec, `cannot find type:${type}, group ${group}`);
}),
]);
const targetResourcesBuildMap = (resourcesSpec = []) =>
pipe([
tap(() => {
assert(resourcesSpec);
}),
() => resourcesSpec,
map(({ type, group, ...other }) =>
pipe([
tap(() => {
assert(type);
assert(other);
}),
getSpecs,
findSpecByGroupType({ type, group }),
tap((params) => {
assert(true);
}),
(spec) => ({
...other,
provider,
programOptions: getProgramOptions(),
spec,
}),
ResourceMaker,
tap((params) => {
assert(true);
}),
targetResourcesAdd,
])()
),
//TODO remove below
() => resourcesSet,
tap((params) => {
assert(true);
}),
forEach(tryCatch(targetResourceAdd, () => undefined)),
tap((params) => {
assert(true);
}),
//TODO use dependencies
() => resourcesSet,
forEach(targetResourceAdd),
])();
const getTargetResources = () => [...mapNameToResource.values()];
const getResource = createGetResource({ mapGloblalNameToResource });
const getSpecs = memoize(
pipe([
getProviderConfig,
fnSpecs,
map(createSpec({ config: getProviderConfig() })),
tap((params) => {
assert(true);
}),
]),
() => "k"
);
const getResourcesByType = ({ type, group }) =>
pipe([
tap(() => {
assert(type);
//assert(group);
}),
() => mapTypeToResources.get(`${group}::${type}`) || [],
tap((params) => {
assert(true);
}),
])();
const createClientFromSpec = (spec) =>
createClient({
getTargetResources,
getResourcesByType,
getResourceFromLive,
getResource,
spec,
config: getProviderConfig(),
providerName,
getListHof,
lives: getLives(),
getContext,
});
const getClients = pipe([
getSpecs,
map(createClientFromSpec),
filter(filterClient),
tap((clients) => {
logger.debug(`#client ${size(clients)}`);
}),
]);
const getClient = ({ groupType }) =>
pipe([
tap(() => {
assert(groupType);
}),
getSpecs,
tap((params) => {
assert(true);
}),
find(eq(get("groupType"), groupType)),
tap((spec) => {
assert(spec, `no ${groupType}`);
}),
createClientFromSpec,
tap((params) => {
assert(true);
}),
])();
const listTargets = () =>
pipe([
getTargetResources,
map(async (resource) => ({
...resource.toJSON(),
data: await resource.getLive(),
})),
filter(get("data")),
tap((list) => {
logger.debug(`listTargets ${tos(list)}`);
}),
])();
const listConfig = () =>
pipe([
getTargetResources,
map((resource) => ({
resource: resource.toJSON(),
})),
tap((list) => {
logger.debug(`listConfig ${tos(list)}`);
}),
])();
const runScriptCommands = ({ onStateChange, hookType, hookName }) =>
pipe([
tap((x) => {
assert(hookType, "hookType");
assert(hookName, "hookName");
logger.info(
`runScriptCommands hookName: ${hookName}, type: ${hookType}`
);
}),
tap((x) =>
onStateChange({
context: contextFromHook({ providerName, hookName, hookType }),
nextState: "RUNNING",
})
),
tryCatch(
pipe([
assign({
payload: pipe([callProp("init")]),
}),
({ actions, payload, cleanUp }) =>
pipe([
() => actions,
map.pool(
mapPoolSize,
tryCatch(
pipe([
tap((action) => {
onStateChange({
context: contextFromHookAction({
providerName,
hookType,
hookName,
name: action.name,
}),
nextState: "RUNNING",
});
}),
tap((action) => {
assert(action.command);
}),
tap(callProp("command", payload)),
tap((action) => {
onStateChange({
context: contextFromHookAction({
providerName,
hookType,
hookName,
name: action.name,
}),
nextState: "DONE",
});
}),
]),
(error, action) => {
logger.error(
`runScriptCommands ${hookType}, error for ${action.name}`
);
logger.error(tos(error));
error.stack && logger.error(error.stack);
onStateChange({
context: contextFromHookAction({
providerName,
hookType,
hookName,
name: action.name,
}),
nextState: "ERROR",
error: convertError({ error }),
});
return {
error,
action: action.name,
hookType,
hookName,
providerName,
};
}
)
),
tap((params) => {
assert(true);
}),
tap(() => cleanUp(payload)),
])(),
tap((xx) => {
logger.debug(
`runScriptCommands ${tos({ hookName, hookType })} ${tos(xx)}`
);
}),
(results) =>
assign({
error: ({ results }) => any(({ error }) => error)(results),
})({
results,
}),
tap((xx) => {
logger.debug(
`runScriptCommands ${tos({ hookName, hookType })} ${tos(xx)}`
);
}),
tap(({ error }) =>
onStateChange({
context: contextFromHook({ providerName, hookName, hookType }),
nextState: nextStateOnError(error),
})
),
tap((xx) => {
logger.info(`runScriptCommands DONE ${tos(xx)}`);
}),
]),
(error, script) => {
logger.debug(
`runScriptCommands error: ${hookName}, type: ${hookType}, script ${tos(
script
)}`
);
onStateChange({
context: contextFromHook({ providerName, hookName, hookType }),
nextState: "ERROR",
error: error,
});
return {
results: [{ error, hookType, hookName, providerName, script }],
error: true,
};
}
),
]);
const runHook = ({ onStateChange, hookType, onHook, skip }) =>
switchCase([
() => !skip,
() =>
pipe([
() => [...hookMap.values()],
tap((hooks) => {
// logger.debug(
// `runHook hookType: ${hookType}, #hooks ${hooks.length}`
// );
assert(onStateChange);
assert(hookType);
assert(onHook);
}),
map((hook) =>
pipe([
tap(() => {
assert(hook.name, "name");
assert(hook[onHook], `hook[onHook]: ${onHook}`);
logger.debug(`runHook start ${hook.name}`);
}),
() => hook[onHook],
runScriptCommands({
onStateChange,
hookType,
hookName: hook.name,
}),
tap((obj) => {
logger.info(`runHook ${hookType} stop`);
}),
])()
),
fork({ error: any(get("error")), results: identity }),
tap((result) => {
assert(true);
}),
])(),
() => {
//logger.debug(`runHook skipped`);
},
])();
const runOnDeployed = ({ onStateChange, skip }) =>
runHook({
onStateChange,
skip,
onHook: "onDeployed",
hookType: HookType.ON_DEPLOYED,
});
const runOnDestroyed = ({ onStateChange, skip }) =>
runHook({
onStateChange,
skip,
onHook: "onDestroyed",
hookType: HookType.ON_DESTROYED,
});
const setHookWaitingState = ({ onStateChange, hookType, hookName }) =>
pipe([
tap(({ actions }) => {
// logger.debug(
// `setHookWaitingState ${hookName}::${hookType}, #actions ${actions.length}`
// );
assert(hookName, "hookName");
assert(hookType, "hookType");
}),
tap(() => {
onStateChange({
context: contextFromHook({ providerName, hookType, hookName }),
nextState: "WAITING",
indent: 2,
});
}),
({ actions }) =>
map((action) =>
onStateChange({
context: contextFromHookAction({
providerName,
hookType,
hookName,
name: action.name,
}),
nextState: "WAITING",
indent: 4,
})
)(actions),
]);
const spinnersStartProvider = ({ onStateChange }) =>
tap(
pipe([
() =>
onStateChange({
context: contextFromProvider({ providerName: displayName() }),
nextState: "WAITING",
}),
() =>
onStateChange({
context: contextFromProviderInit({ providerName }),
nextState: "WAITING",
indent: 2,
}),
])
);
const spinnersStopProvider = ({ onStateChange, error }) =>
tap(() =>
onStateChange({
context: contextFromProvider({ providerName: displayName() }),
nextState: nextStateOnError(error),
error,
})
)();
const spinnersStartResources = ({ onStateChange, title, resourcesPerType }) =>
tap(
pipe([
tap(() => {
assert(title, "title");
assert(resourcesPerType, "resourcesPerType");
assert(
Array.isArray(resourcesPerType),
"resourcesPerType must be an array"
);
// logger.debug(
// `spinnersStartResources ${title}, #resourcesPerType ${size(
// resourcesPerType
// )}`
// );
}),
() =>
onStateChange({
context: contextFromPlanner({
providerName,
title,
total: resourcesPerType.length,
}),
nextState: "WAITING",
indent: 2,
}),
() =>
pipe([
map((resourcesPerType) =>
onStateChange({
context: contextFromResourceType({
operation: title,
resourcesPerType,
}),
nextState: "WAITING",
indent: 4,
})
),
])(resourcesPerType),
])
);
const spinnersStartClient = ({ onStateChange, title, specs }) =>
tap(
pipe([
tap(() => {
assert(title, "title");
assert(Array.isArray(specs), "specs must be an array");
//logger.debug(`spinnersStartClient ${title}, #specs ${size(specs)}`);
}),
tap(() =>
onStateChange({
context: contextFromPlanner({
providerName,
title,
total: size(specs),
}),
nextState: "WAITING",
indent: 2,
})
),
() => specs,
map((spec) =>
onStateChange({
context: contextFromClient({ spec, title }),
nextState: "WAITING",
indent: 4,
})
),
])
);
const spinnersStopClient = ({ onStateChange, title, error }) =>
tap(
pipe([
tap(() => {
assert(title, "title");
//logger.debug(`spinnersStopClient ${title}, error: ${error}`);
}),
tap(() =>
onStateChange({
context: contextFromPlanner({ providerName, title }),
nextState: nextStateOnError(error),
error,
})
),
])
);
const spinnersStartHooks = ({ onStateChange, hookType }) =>
pipe([
() => [...hookMap.values()],
tap((hooks) => {
//logger.info(`spinnersStart #hooks ${hooks.length}`);
}),
tap(
map(({ name, onDeployed, onDestroyed }) =>
pipe([
tap(() => {
logger.debug(`spinnersStart hook`);
}),
switchCase([
() => hookType === HookType.ON_DEPLOYED,
() => onDeployed,
() => hookType === HookType.ON_DESTROYED,
() => onDestroyed,
() => assert(`Invalid hook type '${hookType}'`),
]),
setHookWaitingState({ onStateChange, hookType, hookName: name }),
])()
)
),
]);
const spinnersStartQuery = ({ onStateChange, options }) =>
pipe([
() => [...mapTypeToResources.values()],
tap((resourcesPerType) => {
// logger.debug(
// `spinnersStartQuery #resourcesPerType ${size(resourcesPerType)}`
// );
}),
map(filter(not(get("spec.listOnly")))),
filter(not(isEmpty)),
tap((resourcesPerType) => {
// logger.debug(
// `spinnersStartQuery #resourcesPerType no listOnly ${size(
// resourcesPerType
// )}`
// );
}),
map((resources) => ({
provider: providerName,
type: typeFromResources(resources),
group: groupFromResources(resources),
resources: map(callProp("toJSON"))(resources),
})),
map(assign({ groupType: buildGroupType })),
callProp("sort", (a, b) => a.groupType.localeCompare(b.groupType)),
tap((xxx) => {
assert(true);
}),
spinnersStartProvider({ onStateChange }),
spinnersStartClient({
onStateChange,
title: TitleListing,
specs: filterReadClient({
options,
targetTypes: getTargetGroupTypes(),
})(getSpecs()),
}),
])();
const spinnersStartListLives = ({ onStateChange, options }) =>
tap(
pipe([
// tap(() => {
// logger.debug(`spinnersStartListLives ${options.types}`);
// }),
spinnersStartProvider({ onStateChange }),
spinnersStartClient({
onStateChange,
planQueryDestroy,
title: TitleListing,
specs: filterReadClient({
options,
targetTypes: getTargetGroupTypes(),
})(getSpecs()),
}),
])
)();
const spinnersStopListLives = ({ onStateChange, options, error }) =>
tap(
pipe([
tap(() => {
//logger.debug(`spinnersStopListLives ${error}`);
}),
spinnersStopClient({
onStateChange,
title: TitleListing,
error,
}),
//TODO
//() => spinnersStopProvider({ onStateChange, error }),
])
)();
const spinnersStartDeploy = ({ onStateChange, plan }) =>
tap(
pipe([
tap(() => {
//logger.debug("spinnersStartDeploy");
assert(plan);
}),
spinnersStartProvider({ onStateChange }),
tap(
switchCase([
() => isValidPlan(plan.resultCreate),
pipe([
tap(() => {
assert(Array.isArray(plan.resultCreate));
}),
spinnersStartResources({
onStateChange,
title: TitleDeploying,
resourcesPerType: planToResourcesPerType({
providerName,
plans: plan.resultCreate,
}),
}),
]),
() => {
logger.debug("no newOrUpdate ");
},
])
),
tap(
switchCase([
() => isValidPlan(plan.resultDestroy),
pipe([
spinnersStartResources({
onStateChange,
title: TitleDestroying,
resourcesPerType: planToResourcesPerType({
providerName,
plans: plan.resultDestroy,
}),
}),
]),
() => {
logger.debug("no destroy ");
},
])
),
spinnersStartHooks({
onStateChange,
hookType: HookType.ON_DEPLOYED,
}),
])
)();
const spinnersStartDestroyQuery = ({ onStateChange, options }) =>
pipe([
tap(() => {
//logger.debug(`spinnersStartDestroyQuery`);
}),
spinnersStartProvider({ onStateChange }),
spinnersStartClient({
onStateChange,
title: TitleListing,
specs: filterReadWriteClient({
options,
targetTypes: getTargetGroupTypes(),
})(getSpecs()),
}),
])();
const spinnersStartDestroy = ({ onStateChange, plans }) =>
pipe([
tap(() => {
assert(plans, "plans");
assert(Array.isArray(plans), "plans !isArray");
// const resources = pipe([filter(({ error }) => !error), pluck("resource")])(
// plans
// );
//logger.debug(`spinnersStartDestroy #resources: ${resources.length}`);
}),
spinnersStartProvider({ onStateChange }),
spinnersStartResources({
onStateChange,
title: TitleDestroying,
resourcesPerType: planToResourcesPerType({
providerName,
plans,
}),
}),
spinnersStartHooks({
onStateChange,
hookType: HookType.ON_DESTROYED,
}),
])();
const spinnersStartHook = ({ onStateChange, hookType }) =>
pipe([
tap(() => {
//logger.debug(`spinnersStartHook hookType: ${hookType}`);
assert(hookType);
}),
spinnersStartProvider({ onStateChange }),
spinnersStartHooks({
onStateChange,
hookType,
}),
])();
const toType = () => type || providerName;
const register = ({ resources, hooks = [] }) =>
pipe([
() => hooks,
tap(() => {
//logger.debug(`register #hooks ${size(hooks)}`);
}),
map((hook) =>
pipe([
tap(() => {
assert(isFunction(hook), "hook must be a function.");
}),
() =>
hook({
resources,
config: getProviderConfig(),
provider,
}),
tap((hookInstance) => {
assert(
!isFunction(hookInstance),
"hook instance must be not a function."
);
}),
(hookInstance) =>
pipe([
() => hookInstance,
get("name", "default"),
tap((name) => {
//logger.debug(`register hook ${name}`);
}),
(name) => hookAdd({ name, hookInstance }),
])(),
tap(() => {
//logger.debug(`register done`);
}),
])()
),
tap(() => {
//logger.debug(`register done`);
}),
])();
const validate = pipe([
() => [...mapTypeToResources.values()],
flatten,
tap((params) => {
assert(true);
}),
map(
tryCatch(
({ name, getClient }) =>
tap.if(
() => getClient().validate,
() => getClient().validate({ name })
)(),
(error, resource) =>
pipe([
tap(() => {
logger.error(
`validate error for resource ${resource.toString()} ${tos(
error
)}`
);
}),
() => ({ error, resource: resource.toString() }),
])()
)
),
filter(get("error")),
tap.if(not(isEmpty), (errors) => {
logger.error(`validate errors ${tos(errors)}`);
throw errors;
}),
]);
const startBase = ({ onStateChange = identity } = {}) =>
tryCatch(
pipe([
tap(() => {
logger.debug(`start ${providerName}`);
}),
tap(() =>
onStateChange({
context: contextFromProviderInit({ providerName }),
nextState: "RUNNING",
})
),
start,
tap((params) => {
assert(true);
}),
tap(
pipe([
validate,
tap((result) =>
onStateChange({
context: contextFromProviderInit({ providerName }),
nextState: "DONE",
result,
})
),
() => createResources,
unless(
isEmpty,
pipe([
flatMap((cr) =>
pipe([
tap(() => {
assert(
isFunction(cr),
"createResources should be a function"
);
}),
() => cr({ provider }),
])()
),
filter(not(isEmpty)),
targetResourcesBuildMap,
])
),
getSpecs,
forEach(
when(
get("setup"),
pipe([
callProp("setup", { config: getProviderConfig() }),
(result = {}) => {
context = {
...context,
...result,
};
},
])
)
),
])
),
]),
(error) => {
logger.error(`start error ${tos(convertError({ error }))}`);
onStateChange({
context: contextFromProviderInit({ providerName }),
nextState: "ERROR",
error: convertError({ error }),
});
onStateChange({
context: contextFromProvider({ providerName: displayName() }),
nextState: "ERROR",
});
throw error;
}
)();
const listLives = ({
onStateChange = identity,
options = {},
title = TitleListing,
readWrite = false,
} = {}) =>
pipe([
tap(() => {}),
getSpecs,
tap((clients) => {
// logger.debug(
// `listLives #clients: ${size(clients)}, ${JSON.stringify({
// providerName,
// title,
// readWrite,
// options,
// })}`
// );
}),
switchCase([
() => readWrite,
filterReadWriteClient({
options,
targetTypes: getTargetGroupTypes(),
}),
filterReadClient({
options,
targetTypes: getTargetGroupTypes(),
}),
]),
tap((clients) => {
//logger.debug(`listLives #clients ${size(clients)}`);
}),
map((spec) => ({
meta: pick(["type", "group", "groupType", "providerName"])(spec),
key: `${spec.providerName}::${spec.groupType}`,
dependsOn: pipe([
() => spec,
get("dependsOnList", []),
map((dependOn) => `${spec.providerName}::${dependOn}`),
])(),
executor: ({ results }) =>
pipe([
() => spec,
getClient,
(client) =>
client.getLives({
lives: getLives(),
deep: true,
options,
resources: getResourcesByType(spec),
}),
//TODO add client.toString()
({ error, resources }) => ({
...(error && { error }),
resources,
groupType: spec.groupType,
providerName: spec.providerName,
}),
tap.if(get("error"), ({ error, resources }) => {
//assert(error);
}),
tap((resourcesPerType) => {
assert(resourcesPerType);
//getLives().addResources(resourcesPerType);
}),
])(),
})),
tap((params) => {
assert(true);
}),
Lister({
poolSize: 10,
onStateChange: ({ key, meta, result, error, ...other }) => {
assert(key);
const spec = meta;
assert(spec.type);
assert(spec.groupType);
assert(spec.providerName);
onStateChange({
context: contextFromClient({
spec,
title,
}),
error,
...other,
});
},
}),
tap((result) => {
assert(result);
}),
assign({
results: pipe([
get("results"),
filterOut(
and([not(get("error")), pipe([get("resources"), isEmpty])])
),
callProp("sort", (a, b) => a.groupType.localeCompare(b.groupType)),
map(
when(
get("resources"),
assign({
resources: pipe([
get("resources"),
callProp("sort", (a, b) => a.name.localeCompare(b.name)),
]),
})
)
),
]),
}),
tap((params) => {
assert(true);
}),
assign({ providerName: () => providerName }),
tap(({ results }) => {
// logger.debug(
// `listLives provider ${providerName}, ${size(
// results
// )} results: ${pluck("groupType")(results).join(", ")}`
// );
}),
])();
const filterDestroyResources =
({
client,
options: {
all = false,
name: nameToDelete = "",
id: idsToDelete = [],
types = [],
group = [],
} = {},
direction,
}) =>
(resource) => {
const { type } = client.spec;
const { id } = resource;
assert(direction);
// logger.debug(
// `filterDestroyResources ${JSON.stringify({
// all,
// types,
// id,
// })}`
// );
return switchCase([
// Resource that cannot be deleted
() => resource.cannotBeDeleted,
() => {
// logger.debug(
// `filterDestroyResources ${type}/${id}, default resource cannot be deleted`
// );
return false;
},
// Delete all resources
and([() => all, () => isEmpty(types)]),
() => {
//logger.debug(`filterDestroyResources ${type}/${id}, delete all`);
return true;
},
// ManagedByOther, if types is specified, delete the resource regardless of managedByOther
() => isEmpty(types) && resource.managedByOther,
() => false,
// Delete by id
() => !isEmpty(idsToDelete),
() => idsToDelete.includes(id),
// Delete by name
() => !isEmpty(nameToDelete),
() => resource.name === nameToDelete,
// Not our minion
() => isEmpty(types) && !resource.managedByUs,
() => {
// logger.debug(`filterDestroyResources ${type}::${id}, not our minion`);
return false;
},
// Delete by type
() => !isEmpty(types),
pipe([
() => types,
any((type) =>
isTypeMatch({
type,
typeToMatch: client.spec.type,
groupTypeToMatch: client.spec.groupType,
})
),
tap((params) => {
assert(true);
}),
]),
// PlanDirection
() => direction == PlanDirection.UP,
() => false,
() => true,
])();
};
const planFindDestroy = ({
livesData,
options = {},
direction = PlanDirection.DOWN,
}) =>
pipe([
tap(() => {
// logger.debug(
// `planFindDestroy ${JSON.stringify({ options, direction })}`
// );
assert(livesData);
}),
() => livesData,
get("results"),
find(eq(get("providerName"), providerName)),
get("results"),
filter(not(get("error"))),
flatMap(({ groupType, resources }) =>
pipe([
tap(() => {
//assert(group); k8s
assert(groupType);
assert(
Array.isArray(resources),
`no resources for type ${groupType}`
);
}),
() => getClient({ groupType }),
(client) =>
pipe([
() => resources,
filter(
filterDestroyResources({
client,
options,
direction,
})
),
map(
pipe([
(resource) => ({
// TODO omit ends up calling isDefault
resource,
action: "DESTROY",
live: client.spec.displayResource()(resource.live),
providerName: resource.providerName,
}),
])
),
])(),
])()
),
filter(not(isEmpty)),
tap((results) => {
//logger.debug(`planFindDestroy done`);
}),
])();
const planQueryDestroy = ({ livesData, options = {} }) =>
pipe([
tap(() => {
// logger.info(
// `planQueryDestroy ${JSON.stringify({ providerName, options })}`
// );
assert(livesData);
}),
() => ({ providerName }),
assign({
//TODO
plans: () => planFindDestroy({ livesData, options }),
}),
assign({ error: any(get("error")) }),
tap((result) => {
//logger.debug(`planQueryDestroy done`);
}),
])();
const planUpsert = ({ onStateChange = noop, lives }) =>
pipe([
tap(() => {
// logger.info(`planUpsert`);
}),
tap(() =>
onStateChange({
context: contextFromPlanner({
providerName: providerName,
title: TitleQuery,
}),
nextState: "RUNNING",
})
),
getTargetResources,
tap((resources) => {
//logger.debug(`planUpsert target #resources ${size(resources)}`);
}),
//filter(not(get("spec.listOnly"))),
tap(
map((resource) =>
onStateChange({
context: contextFromResource({
operation: TitleQuery,
resource: resource.toJSON(),
}),
nextState: "RUNNING",
})
)
),
map.pool(
mapPoolSize,
tryCatch(
(resource) =>
pipe([
tap(() => {
onStateChange({
context: contextFromResource({
operation: TitleQuery,
resource: resource.toJSON(),
}),
nextState: "RUNNING",
});
}),
() => ({
resource,
targetResources: getTargetResources(),
lives: getLives(),
}),
resource.planUpsert,
tap((params) => {
onStateChange({
context: contextFromResource({
operation: TitleQuery,
resource: resource.toJSON(),
}),
nextState: "DONE",
});
}),
])(),
(error, resource) => {
logger.error(`error query resource ${resource.toString()}`);
//logger.error(util.inspect(error, { depth: 8 }));
error.stack && logger.error(error.stack);
onStateChange({
context: contextFromResource({
operation: TitleQuery,
resource: resource.toJSON(),
}),
nextState: "ERROR",
error,
});
return [{ error, resource: resource.toJSON() }];
}
)
),
filter(not(isEmpty)),
flatten,
tap((plans) =>
onStateChange({
context: contextFromPlanner({ providerName, title: TitleQuery }),
nextState: nextStateOnError(hasResultError(plans)),
})
),
callProp("sort", (a, b) =>
a.resource.groupType.localeCompare(b.resource.groupType)
),
tap((result) => {
//logger.info(`planUpsert done`);
}),
])();
const planQuery = ({
livesData,
onStateChange = identity,
commandOptions = {},
} = {}) =>
pipe([
tap(() => {
// logger.info(`planQuery begins ${providerName}`);
assert(livesData);
}),
providerRunning({ onStateChange, providerName }),
() => ({ providerName }),
assign({
resultDestroy: () =>
planFindDestroy({
livesData,
direction: PlanDirection.UP,
options: commandOptions,
}),
}),
assign({
resultCreate: () =>
planUpsert({
onStateChange,
}),
}),
assign({
error: or([
pipe([get("resultCreate"), any(get("error"))]),
pipe([get("resultDestroy"), any(get("error"))]),
]),
}),
tap(({ error }) =>
onStateChange({
context: contextFromProvider({ providerName: displayName() }),
nextState: nextStateOnError(error),
})
),
tap((result) => {
// logger.debug(`planQuery done ${providerName}`);
}),
])({});
const onStateChangeResource = ({ operation, onStateChange }) => {
return ({ resource, error, ...other }) => {
// logger.debug(
// `onStateChangeResource resource:${tos(resource)}, ${tos(other)}`
// );
assert(resource, "no resource");
assert(resource.type, "no resource.type");
onStateChange({
context: contextFromResource({ operation, resource }),
error,
...other,
});
};
};
const destroyById = ({ resource, live }) =>
pipe([
tap(() => {
assert(live);
assert(resource);
//logger.debug(`destroyById: ${tos(resource.toString())}`);
}),
() => getClient(resource),
tap((client) => {
assert(client, `Cannot find endpoint ${resource.toString()}}`);
assert(client.spec);
}),
tap((client) =>
retryCall({
name: `destroy ${client.spec.groupType}/${resource.name}`,
fn: () =>
client.destroy({
live,
id: client.findId({
lives: getLives(),
config: getProviderConfig(),
})(live), // TODO remove id, only use live
name: resource.name,
resource,
lives: getLives(),
config: getProviderConfig(),
}),
isExpectedResult: () => true,
shouldRetryOnException: client.shouldRetryOnExceptionDelete,
config: getProviderConfig(),
})
),
])();
const planDestroy = ({
plans,
planAll,
onStateChange = identity,
direction = PlanDirection.DOWN,
}) =>
pipe([
tap(() => {
assert(Array.isArray(plans), "plans must be an array");
assert(planAll);
logger.info(`planDestroy #plans ${plans.length}`);
//logger.debug(`planDestroy ${tos({ plans, direction })}`);
}),
providerRunning({ onStateChange, providerName }),
tap(() =>
onStateChange({
context: contextFromPlanner({
providerName,
title: TitleDestroying,
}),
nextState: "RUNNING",
})
),
() => planAll,
get("results"),
pluck("plans"),
flatten,
(planDestroyAll) => ({
plans,
planDestroyAll,
dependsOnType: getSpecs(),
dependsOnInstance: mapToGraph(getMapNameToResource()),
executor: ({ item }) =>
destroyById({
resource: item.resource,
live: item.live,
}),
down: true,
onStateChange: onStateChangeResource({
operation: TitleDestroying,
onStateChange,
}),
}),
Planner,
callProp("run"),
tap(({ error }) =>
onStateChange({
context: contextFromPlanner({ providerName, title: TitleDestroying }),
nextState: nextStateOnError(error),
})
),
])();
const docPrefix = ({ providerName, group, type }) =>
pipe([
() => `./`,
when(() => group, append(`${group}/`)),
append(`${type}.md`),
])();
const servicesList = ({}) =>
pipe([getSpecs, pluck("group"), uniq, callProp("join", EOL)])();
const resourcesList = () =>
pipe([
tap(() => {
logger.debug(`resourcesList`);
}),
getSpecs,
fork({
resourcesCount: size,
servicesCount: pipe([groupBy("group"), values, size]),
content: pipe([
groupBy("group"),
map.entries(([key, values]) => [
key,
pipe([
() => values,
map((spec) => `[${spec.type}](${docPrefix(spec)})`),
switchCase([
() => isEmpty(key),
pipe([map(prepend("* ")), callProp("join", "\n")]),
pipe([callProp("join", ", "), prepend(`* ${key}: \n`)]),
]),
])(),
]),
values,
callProp("join", "\n"),
]),
}),
({ resourcesCount, servicesCount, content }) => `---
id: ResourcesList
title: Resources List
---
List of ${resourcesCount} resources in ${servicesCount} services for provider ${providerName}:\n
${content}`,
])();
const toString = () => ({ name: providerName, type: toType() });
const provider = {
toString,
directory,
get config() {
return getProviderConfig();
},
getConfig: getProviderConfig,
getContext,
setLives,
get lives() {
return getLives();
},
resources: () => resourcesObj,
name: providerName,
displayName,
dependencies,
type: toType,
servicesList,
resourcesList,
getResourceFromLive,
spinnersStopProvider,
spinnersStartHook,
spinnersStartQuery,
spinnersStartDeploy,
spinnersStartListLives,
spinnersStopListLives,
spinnersStartDestroyQuery,
spinnersStartDestroy,
planQueryDestroy,
planDestroy,
planFindDestroy,
listLives,
planQuery,
listTargets,
listConfig,
targetResourcesAdd,
getClient,
getResource,
getResourcesByType,
getTargetResources,
getClients,
register,
runOnDeployed,
runOnDestroyed,
hookAdd,
info: pipe([
() => ({
provider: toString(),
stage: getProviderConfig().stage,
...info(),
}),
tap((info) => {
logger.debug(`info ${tos(info)}`);
}),
]),
init,
unInit,
start: startBase,
getSpecs,
getMapNameToResource,
generateCode,
targetResourcesBuildMap,
getListHof,
};
return pipe([
() => ({}),
defaultsDeep(
createResourceMakers({
provider,
specs: getSpecs(),
prefix: "make",
filterResource: not(get("listOnly")),
programOptions: getProgramOptions(),
})
),
defaultsDeep(
createResourceMakers({
provider,
specs: getSpecs(),
prefix: "use",
programOptions: getProgramOptions(),
})
),
defaultsDeep(
createResourceMakers({
provider,
specs: getSpecs(),
prefix: "useDefault",
programOptions: getProgramOptions(),
})
),
(enhanced) => Object.assign(provider, enhanced),
])();
}
module.exports = CoreProvider;