"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");
// This is to deal with the long URLs in doc comments.
// tslint:disable: max-line-length
const core_1 = tslib_1.__importStar(require("@adpt/core"));
const utils_1 = require("@adpt/utils");
const lodash_1 = require("lodash");
const NetworkService_1 = require("../NetworkService");
const common_1 = require("./common");
const k8s_observer_1 = require("./k8s_observer");
const manifest_support_1 = require("./manifest_support");
const Resource_1 = require("./Resource");
function toServiceType(scope) {
    switch (scope) {
        case "cluster-internal":
        case undefined:
            return "ClusterIP";
        case "cluster-public":
            return "NodePort";
        case "external":
            return "LoadBalancer";
        default:
            throw new Error(`Service: NetworkService scope '${scope}' not mapped to a Kubernetes service type`);
    }
}
/**
 * Convert {@link NetworkService} props to {@link k8s.Service} props
 * @param abstractProps - props to convert
 * @returns Kubernetes spec corresponding to `abstractProps`
 *
 * @internal
 */
function k8sServiceProps(abstractProps) {
    if (typeof abstractProps.port !== "number")
        throw new Error(`Service: Port string not yet implemented`);
    if (abstractProps.ip != null)
        throw new Error(`Service: IP not yet implemented`);
    if (abstractProps.name != null)
        throw new Error(`Service: name not yet implemented`);
    const port = {
        port: abstractProps.port,
        targetPort: NetworkService_1.targetPort(abstractProps),
    };
    if (abstractProps.protocol != null)
        port.protocol = abstractProps.protocol;
    const ret = {
        key: abstractProps.key,
        ports: [port],
        selector: abstractProps.endpoint,
        type: toServiceType(abstractProps.scope),
    };
    return ret;
}
exports.k8sServiceProps = k8sServiceProps;
const defaultProps = {
    sessionAffinity: "None",
    type: "ClusterIP",
};
function findInArray(arr, keyProp, key) {
    if (!arr)
        return undefined;
    for (const item of arr) {
        if (item[keyProp] === key)
            return item;
    }
    return undefined;
}
const noExternalName = { noName: true };
function isNoExternalName(x) {
    return x === noExternalName;
}
async function getExternalName(props) {
    const log = console; //FIXME(manishv) Use proper logger here
    const resourceHand = props.handle;
    const resourceElem = resourceHand.target;
    if (!resourceElem)
        return noExternalName; //This should not be able to happen
    if (!core_1.isMountedElement(resourceElem))
        return noExternalName; //Should not be possible
    //Don't fetch status if we don't need it
    if (!(props.type === "LoadBalancer" || props.type === "ExternalName"))
        return noExternalName;
    let statusTop;
    try {
        statusTop = await resourceElem.status();
    }
    catch (e) {
        //Status not available yet
        if (e.message.startsWith("Resource not found"))
            return core_1.waiting("Waiting for resource to be created");
        throw e;
    }
    if (!statusTop)
        return core_1.waiting("Waiting for status from k8s");
    const spec = statusTop.spec;
    const status = statusTop.status;
    if (!status)
        return core_1.waiting("Waiting for status from k8s");
    if (spec.type === "LoadBalancer") {
        if (status.loadBalancer === undefined)
            return core_1.waiting("Waiting for loadBlancer status from k8s");
        const ingresses = status.loadBalancer.ingress;
        if (ingresses == null)
            return core_1.waiting("Waiting for Ingress IP");
        if (typeof ingresses === "string")
            return ingresses;
        if (Array.isArray(ingresses)) {
            if (ingresses.length === 0)
                return noExternalName;
            if (ingresses.length !== 1)
                log.warn(`Multiple k8s LoadBalancer ingresses returned, using only one: ${ingresses}`);
            for (const ingress of ingresses) {
                if (ingress.hostname)
                    return ingress.hostname;
                if (ingress.ip)
                    return ingress.ip;
            }
        }
        return noExternalName;
    }
    if (spec.type === "ExternalName" && spec.externalName)
        return spec.externalName;
    return noExternalName;
}
/**
 * Native Kubernetes Service resource
 *
 * @remarks
 *
 * Implements the {@link NetworkServiceInstance} interface.
 *
 * @public
 */
