"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 ld = tslib_1.__importStar(require("lodash"));
const utils_1 = require("@adpt/utils");
const yup = tslib_1.__importStar(require("yup"));
const action_1 = require("../action");
const common_1 = require("../common");
const kubectl_1 = require("./kubectl");
const manifest_support_1 = require("./manifest_support");
/**
 * Type assertion to see if an element is both a {@link k8s.Resource | Resource}
 * and a {@link @adpt/core#FinalElement | FinalElement}
 *
 * @param e - element to test
 * @returns `true` if e is both a FinalElement and a {@link k8s.Resource | Resource}, `false` otherwise
 *
 * @public
 */
function isResourceFinalElement(e) {
    return core_1.isFinalDomElement(e) && e.componentType === Resource;
}
exports.isResourceFinalElement = isResourceFinalElement;
/**
 * Decides if an existing Resource is scheduled for deletion
 */
function isDeleting(info) {
    return (info !== undefined) && ("deletionTimestamp" in info.metadata);
}
/**
 * Primitive Component recognized by the k8s plugin to represent resources
 * @public
 */
class Resource extends action_1.Action {
    constructor(props) {
        super(props);
        this.deployedWhen = async (goalStatus, helpers) => {
            if (!isResourcePropsWithConfig(this.props))
                return true;
            const kind = this.props.kind;
            const info = manifest_support_1.getResourceInfo(kind);
            const hand = this.props.handle;
            if (!info)
                throw new Error(`Invalid Resource kind ${kind}`);
            if (!hand)
                throw new Error("Invalid handle");
            try {
                const statObj = await helpers.elementStatus(hand);
                if (goalStatus === core_1.DeployStatus.Destroyed) {
                    return core_1.waiting(`Waiting for ${kind} to be destroyed`);
                }
                return info.deployedWhen(statObj, core_1.DeployStatus.Deployed);
            }
            catch (err) {
                if (ld.isError(err) && err.name === "K8sNotFound") {
                    if (goalStatus === core_1.DeployStatus.Destroyed)
                        return true;
                    return core_1.waiting(`${kind} not present`);
                }
                throw err;
            }
        };
    }
    validate() {
        const children = core_1.childrenToArray(this.props.children);
        if (!ld.isEmpty(children))
            return "Resource elements cannot have children";
        if (!this.props.isTemplate && this.props.config === undefined) {
            throw new Error("Non-template Resource elements must have a config prop");
        }
        yup.object().shape({
            kubeconfig: yup.object().shape({ "current-context": yup.string() }).required(),
            registry: yup.mixed().oneOf([
                yup.object().shape({ internal: yup.string().url(), external: yup.string().url() }),
                yup.string().url()
            ])
        }).validateSync(this.props.config);
        //Do other validations of Specs here
    }
    async shouldAct(op, ctx) {
        if (!isResourcePropsWithConfig(this.props))
            return false;
        const kubeconfig = this.props.config.kubeconfig;
        const deployID = ctx.buildData.deployID;
        const manifest = this.manifest(deployID);
        const name = manifest.metadata.name;
        const kind = manifest.kind;
        const oldManifest = await kubectl_1.kubectlGet({
            kubeconfig,
            name,
            kind
        });
        switch (op) {
            case core_1.ChangeType.create:
            case core_1.ChangeType.modify:
            case core_1.ChangeType.replace:
                if (oldManifest === undefined || isDeleting(oldManifest)) {
                    return {
                        act: true,
                        detail: `Creating ${kind} ${name}`
                    };
                }
                else {
                    const { forbidden, diff } = await kubectl_1.kubectlDiff({
                        kubeconfig,
                        manifest
                    });
                    const opStr = (forbidden || (op === core_1.ChangeType.replace)) ? "Replacing" : "Updating";
                    if (((diff !== undefined) && (diff !== "")) || forbidden) {
                        return {
                            act: true,
                            detail: `${opStr} ${kind} ${name}`
                        };
                    }
                }
                return false;
            case core_1.ChangeType.delete:
                if (oldManifest && !isDeleting(oldManifest)) {
                    return {
                        act: true,
                        detail: `Deleting ${kind} ${name}`
                    };
                }
                return false;
            case core_1.ChangeType.none:
                return false;
        }
    }
    async action(op, ctx) {
        if (!isResourcePropsWithConfig(this.props))
            return;
        const kubeconfig = this.props.config.kubeconfig;
        const deployID = ctx.buildData.deployID;
        const manifest = this.manifest(deployID);
        const name = manifest.metadata.name;
        const kind = manifest.kind;
        const info = await kubectl_1.kubectlGet({
            kubeconfig,
            name,
            kind
        });
        let deleted = false;
        if (isDeleting(info)) {
            //Wait for deleting to complete, else create/modify/apply will fail
            await kubectl_1.kubectlOpManifest("delete", {
                kubeconfig,
                manifest,
                wait: true
            });
            deleted = true;
        }
        if (op === core_1.ChangeType.modify) {
            const { forbidden } = await kubectl_1.kubectlDiff({
                kubeconfig,
                manifest
            });
            op = (op === core_1.ChangeType.modify) && forbidden ? core_1.ChangeType.replace : op;
        }
        switch (op) {
            case core_1.ChangeType.create:
            case core_1.ChangeType.modify:
                await kubectl_1.kubectlOpManifest("apply", {
                    kubeconfig,
                    manifest
                });
                return;
            case core_1.ChangeType.replace:
                if (!deleted) {
                    await kubectl_1.kubectlOpManifest("delete", {
                        kubeconfig,
                        manifest,
                        wait: true
                    });
                }
                await kubectl_1.kubectlOpManifest("apply", {
                    kubeconfig,
                    manifest
                });
                return;
            case core_1.ChangeType.delete:
                if (deleted)
                    return;
                await kubectl_1.kubectlOpManifest("delete", {
                    kubeconfig,
                    manifest,
                    wait: false
                });
                return;
            case core_1.ChangeType.none:
                return;
        }
    }
    async status(observe, buildData) {
        if (!isResourcePropsWithConfig(this.props))
            return { noStatus: "no status for template resources" };
        const info = manifest_support_1.getResourceInfo(this.props.kind);
        const statusQuery = info && info.statusQuery;
        if (!statusQuery)
            return { noStatus: "no status query defined for this kind" };
        if (!isResourcePropsWithConfig(this.props))
            throw new utils_1.InternalError("Resource config is undefined");
        try {
            return await statusQuery(this.props, observe, buildData);
        }
        catch (err) {
            // If there's only one GQL error and it's K8sNotFound, throw
            // that on up the stack. Otherwise, return a Status object.
            const orig = core_1.gqlGetOriginalErrors(err);
            if (orig && orig.length === 1 && orig[0].name === "K8sNotFound") {
                throw orig[0];
            }
            return core_1.errorToNoStatus(err);
        }
    }
    mountedElement() {
        return common_1.mountedElement(this.props);
    }
    manifest(deployID) {
        if (this.manifest_)
            return this.manifest_;
        const elem = this.mountedElement();
        const manifest = manifest_support_1.makeManifest(elem, deployID);
        const info = manifest_support_1.getResourceInfo(this.props.kind);
        this.manifest_ = info.makeManifest ? info.makeManifest(manifest, elem, deployID) : manifest;
        return this.manifest_;
    }
}
exports.Resource = Resource;
/**
 * Tests whether a ResourceProps is for a template object
 *
 * @public
 */
function isResourcePropsWithConfig(x) {
    return (!x.isTemplate) && (x.config !== undefined);
}
exports.isResourcePropsWithConfig = isResourcePropsWithConfig;
/**
 * Tests to see if an object is a {@link k8s.Resource} element
 * @param x - object to test
 * @returns true if object is an AdaptElement of type {@link k8s.Resource}
 *
 * @public
 */
function isResource(x) {
    return core_1.isElement(x) && x.componentType === Resource;
}
exports.isResource = isResource;
//# sourceMappingURL=Resource.js.map