"use strict";
/*
 * Copyright 2018-2020 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.18-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