@grucloud/provider-k8s
Version:
The GruCloud Kubernetes provider
396 lines (372 loc) • 10.5 kB
JavaScript
const assert = require("assert");
const urljoin = require("url-join");
const {
map,
pipe,
tap,
tryCatch,
get,
switchCase,
eq,
omit,
assign,
fork,
not,
} = require("rubico");
const {
size,
defaultsDeep,
isEmpty,
includes,
isFunction,
append,
} = require("rubico/x");
const { retryCall, retryCallOnError } = require("@grucloud/core/Retry");
const { tos } = require("@grucloud/core/tos");
const { logError, axiosErrorToJSON } = require("@grucloud/core/Common");
const logger = require("@grucloud/core/logger")({ prefix: "K8sClient" });
const {
createAxiosMakerK8s,
getServerUrl,
getNamespace,
displayNameResourceNamespace,
displayNameNamespace,
} = require("./K8sCommon");
module.exports = K8sClient = ({
spec,
config,
configDefault,
pathList,
pathGet,
pathGetStatus,
pathCreate,
pathDelete,
pathUpdate,
displayNameResource = displayNameResourceNamespace,
displayName = displayNameNamespace,
isInstanceUp = not(isEmpty),
cannotBeDeleted = () => () => false,
findDependencies,
}) => {
assert(spec);
assert(spec.type);
assert(config);
assert(config.accessToken);
assert(config.kubeConfig);
assert(pathList);
assert(pathGet);
const { kubeConfig } = config;
const { type, providerName } = spec;
assert(providerName);
const findNameShort = ({ live }) =>
pipe([
tap(() => {
assert(live, `findName: no live`);
}),
() => live,
get("metadata.name"),
])();
const findName = () => (live) =>
pipe([
tap(() => {
assert(live, `findName: no live`);
}),
() => live,
get("metadata"),
({ name, namespace }) =>
pipe([
tap(() => {
assert(name);
}),
() => namespace,
switchCase([isEmpty, () => "", append("::")]),
append(name),
])(),
tap((params) => {
assert(true);
}),
])();
const findMeta = () => (live) =>
pipe([
tap(() => {
if (!live) {
assert(live, `findMeta: no live`);
}
}),
() => live,
get("metadata", {}),
tap((metadata) => {
assert(metadata);
}),
])();
const findId = findName;
const findNamespace = () => (live) =>
pipe([
tap(() => {
assert(live, `findNamespace: no live`);
}),
() => live,
get("metadata.namespace", ""),
])();
const findNamespaceFromTarget = ({ properties }) =>
get("live.metadata.namespace", "")(properties({ dependencies: {} }));
const axios = () => createAxiosMakerK8s({ config });
const filterList =
({ type }) =>
(data) =>
pipe([
get("items"),
map(omit(["metadata.managedFields"])),
tap((items) => {
assert(true);
}),
(items) => assign({ type: () => type, items: () => items })(data),
tap((result) => {
//logger.debug(`filterList result ${tos(result)}`);
}),
])(data);
const getList = tryCatch(
pipe([
tap((params) => {
logger.debug(`getList k8s ${type}`);
}),
pathList,
(path) => urljoin(getServerUrl(kubeConfig()), path),
(fullPath) =>
retryCallOnError({
name: `getList type: ${type}, path ${fullPath}`,
fn: () => axios().get(fullPath),
isExpectedException: pipe([
tap((ex) => {
logger.info(`getList type: ${type}, ex: ${ex}`);
}),
eq(get("response.status"), 404),
tap((result) => {
logger.info(
`getList type: ${type} isExpectedException ${result}`
);
}),
]),
config,
}),
get("data", []),
filterList({ type }),
tap(({ items }) => {
logger.info(`getList k8s ${type}, #items ${size(items)}`);
}),
]),
(error) => {
logError(`getList ${type}`, error);
throw Error(axiosErrorToJSON(error));
}
);
const getByKey = ({ name, namespace, resolvePath }) =>
tryCatch(
pipe([
tap(() => {
logger.info(`getByKey ${JSON.stringify({ name, namespace })}`);
//assert(name);
//assert(namespace);
}),
() => resolvePath({ name, namespace }),
(path) => urljoin(getServerUrl(kubeConfig()), path),
(fullPath) =>
retryCallOnError({
name: `getByKey type ${type}, name: ${name}, path: ${fullPath}`,
fn: () => axios().get(fullPath),
config,
}),
get("data"),
tap((data) => {
logger.debug(`getByKey result ${name}: ${tos(data.status)}`);
}),
]),
switchCase([
eq(get("response.status"), 404),
() => {},
(error) => {
logError("getByKey", error);
throw axiosErrorToJSON(error);
},
])
)();
const getByName = ({ name, dependencies, properties = ({}) => ({}) }) =>
pipe([
tap(() => {
assert(isFunction(dependencies));
}),
() => properties({ dependencies: {} }),
(props) =>
getByKey({
resolvePath: pathGet,
name: get("metadata.name", name)(props),
namespace: get("metadata.namespace")(props),
}),
])();
const getById = ({ live }) =>
getByKey({
resolvePath: pathGetStatus || pathGet,
name: findNameShort({ live }),
namespace: get("namespace")(findMeta()(live)),
});
const isUpById = pipe([getById, isInstanceUp]);
const isDownById = pipe([getById, isEmpty]);
const create = ({ name, payload, dependencies }) =>
tryCatch(
pipe([
tap(() => {
logger.info(`create ${type}::${name}, payload: ${tos(payload)}`);
assert(name);
assert(payload);
}),
() =>
pathCreate({
name,
apiVersion: payload.apiVersion,
namespace: payload.metadata.namespace,
}),
tap((path) => {
logger.info(`create ${type}/${name}, path: ${path}`);
}),
(path) => urljoin(getServerUrl(kubeConfig()), path),
(fullPath) =>
retryCallOnError({
name: `create ${type}/${name} path: ${fullPath}`,
fn: () => axios().post(fullPath, payload),
config,
shouldRetryOnException: ({ error }) =>
pipe([
() => error,
get("response.status"),
//TODO 404 on Create ?
(status) => includes(status)([404, 500]),
tap((retry) => {
logger.info(
`shouldRetryOnException create ${type}/${name}, status: ${error.status}, retry: ${retry}`
);
}),
])(),
}),
tap((result) => {
logger.info(`created ${type}/${name}, status: ${result.status}`);
}),
get("data"),
tap((live) =>
retryCall({
name: `create ${type}, name: ${name}, isUpById`,
fn: () => isUpById({ live }),
config: { retryDelay: 5e3, retryCount: 5 * 12e3 },
})
),
tap((live) => {
//logger.debug(`created ${type}/${name}, live: ${tos(live)}`);
}),
]),
(error) => {
logError(`create ${type}/${name}`, error);
throw axiosErrorToJSON(error);
}
)();
const update = ({ name, payload, dependencies, live, diff }) =>
tryCatch(
pipe([
tap(() => {
logger.info(`update ${type}/${name}, diff: ${tos(diff)}`);
assert(name);
assert(payload);
assert(live);
assert(payload.metadata.name);
}),
fork({
fullPath: pipe([
() =>
pathUpdate({
name: payload.metadata.name,
namespace:
payload.metadata.namespace ||
getNamespace(dependencies().namespace),
}),
tap((path) => {
logger.info(`update ${type}/${name}, path: ${path}`);
}),
(path) => urljoin(getServerUrl(kubeConfig()), path),
]),
data: pipe([() => payload, defaultsDeep(omit(["status"])(live))]),
}),
({ fullPath, data }) =>
retryCallOnError({
name: `update ${type}/${name} path: ${fullPath}`,
fn: () => axios().put(fullPath, data),
config,
}),
tap((result) => {
logger.info(`updated ${type}/${name}, status: ${result.status}`);
logger.debug(`updated ${type}/${name} data: ${tos(result.data)}`);
}),
get("data"),
]),
(error) => {
logError(`update ${type}/${name}`, error);
throw axiosErrorToJSON(error);
}
)();
const destroy = ({ live }) =>
tryCatch(
pipe([
tap(() => {
assert(!isEmpty(live), `destroy invalid live`);
}),
() => ({
name: findNameShort({ live }),
namespace: findMeta()(live).namespace,
}),
tap((params) => {
logger.info(`destroy k8s ${JSON.stringify({ params })}`);
}),
pathDelete,
//TODO check gracePeriodSeconds
(path) =>
urljoin(getServerUrl(kubeConfig()), path, "?gracePeriodSeconds=10"),
(fullPath) =>
retryCallOnError({
name: `destroy type ${type}, path: ${fullPath}`,
fn: () => axios().delete(fullPath),
config,
}),
get("data"),
tap((data) => {
logger.info(`destroy ${JSON.stringify({ type, data })} destroyed`);
}),
tap(() =>
retryCall({
name: `destroy ${type}, name: ${findName({ live })}, isDownById`,
fn: () => isDownById({ live }),
config: { retryDelay: 5e3, retryCount: 5 * 12e3 },
})
),
]),
(error) => {
logError(`delete ${type} ${tos({ live })}`, error);
throw axiosErrorToJSON(error);
}
)();
return {
spec,
displayName,
displayNameResource,
findName,
findMeta,
getByName,
findId,
getList,
getById,
create,
update,
destroy,
cannotBeDeleted,
configDefault,
findDependencies,
findNamespace,
findNamespaceFromTarget,
};
};