@grucloud/core
Version:
GruCloud core, generate infrastructure code
1,113 lines (1,085 loc) • 29.8 kB
JavaScript
const assert = require("assert");
const {
pipe,
tap,
map,
filter,
tryCatch,
switchCase,
get,
assign,
any,
eq,
not,
flatMap,
transform,
pick,
} = require("rubico");
const {
size,
isEmpty,
callProp,
pluck,
forEach,
find,
defaultsDeep,
includes,
identity,
isFunction,
flatten,
} = require("rubico/x");
const { Lister } = require("./Lister");
const logger = require("./logger")({ prefix: "ProviderGru" });
const { tos } = require("./tos");
const { Planner, mapToGraph } = require("./Planner");
const { convertError, TitleDeploying } = require("./Common");
const { buildGraphLive } = require("./GraphLive");
const { buildGraphTarget } = require("./GraphTarget");
const { buildGraphTree } = require("./GraphTree");
const { decorateLive } = require("./Client");
const {
nextStateOnError,
contextFromProvider,
contextFromPlanner,
contextFromResource,
createGetResource,
contextFromHookGlobal,
contextFromHookGlobalInit,
contextFromHookGlobalAction,
addErrorToResults,
assignErrorToObject,
} = require("./ProviderCommon");
const GraphCommon = require("./GraphCommon");
const { createLives } = require("./Lives");
const dependenciesTodependsOn = ({ dependencies, stacks }) =>
pipe([
tap(() => {
assert(Array.isArray(stacks));
}),
() => dependencies,
transform(
map(({ name }) => [name]),
() => []
),
filter((name) =>
pipe([() => stacks, find(eq(get("provider.name"), name))])()
),
])();
const runnerParams = ({ provider, isProviderUp, stacks }) => ({
key: provider.name,
meta: { providerName: provider.name, provider },
dependsOn: dependenciesTodependsOn({
dependencies: provider.dependencies,
stacks,
}),
isUp: isProviderUp,
});
const buildDependsOnReverse = (stacks) =>
pipe([
() => stacks,
map(
pipe([
get("provider"),
({ name, dependencies }) => ({
name,
dependsOn: dependenciesTodependsOn({ dependencies, stacks }),
}),
])
),
(specDependsOn) =>
pipe([
() => specDependsOn,
map(({ name }) => ({
name,
dependsOn: pipe([
() => specDependsOn,
filter(pipe([get("dependsOn"), includes(name)])),
map(get("name")),
])(),
})),
])(),
tap((specDependsOn) => {
//logger.info(`buildDependsOnReverse: ${tos(specDependsOn)}`);
}),
])();
exports.ProviderGru = ({
mapGloblalNameToResource = new Map(),
hookGlobal,
stacks,
lives = createLives(),
programOptions = {},
}) => {
assert(Array.isArray(stacks));
assert(mapGloblalNameToResource);
assert(programOptions);
const getResource = createGetResource({ mapGloblalNameToResource });
const getProviderConfig = pipe([
get("provider.config"),
tap((config) => {
assert(config);
}),
]);
const onStateChangeResource =
({ operation, onStateChange }) =>
({ resource, error, ...other }) =>
pipe([
tap((params) => {
assert(resource, "no resource");
assert(resource.type, "no resource.type");
}),
() =>
onStateChange({
context: contextFromResource({ operation, resource }),
error,
...other,
}),
tap.if(
() => error,
pipe([
() =>
onStateChange({
context: contextFromPlanner({
providerName: resource.providerName,
title: TitleDeploying,
}),
nextState: nextStateOnError(true),
}),
() =>
onStateChange({
context: contextFromProvider({
providerName: getProvider(resource).displayName(),
}),
nextState: nextStateOnError(true),
}),
])
),
])();
const upsertResources =
({ onStateChange, title }) =>
(plans) =>
pipe([
tap((params) => {
assert(onStateChange);
assert(title);
assert(Array.isArray(plans));
assert(plans);
}),
() => plans,
switchCase([
isEmpty,
pipe([() => ({ error: false, plans })]),
pipe([
() => ({
plans,
dependsOnType: getSpecs(),
dependsOnInstance: mapToGraph(mapGloblalNameToResource),
executor: plannerExecutor,
onStateChange: onStateChangeResource({
operation: TitleDeploying,
onStateChange,
}),
}),
Planner,
callProp("run"),
tap((params) => {
assert(true);
}),
]),
]),
])();
const plannerExecutor = ({ item: { resource, live, action, diff } }) =>
pipe([
tap(() => {
assert(resource);
assert(resource.type);
assert(resource.name);
// logger.debug(
// `plannerExecutor: executor ${resource.uri}, action: ${action}`
// );
}),
() => ({}),
assign({ engine: () => getResource(resource) }),
tap(({ engine }) => {
assert(engine, `Cannot find resource ${tos(resource)}`);
}),
assign({
resolvedDependencies: ({ engine }) =>
engine.resolveDependencies({
dependenciesMustBeUp: true,
}),
}),
assign({
input: ({ engine, resolvedDependencies }) =>
engine.resolveConfig({
live,
resolvedDependencies,
deep: true,
}),
}),
assign({
output: switchCase([
//TODO rubico
() => action === "UPDATE",
({ engine, input, resolvedDependencies }) =>
engine.update({
payload: input,
live,
diff: engine.spec.compare({
...engine.spec,
live,
lives,
target: input,
config: getProviderConfig(engine),
targetResources: [...mapGloblalNameToResource.values()],
programOptions,
}),
resolvedDependencies,
lives,
}),
//TODO rubico
() => action === "CREATE",
({ engine, input, resolvedDependencies }) =>
pipe([
() =>
engine.create({
payload: input,
resolvedDependencies,
lives,
}),
tap((live) => {
assert(live);
}),
decorateLive({
client: engine.getClient(),
lives,
config: getProviderConfig(engine),
}),
tap((resource) => {
lives.addResource({
groupType: engine.spec.groupType,
resource,
});
}),
])(),
() => action === "WAIT_CREATION",
({ engine, input, resolvedDependencies }) =>
pipe([
() => ({ lives }),
engine.waitForResourceUp,
tap((params) => {
assert(true);
}),
])(),
() => {
assert(false, `action '${action}' is not handled`);
},
]),
}),
pick(["input", "output"]),
tap((params) => {
assert(true);
}),
])();
const getProviders = pipe([
tap(() => {
assert(stacks);
}),
() => stacks,
pluck("provider"),
]);
const getSpecs = pipe([
getProviders,
flatMap(pipe([callProp("getSpecs")])),
tap((params) => {
assert(true);
}),
]);
pipe([
() => stacks,
forEach(({ provider, resources, hooks }) =>
pipe([
tap(() => {
assert(provider);
assert(provider.register);
}),
() => provider.register({ resources, hooks }),
() => provider.setLives(lives),
])()
),
])();
const onStateChangeDefault =
({ onStateChange }) =>
({ key, error, result, nextState }) =>
pipe([
tap.if(
pipe([() => ["DONE", "ERROR"], includes(nextState)]),
pipe([
() => getProvider({ providerName: key }),
(provider) =>
provider.spinnersStopProvider({
onStateChange,
error: result?.error || error,
}),
])
),
])();
const getStack = ({ providerName }) =>
pipe([
() => stacks,
find(eq(get("provider.name"), providerName)),
tap.if(isEmpty, () => {
assert(`no provider with name: '${providerName}'`);
}),
])();
const getProvider = ({ providerName }) =>
pipe([
tap(() => {
assert(providerName);
}),
getProviders,
find(eq(get("name"), providerName)),
tap.if(isEmpty, () => {
assert(`no provider with name: '${providerName}'`);
}),
tap((params) => {
assert(true);
}),
])();
const listLives = ({
onStateChange,
onProviderEnd = () => {},
options,
readWrite,
}) =>
pipe([
tap((providers) => {
//logger.info(`listLives ${JSON.stringify({ options, readWrite })}`);
assert(onStateChange);
assert(providers);
}),
map((provider) =>
pipe([
tap(() => {
assert(provider);
}),
() =>
provider.listLives({
onStateChange,
options,
readWrite,
}),
tap(({ error }) =>
provider.spinnersStopListLives({
onStateChange,
error,
})
),
tap(({ error }) => {
onProviderEnd({ provider, error });
}),
])()
),
tap((params) => {
assert(true);
}),
addErrorToResults,
]);
const planQuery = ({ onStateChange = identity, providers } = {}) =>
pipe([
tap(() => {
//logger.info(`planQuery`);
assert(Array.isArray(providers));
}),
() => providers,
listLives({ onStateChange }),
tap((params) => {
assert(true);
}),
switchCase([
get("error"),
(livesData) => ({ lives: livesData }),
(livesData) =>
pipe([
() => stacks,
map(({ provider, isProviderUp }) => ({
...runnerParams({ provider, isProviderUp, stacks }),
executor: ({ results }) =>
pipe([
tap(() => {
assert(results);
}),
() =>
provider.planQuery({
livesData,
onStateChange,
}),
])(),
})),
Lister({
onStateChange: ({ key, result, nextState }) =>
pipe([
tap.if(
() => includes(nextState)(["DONE", "ERROR"]),
pipe([
() => getProvider({ providerName: key }),
(provider) => {
//TODO
},
])
),
])(),
}),
tap((params) => {
assert(true);
}),
assign({
results: pipe([
get("results"),
callProp("sort", (a, b) =>
a.providerName.localeCompare(b.providerName)
),
]),
}),
(result) => ({
lives: livesData,
resultQuery: result,
}),
assignErrorToObject,
tap((result) => {
//logger.info(`planQuery done`);
}),
])(),
]),
tap((params) => {
assert(true);
}),
]);
const planApply = ({ plan, onStateChange }) =>
pipe([
tap(() => {
//logger.info(`planApply`);
assert(Array.isArray(plan.results));
}),
() => plan,
get("results"),
filter(not(get("error"))),
(plans) => ({ plans }),
assign({
start: pipe([
get("plans"),
map(
tryCatch(
({ providerName }) =>
pipe([
() => ({ providerName }),
getProvider,
callProp("start", { onStateChange }),
])(),
(error, { providerName }) => {
logger.error(
`planApply start error ${tos(convertError({ error }))}`
);
return {
error: convertError({ error, name: "Apply" }),
providerName,
};
}
)
),
]),
}),
assign({
resultCreate: pipe([
get("plans"),
pluck("resultCreate"),
flatten,
upsertResources({
onStateChange,
title: TitleDeploying,
}),
tap((params) => {
assert(true);
}),
]),
}),
assign({
resultHooks: pipe([
get("plans"),
tap((params) => {
assert(true);
}),
map(
tryCatch(
pipe([
pick(["providerName"]),
getProvider,
callProp("runOnDeployed", { onStateChange }),
]),
(error, { providerName }) => {
logger.error(
`planApply hooks error ${tos(convertError({ error }))}`
);
return {
error: convertError({ error, name: "Apply" }),
providerName,
};
}
)
),
addErrorToResults,
]),
}),
assign({
resultOnDeploy: ({ resultCreate }) =>
pipe([
tap((params) => {
assert(resultCreate);
}),
getSpecs,
filter(get("onDeployed")),
map(
tryCatch(
({ providerName, onDeployed }) =>
pipe([
() => ({ providerName }),
getProvider,
(provider) =>
onDeployed({
resultCreate,
lives: provider.lives,
config: provider.getConfig(),
}),
])(),
(error) => {
logger.error(
`planApply onDeployed error ${tos(convertError({ error }))}`
);
return {
error: convertError({ error, name: "Apply" }),
};
}
)
),
addErrorToResults,
])(),
}),
tap((params) => {
assert(true);
}),
tap(
pipe([
get("plans"),
map(({ providerName }) =>
pipe([
() =>
onStateChange({
context: contextFromPlanner({
providerName,
title: TitleDeploying,
}),
nextState: "DONE",
}),
() =>
onStateChange({
context: contextFromProvider({
providerName: getProvider({ providerName }).displayName(),
}),
nextState: "DONE",
}),
])()
),
])
),
tap((params) => {
assert(true);
}),
assignErrorToObject,
])();
const startProvider = ({ onStateChange }) =>
pipe([
tap(() => {
//logger.debug(`startProvider`);
assert(onStateChange);
assert(stacks);
}),
() => stacks,
map(({ provider, isProviderUp }) => ({
...runnerParams({ provider, isProviderUp, stacks }),
executor: ({ results }) =>
pipe([
() => provider.start({ onStateChange }),
tap((params) => {
// logger.debug(`startProvider started`);
}),
() => ({
provider,
isProviderUp,
}),
])(),
})),
Lister({
onStateChange: ({ key, result, nextState }) =>
pipe([
tap.if(
//TODO eq ?
() => includes(nextState)(["ERROR"]),
pipe([
() => getProvider({ providerName: key }),
tap((provider) => {
logger.error(`error startProvider provider ${provider.name}`);
}),
(provider) =>
provider.spinnersStopListLives({
onStateChange,
error: true,
}),
])
),
])(),
}),
get("results"),
map(pick(["error", "providerName"])),
addErrorToResults,
tap(({ results }) => {
//logger.debug(`startProvider #providers ${size(results)}`);
}),
])();
const planQueryDestroy = ({ onStateChange, options, providers }) =>
pipe([
tap(() => {
//logger.info(`planQueryDestroy ${JSON.stringify(options)}`);
assert(onStateChange);
assert(Array.isArray(providers));
}),
() => providers,
listLives({ onStateChange, options, readWrite: true }),
(livesData) =>
pipe([
tap((params) => {
assert(livesData);
}),
//TODO start twice ?
() => ({ onStateChange }),
startProvider,
() => providers,
map((provider) =>
pipe([
tap((params) => {
assert(provider);
}),
() =>
provider.planQueryDestroy({
onStateChange,
options,
livesData,
}),
tap(({ error }) => {
provider.spinnersStopProvider({
onStateChange,
error,
});
}),
])()
),
tap((params) => {
assert(true);
}),
addErrorToResults,
(resultQueryDestroy) => ({ lives: livesData, resultQueryDestroy }),
assignErrorToObject,
tap((results) => {
//logger.info(`planQueryDestroy done`);
}),
])(),
]);
const planDestroy = ({ plan, onStateChange = identity, options }) =>
pipe([
tap(() => {
//logger.info(`planDestroy`);
assert(plan);
}),
() => plan.results,
filter(not(get("error"))),
map(({ providerName, plans }) =>
pipe([
() => getStack({ providerName }),
({ provider }) => ({
key: providerName,
meta: { providerName },
dependsOn: pipe([
() => stacks,
buildDependsOnReverse,
find(eq(get("name"), providerName)),
get("dependsOn"),
])(),
isUp: () => true,
executor: ({}) =>
pipe([
() => provider.start({ onStateChange }),
assign({
resultDestroy: () =>
provider.planDestroy({
plans,
planAll: plan,
onStateChange,
options,
}),
}),
assign({
resultHooks: () => provider.runOnDestroyed({ onStateChange }),
}),
assignErrorToObject,
tap((params) => {
assert(params);
}),
])(),
}),
])()
),
Lister({
onStateChange: onStateChangeDefault({ onStateChange }),
}),
tap((result) => {
assert(true);
}),
])();
//TODO do not use lister
const runCommand = ({
onStateChange = identity,
commandOptions,
programOptions,
functionName,
} = {}) =>
pipe([
tap(() => {
assert(functionName);
//logger.info(`runCommand ${functionName}`);
}),
() => stacks, //TODO provider up
map(({ provider, isProviderUp }) => ({
...runnerParams({ provider, isProviderUp, stacks }),
executor: ({ results }) =>
pipe([
tap(() => {
assert(
provider[functionName],
`${functionName} is not a provider function `
);
}),
() => provider.start({ onStateChange }),
//TODO
() =>
provider[functionName]({
onStateChange,
commandOptions,
programOptions,
providers: getProviders(),
}),
assign({ providerName: () => provider.name }),
])(),
})),
Lister({
onStateChange: onStateChangeDefault({ onStateChange }),
name: functionName,
}),
tap((result) => {
//logger.info(`runCommand result: ${JSON.stringify(result)}`);
}),
])();
const startHookGlobalSpinners = ({ hookType, onStateChange, hookInstance }) =>
pipe([
() => {
//logger.debug(`startHookGlobalSpinners: ${hookType}`);
assert(hookType);
assert(onStateChange);
assert(hookInstance[hookType]);
},
() =>
onStateChange({
context: contextFromHookGlobalInit({ hookType }),
nextState: "WAITING",
indent: 2,
}),
() => hookInstance,
get(hookType),
get("actions"),
forEach((action) =>
onStateChange({
context: contextFromHookGlobalAction({
hookType,
name: action.name,
}),
nextState: "WAITING",
indent: 4,
})
),
])();
const runGlobalActions = ({
actions,
hookType,
onStateChange,
actionPayload,
}) =>
pipe([
tap(() => {
logger.debug(`runGlobalActions: ${hookType}, #action ${size(actions)}`);
//assert(Array.isArray(actions), "actions is not an array");
assert(hookType);
assert(onStateChange);
}),
() => actions,
map((action) =>
pipe([
tap(() => {
assert(action);
assert(isFunction(action.command), `command is not a function`);
assert(action.name, "action is missing the name properties");
logger.debug(`action name: ${action.name}`);
}),
tap(() =>
onStateChange({
context: contextFromHookGlobalAction({
hookType,
name: action.name,
}),
nextState: "RUNNING",
})
),
tryCatch(
() => action.command(actionPayload),
pipe([
(error) => convertError({ error }),
tap((error) => {
logger.error(
`runCommandGlobal ${convertError({
error,
name: action.name,
})}`
);
}),
(error) => ({ error, action: action.name }),
])
),
tap(({ error } = {}) =>
onStateChange({
context: contextFromHookGlobalAction({
hookType,
name: action.name,
}),
nextState: nextStateOnError(error),
error,
})
),
])()
),
(results) => ({
error: any(get("error"))(results),
results,
}),
tap((result) => {
assert(result);
}),
])();
const runHookInstance = ({ hookType, onStateChange, hookInstance }) =>
pipe([
tap(() => {
assert(hookInstance);
assert(hookInstance[hookType]);
}),
tap(() =>
startHookGlobalSpinners({
hookType,
onStateChange,
hookInstance,
})
),
tap(() =>
onStateChange({
context: contextFromHookGlobalInit({ hookType }),
nextState: "RUNNING",
})
),
tryCatch(
pipe([
() => hookInstance,
get(hookType),
callProp("init"),
tap((actionPayload) => {
logger.debug(`init result: ${actionPayload}`);
}),
tap(() =>
onStateChange({
context: contextFromHookGlobalInit({ hookType }),
nextState: "DONE",
})
),
(actionPayload) => ({ kind: "#hookActions", actionPayload }),
assign({
results: ({ actionPayload }) =>
runGlobalActions({
onStateChange,
actions: hookInstance[hookType].actions,
hookType,
actionPayload,
}),
}),
assignErrorToObject,
tap((result) => {
assert(true);
}),
]),
pipe([
(error) => convertError({ error }),
tap((error) => {
logger.error(`runHookInstance ${hookType}, ${tos(error)}`);
}),
tap((error) =>
onStateChange({
context: contextFromHookGlobalInit({ hookType }),
nextState: "ERROR",
error,
})
),
(error) => ({ error, hookType }),
])
),
tap((result) => {
assert(result);
}),
])();
const runCommandGlobal = ({ hookType, onStateChange }) =>
pipe([
tap(() => {
assert(hookType);
assert(onStateChange);
// logger.debug(
// `runCommandGlobal ${hookType}, hashookGlobal: ${!!hookGlobal}`
// );
}),
switchCase([
() => hookGlobal,
pipe([
tap(() =>
onStateChange({
context: contextFromHookGlobal({ hookType }),
nextState: "WAITING",
})
),
tap(() =>
onStateChange({
context: contextFromHookGlobal({ hookType }),
nextState: "RUNNING",
})
),
tryCatch(
pipe([
tap(() => {
logger.debug(
`runCommandGlobal ${hookType}, hashookGlobal: ${!!hookGlobal}`
);
assert(isFunction(hookGlobal));
assert(stacks);
}),
() => hookGlobal({ stacks }),
switchCase([
pipe([get(hookType), isEmpty]),
identity,
(hookInstance) =>
runHookInstance({ hookType, onStateChange, hookInstance }),
]),
]),
(error) =>
pipe([
tap(() => {
logger.error(
`runCommandGlobal ${hookType}, ${tos(
convertError({ error })
)}`
);
}),
tap(() =>
onStateChange({
context: contextFromHookGlobalInit({ hookType }),
nextState: "ERROR",
error: convertError({ error, name: hookType }),
})
),
() => ({ error, hookType }),
])()
),
tap((result) => {
logger.debug(result);
}),
tap(({ error }) =>
onStateChange({
context: contextFromHookGlobal({ hookType }),
nextState: nextStateOnError(error),
error,
})
),
]),
identity,
]),
tap((result) => {
//logger.debug(`runCommandGlobal ${hookType}, done`);
}),
])();
const generateCode = ({ commandOptions, programOptions }) =>
pipe([
tap(() => {
logger.info(
`generateCode ${JSON.stringify({ commandOptions, programOptions })}`
);
}),
getProviders,
// Synchronous loop
map.pool(
1,
tryCatch(
callProp("generateCode", {
commandOptions,
programOptions,
providers: getProviders(),
}),
(error) => {
logger.error("generateCode", error);
throw error;
}
)
),
])();
return {
startProvider,
listLives,
planQuery,
planApply,
planQueryDestroy,
planDestroy,
generateCode,
getProvider,
getProviders,
runCommand,
runCommandGlobal,
buildGraphTarget: ({ options }) =>
buildGraphTarget({
providers: getProviders(),
options: defaultsDeep(GraphCommon.optionsDefault({ kind: "target" }))(
options
),
}),
buildGraphLive: ({ lives, options }) =>
buildGraphLive({
lives,
options: defaultsDeep(GraphCommon.optionsDefault({ kind: "live" }))(
options
),
}),
buildGraphTree: ({ options }) =>
buildGraphTree({
providers: getProviders(),
options,
}),
generateCode,
getResource,
};
};