@adpt/cloud
Version:
AdaptJS cloud component library
215 lines • 8.47 kB
JavaScript
;
/*
* Copyright 2018-2019 Unbounded Systems, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const core_1 = require("@adpt/core");
const graphql_1 = require("graphql");
const graphql_type_json_1 = tslib_1.__importDefault(require("graphql-type-json"));
const jsonStableStringify = require("json-stable-stringify");
const node_fetch_1 = tslib_1.__importDefault(require("node-fetch"));
const ts_custom_error_1 = require("ts-custom-error");
const k8sSwagger = require("../../src/k8s/kubernetes-1.8-swagger.json");
const swagger2gql_1 = tslib_1.__importDefault(require("../../src/swagger2gql"));
const kubectl_1 = require("./kubectl");
// tslint:disable-next-line:no-var-requires
const swaggerClient = require("swagger-client");
class K8sNotFound extends ts_custom_error_1.CustomError {
constructor(jsonBody) {
let msg = `Resource not found`;
const info = jsonBody && jsonBody.message;
if (info)
msg += ": " + info;
super(msg);
const request = jsonBody && jsonBody.details;
if (request && typeof request.kind === "string" &&
typeof request.name === "string") {
this.request = request;
}
}
}
exports.K8sNotFound = K8sNotFound;
class K8sResponseError extends ts_custom_error_1.CustomError {
constructor(resp, status) {
super(`Error fetching ${resp.url}: status ${resp.statusText} ` +
`(${resp.status}): ${status.message || ""}\n` +
JSON.stringify(status, null, 2));
this.status = status;
}
}
exports.K8sResponseError = K8sResponseError;
function getK8sConnectInfo(kubeconfig) {
function byName(name) { return (x) => x.name === name; }
const contextName = kubeconfig["current-context"];
const context = kubeconfig.contexts.find(byName(contextName));
if (!context)
throw new Error(`Could not find context ${contextName}`);
const cluster = kubeconfig.clusters.find(byName(context.context.cluster));
if (!cluster)
throw new Error(`Could not find cluster ${context.context.cluster}`);
return { url: cluster.cluster.server };
}
const infoSym = Symbol("k8sInfoSym");
function computeQueryId(clusterId, fieldName, args) {
return jsonStableStringify({
clusterId,
fieldName,
args,
});
}
async function fetchProxy(context, kubeconfig) {
const proxies = context.proxies;
const id = jsonStableStringify(kubeconfig);
if (proxies[id])
return proxies[id];
const rawInfo = kubectl_1.kubectlProxy({ kubeconfig });
proxies[id] = rawInfo.then((info) => {
return {
url: info.url,
child: info.child,
kill: () => {
delete proxies[id];
info.kill();
}
};
});
return proxies[id];
}
const k8sObserveResolverFactory = {
fieldResolvers: (_type, fieldName, isQuery) => {
if (!isQuery)
return;
if (fieldName === "withKubeconfig") {
return async (_obj, args, context) => {
const kubeconfig = args.kubeconfig;
if (kubeconfig === undefined)
throw new Error("No kubeconfig specified");
const info = getK8sConnectInfo(kubeconfig);
const { url: proxyUrl } = await fetchProxy(context, kubeconfig);
//FIXME(manishv) Canonicalize id here (e.g. port, fqdn, etc.)
return { [infoSym]: { host: proxyUrl, id: info.url, headers: {} } };
};
}
return async (obj, args, context, _info) => {
const req = await swaggerClient.buildRequest({
spec: k8sSwagger,
operationId: fieldName,
parameters: args,
requestContentType: "application/json",
responseContentType: "application/json"
});
const url = obj[infoSym].host + req.url;
const headers = obj[infoSym].headers;
const resp = await node_fetch_1.default(url, Object.assign({}, req, { headers }));
const ret = await resp.json();
if (resp.status === 404)
throw new K8sNotFound(ret);
else if (!resp.ok)
throw new K8sResponseError(resp, ret);
const queryId = computeQueryId(obj[infoSym].id, fieldName, args);
context.observations[queryId] = ret; //Overwrite in case data got updated on later query
return ret;
};
}
};
const k8sQueryResolverFactory = {
fieldResolvers: (_type, fieldName, isQuery) => {
if (!isQuery)
return;
if (fieldName === "withKubeconfig") {
return async (_obj, args, _context) => {
const kubeconfig = args.kubeconfig;
if (kubeconfig === undefined)
throw new Error("No kubeconfig specified");
const info = getK8sConnectInfo(kubeconfig);
return { [infoSym]: { id: info.url } };
};
}
return async (obj, args, context, _info) => {
const queryId = computeQueryId(obj[infoSym].id, fieldName, args);
if (!context)
throw new core_1.ObserverNeedsData();
if (!Object.hasOwnProperty.call(context, queryId))
throw new core_1.ObserverNeedsData();
return context[queryId];
};
}
};
function buildSchema(resolverFactory) {
const k8sSchema = swagger2gql_1.default(k8sSwagger, resolverFactory);
const k8sQueryOrig = k8sSchema.getQueryType();
if (k8sQueryOrig === undefined)
throw new Error("Internal error, invalid kubernetes schema");
if (k8sQueryOrig === null)
throw new Error("Internal Error, invalid kuberenetes schema");
const k8sQuery = Object.create(k8sQueryOrig);
k8sQuery.name = "K8sApi";
const query = new graphql_1.GraphQLObjectType({
name: "Query",
fields: () => ({
withKubeconfig: {
type: k8sQuery,
args: {
kubeconfig: {
type: new graphql_1.GraphQLNonNull(graphql_type_json_1.default),
}
},
resolve: resolverFactory.fieldResolvers ?
resolverFactory.fieldResolvers(query, "withKubeconfig", true) :
() => undefined
}
}),
});
const k8sObserverSchema = new graphql_1.GraphQLSchema({
query
});
return k8sObserverSchema;
}
function buildObserveSchema() {
return buildSchema(k8sObserveResolverFactory);
}
function buildQuerySchema() {
return buildSchema(k8sQueryResolverFactory);
}
//Building these can be very slow so we wait for someone to use K8sObserver
let querySchema;
let observeSchema;
class K8sObserver {
constructor() {
this.observe = async (queries) => {
const context = { proxies: {}, observations: {} };
if (queries.length > 0) {
if (!observeSchema)
observeSchema = buildObserveSchema();
const waitFor = queries.map((q) => Promise.resolve(graphql_1.execute(observeSchema, q.query, null, context, q.variables)));
const results = await Promise.all(waitFor);
for (const proxy of Object.keys(context.proxies)) {
(await context.proxies[proxy]).kill();
}
core_1.throwObserverErrors(results);
}
return { context: context.observations };
};
}
get schema() {
if (!querySchema)
querySchema = buildQuerySchema();
return querySchema;
}
}
exports.K8sObserver = K8sObserver;
core_1.registerObserver(new K8sObserver());
//# sourceMappingURL=k8s_observer.js.map