"use strict";
/*
 * Copyright 2019-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 utils_1 = require("@adpt/utils");
const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
const json_stable_stringify_1 = tslib_1.__importDefault(require("json-stable-stringify"));
const path = tslib_1.__importStar(require("path"));
const action_1 = require("../action");
const cli_1 = require("./cli");
const image_ref_1 = require("./image-ref");
/**
 * Locally builds a docker image
 *
 * @remarks
 * See {@link docker.LocalDockerImageProps}.
 *
 * @public
 */
class LocalDockerImage extends action_1.Action {
    constructor(props) {
        super(props);
        // Even though we have a custom deployedWhen, don't normally show
        // status from this component, unless there's an active action.
        this.deployedWhenIsTrivial = true;
        /** @internal */
        this.deployedWhen = (goalStatus) => {
            if (goalStatus === core_1.DeployStatus.Destroyed)
                return true;
            if (this.buildComplete())
                return true;
            if (this.state.imagePropsJson &&
                this.state.imagePropsJson !== this.imagePropsJson) {
                return core_1.waiting("Waiting for Docker image to be re-built");
            }
            return core_1.waiting("Waiting for Docker image to be built");
        };
        this.options_ = Object.assign({}, LocalDockerImage.defaultProps.options, (props.options || {}));
        if (!props.dockerfile && !props.dockerfileName) {
            throw new Error(`LocalDockerImage: one of dockerfile or ` +
                `dockerfileName must be given`);
        }
    }
    /*
     * Public instance properties/methods
     */
    buildComplete() { return this.image() != null; }
    ready() { return this.buildComplete(); }
    image() {
        if (this.image_ == null) {
            if (this.state.image != null &&
                // Ensure we've rebuilt at least once this OpID
                this.state.deployOpID === this.deployInfo.deployOpID &&
                // And ensure the current build matches current props
                this.state.imagePropsJson === this.imagePropsJson) {
                this.image_ = this.state.image;
            }
        }
        return this.image_;
    }
    async pushTo(registryUrl, newPathTag) {
        const source = this.latestImage();
        if (!source)
            return undefined;
        newPathTag = newPathTag || source.pathTag;
        if (!newPathTag) {
            throw new Error(`Unable to push image to registry: path and tag ` +
                `not set for image '${source.ref}' and new path and tag not ` +
                `provided. Set path and tag on this image with 'options.imageName' ` +
                `and 'options.imageTag' or 'output.uniqueTag'. Or use a new ` +
                `path and tag, probably via the 'newPathTag' prop on ` +
                `RegistryDockerImage.`);
        }
        const dest = image_ref_1.mutableImageRef({
            id: source.id,
        });
        dest.pathTag = newPathTag;
        dest.domain = registryUrl;
        const destRef = dest.registryTag;
        if (!destRef) {
            throw new utils_1.InternalError(`Unable to push image to registry: ` +
                `destination reference '${dest.ref}' has no registryTag`);
        }
        const globals = cli_1.pickGlobals(this.options_);
        await cli_1.dockerTag(Object.assign({ existing: source.id, newTag: destRef }, globals));
        await cli_1.dockerPush(Object.assign({ nameTag: destRef }, globals));
        const final = dest.freeze();
        if (!image_ref_1.isImageRefRegistryWithId(final)) {
            throw new utils_1.InternalError(`Final pushed image '${final.ref}' is not ` +
                `a complete registry image with ID`);
        }
        return final;
    }
    latestImage() {
        return this.image_ || this.state.image;
    }
    /**
     * User-facing name to display in status messages.
     */
    displayName() { return this.options_.imageName; }
    /**
     * Implementations for Action base class
     * @internal
     */
    async shouldAct(op) {
        let imgName = this.options_.imageName || "";
        if (imgName)
            imgName = ` '${imgName}'`;
        if (op === core_1.ChangeType.delete)
            return false;
        if (this.buildComplete())
            return false;
        return {
            act: true,
            detail: `Building Docker image${imgName}`,
        };
    }
    /**
     * Implementations for Action base class
     * @internal
     */
    async action(op, ctx) {
        const options = Object.assign({}, this.options_, { deployID: ctx.buildData.deployID });
        const prevUniqueNameTag = this.state.prevUniqueNameTag;
        if (op === core_1.ChangeType.delete) {
            throw new utils_1.InternalError(`Delete action should not happen due to check in shouldAct`);
        }
        let dockerfile = this.props.dockerfile;
        if (!dockerfile) {
            if (!this.props.dockerfileName) {
                throw new utils_1.InternalError(`dockerfileName should not be null`);
            }
            dockerfile = (await fs_extra_1.default.readFile(this.props.dockerfileName)).toString();
        }
        const stages = this.props.stages || [];
        const image = await cli_1.withFilesImage(this.props.files, options, async (img) => {
            if (img && img.nameTag)
                stages.push({ image: img.nameTag, name: "files" });
            const stageConfig = stages
                .map((s) => `FROM ${s.image} as ${s.name}`)
                .join("\n");
            if (stageConfig) {
                dockerfile = `${stageConfig}\n\n${dockerfile}`;
            }
            let contextDir = this.props.contextDir || ".";
            contextDir = path.resolve(contextDir);
            return cli_1.dockerBuild("-", contextDir, Object.assign({}, options, { prevUniqueNameTag, stdin: dockerfile }));
        });
        this.image_ = image;
        this.setState({
            deployOpID: this.deployInfo.deployOpID,
            image,
            imagePropsJson: this.imagePropsJson,
            prevUniqueNameTag: options.uniqueTag ? image.nameTag : undefined,
        });
    }
    /*
     * Component methods
     */
    /** @internal */
    initialState() { return {}; }
    /** @internal */
    get imagePropsJson() {
        if (!this.imagePropsJson_) {
            const _a = this.props, { handle: _h, key } = _a, imageProps = tslib_1.__rest(_a, ["handle", "key"]);
            this.imagePropsJson_ = json_stable_stringify_1.default(imageProps);
        }
        return this.imagePropsJson_;
    }
}
LocalDockerImage.defaultProps = {
    options: {
        dockerHost: process.env.DOCKER_HOST,
        forceRm: true,
    },
};
exports.LocalDockerImage = LocalDockerImage;
//# sourceMappingURL=LocalDockerImage.js.map