function Service(propsIn) {
    const props = propsIn;
    const helpers = core_1.useBuildHelpers();
    const deployID = helpers.deployID;
    if (props.ports && (props.ports.length > 1)) {
        for (const port of props.ports) {
            if (port.name === undefined)
                throw new Error("Service with multiple ports but no name on port");
        }
    }
    const [externalName, setExternalName] = core_1.useState(undefined);
    const [epSelector, updateSelector] = core_1.useState(undefined);
    const manifest = makeSvcManifest(props, { endpointSelector: epSelector });
    core_1.useImperativeMethods(() => ({
        hostname: (scope) => {
            const resourceHand = props.handle;
            const resourceElem = resourceHand.target;
            if (!resourceElem)
                return undefined;
            if (scope && scope === NetworkService_1.NetworkScope.external) {
                if (isNoExternalName(externalName))
                    throw new Error("External name request for element, but no external name available");
                if (typeof externalName === "string")
                    return externalName;
                return undefined;
            }
            else {
                const resourceName = manifest_support_1.resourceElementToName(resourceElem, deployID);
                const namespace = common_1.computeNamespaceFromMetadata(manifest.metadata);
                return `${resourceName}.${namespace}.svc.cluster.local.`;
            }
        },
        port: (name) => {
            if (name) {
                const item = findInArray(props.ports, "name", name);
                if (!item)
                    return undefined;
                return item.port;
            }
            else if (props.ports) {
                //Should it be an error to ask for ports without a name when there is more than one?
                return props.ports[0].port;
            }
            else {
                return undefined;
            }
        }
    }));
    core_1.useDeployedWhen(async () => {
        const statusName = await getExternalName(props);
        if ((typeof statusName === "string") || isNoExternalName(statusName)) {
            setExternalName(statusName);
            return true;
        }
        return statusName;
    });
    updateSelector(async () => {
        const { selector: ep } = props;
        if (!core_1.isHandle(ep))
            return utils_1.removeUndef(ep || {});
        if (!ep.target)
            return {};
        if (!core_1.isMountedElement(ep.target))
            return {};
        if (ep.target.componentType !== Resource_1.Resource) {
            throw new Error(`Cannot handle k8s.Service endpoint of type ${ep.target.componentType.name}`);
        }
        const epProps = ep.target.props;
        if (epProps.kind === "Pod") {
            return utils_1.removeUndef({
                [manifest_support_1.labelKey("name")]: manifest_support_1.resourceElementToName(ep.target, deployID)
            });
        }
        if (hasSelector(epProps)) {
            const sel = epProps.spec.selector;
            if (common_1.isLabelSelector(sel)) {
                if (sel.matchExpressions !== undefined) {
                    throw new Error(`Cannot have k8s.Service endpoint of kind ${epProps.kind} with a matchExpressions selector`);
                }
                return sel.matchLabels;
            }
            else {
                return sel;
            }
        }
        throw new Error(`Cannot have k8s.Service endpoint of kind ${epProps.kind}, is not Pod and does not have a selector prop.`);
    });
    return (core_1.default.createElement(Resource_1.Resource, { key: props.key, config: props.config, kind: manifest.kind, metadata: manifest.metadata, spec: manifest.spec }));
}
exports.Service = Service;
// TODO: The "as any" is a workaround for an api-extractor bug. See issue #185.
Service.defaultProps = defaultProps;
Service.status = async (_props, _observe, buildData) => {
    const succ = buildData.successor;
    if (!succ)
        return undefined;
    return succ.status();
};
function hasSelector(props) {
    return props.spec.selector != undefined;
}
function makeSvcManifest(props, options) {
    const { config, key, handle } = props, spec = tslib_1.__rest(props, ["config", "key", "handle"]);
    // Explicit default for ports.protocol
    if (spec.ports) {
        for (const p of spec.ports) {
            if (p.protocol === undefined)
                p.protocol = "TCP";
        }
    }
    if (spec.type === "LoadBalancer") {
        if (spec.sessionAffinity === undefined)
            spec.sessionAffinity = "None";
        if (spec.externalTrafficPolicy === undefined)
            spec.externalTrafficPolicy = "Cluster";
    }
    return {
        kind: "Service",
        metadata: {},
        spec: Object.assign({}, spec, { selector: core_1.isHandle(spec.selector) ? options.endpointSelector : spec.selector }),
        config,
    };
}
function deployedWhen(statusObj) {
    const status = statusObj;
    // There doesn't appear to be much actual status for a
    // service like there is for a Pod.
    if (status == null || !lodash_1.isObject(status.status)) {
        return core_1.waiting(`Kubernetes cluster returned invalid status for Service`);
    }
    return true;
}
/** @internal */
exports.serviceResourceInfo = {
    kind: "Service",
    deployedWhen,
    statusQuery: async (props, observe, buildData) => {
        const obs = await observe(k8s_observer_1.K8sObserver, core_1.gql `
            query ($name: String!, $kubeconfig: JSON!, $namespace: String!) {
                withKubeconfig(kubeconfig: $kubeconfig) {
                    readCoreV1NamespacedService(name: $name, namespace: $namespace) @all(depth: 100)
                }
            }`, {
            name: manifest_support_1.resourceIdToName(props.key, buildData.id, buildData.deployID),
            kubeconfig: props.config.kubeconfig,
            namespace: common_1.computeNamespaceFromMetadata(props.metadata)
        });
        return obs.withKubeconfig.readCoreV1NamespacedService;
    },
};
manifest_support_1.registerResourceKind(exports.serviceResourceInfo);
//# sourceMappingURL=Service.js.map