"use strict";
/*
 * Copyright © 2020 Atomist, Inc.
 *
 * 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 });
exports.defaultImage = exports.repoSpecsKubernetsApplication = exports.dockerPort = exports.defaultKubernetesApplication = exports.getKubernetesGoalEventData = exports.generateKubernetesGoalEventData = exports.sdmPackK8s = void 0;
const logger_1 = require("@atomist/automation-client/lib/util/logger");
const dockerfileParser = require("docker-file-parser");
const stringify = require("json-stringify-safe");
const _ = require("lodash");
const sdmGoal_1 = require("../../../api-helper/goal/sdmGoal");
const projectVersioner_1 = require("../../../core/delivery/build/local/projectVersioner");
const name_1 = require("../kubernetes/name");
const namespace_1 = require("../support/namespace");
const fulfiller_1 = require("./fulfiller");
const goal_1 = require("./goal");
const spec_1 = require("./spec");
/**
 * JSON propery under the goal event data where the
 * [[KubernetesDeployment]] goal [[KubernetesApplication]] data are
 * stored, i.e., the value of `goal.data[sdmPackK8s]`.
 */
exports.sdmPackK8s = "@atomist/sdm-pack-k8s";
/**
 * Generate KubernetesApplication from goal, goal registration,
 * project, and goal invocation.  The priority of the various sources
 * of KubernetesApplication data are, from lowest to highest:
 *
 * 1.  Starting point is `{ ns: defaultNamespace }`
 * 2.  [[KubernetesDeployDataSources.SdmConfiguration]]
 * 3.  [[defaultKubernetesApplication]]
 * 4.  [[KubernetesDeployDataSources.Dockerfile]]
 * 5.  The various partial Kubernetes specs under `.atomist/kubernetes`
 * 6.  [[KubernetesDeployDataSources.GoalEvent]]
 * 7.  [[KubernetesDeployRegistration.applicationData]]
 *
 * Specific sources can be enabled/disabled via the
 * [[KubernetesDeployRegistration.dataSources]].
 *
 * @param k8Deploy Kubernetes deployment goal
 * @param registration Goal registration/configuration for `k8Deploy`
 * @param goalInvocation The Kubernetes deployment goal currently triggered.
 * @return The SdmGoalEvent augmented with [[KubernetesApplication]] in the data
 */
function generateKubernetesGoalEventData(k8Deploy, registration, goalInvocation) {
    return k8Deploy.sdm.configuration.sdm.projectLoader.doWithProject(Object.assign(Object.assign({}, goalInvocation), { readOnly: true }), async (p) => {
        const { context, goalEvent } = goalInvocation;
        const sdmConfigAppData = (registration.dataSources.includes(goal_1.KubernetesDeployDataSources.SdmConfiguration)) ?
            _.get(k8Deploy, "sdm.configuration.sdm.k8s.app", {}) : {};
        const defaultAppData = await defaultKubernetesApplication(goalEvent, k8Deploy, context);
        const dockerfileAppData = (registration.dataSources.includes(goal_1.KubernetesDeployDataSources.Dockerfile)) ?
            await dockerfileKubernetesApplication(p) : {};
        const specAppData = await repoSpecsKubernetsApplication(p, registration);
        const mergedAppData = _.merge({ ns: namespace_1.K8sDefaultNamespace }, sdmConfigAppData, defaultAppData, dockerfileAppData, specAppData);
        const eventAppData = {};
        eventAppData[exports.sdmPackK8s] = mergedAppData;
        if (registration.dataSources.includes(goal_1.KubernetesDeployDataSources.GoalEvent)) {
            const slug = goal_1.goalEventSlug(goalEvent);
            let eventData;
            try {
                eventData = sdmGoal_1.goalData(goalEvent);
            }
            catch (e) {
                logger_1.logger.warn(`Failed to parse goal event data for ${slug} as JSON: ${e.message}`);
                logger_1.logger.warn(`Ignoring current value of goal event data: ${goalEvent.data}`);
                eventData = {};
            }
            _.merge(eventAppData, eventData);
        }
        if (registration.applicationData) {
            const callbackAppData = await registration.applicationData(eventAppData[exports.sdmPackK8s], p, k8Deploy, goalEvent, context);
            eventAppData[exports.sdmPackK8s] = callbackAppData;
        }
        goalEvent.data = JSON.stringify(eventAppData);
        goalEvent.fulfillment = {
            method: "sdm",
            name: fulfiller_1.KubernetesDeployFulfillerGoalName,
            registration: registration.name,
        };
        return goalEvent;
    });
}
exports.generateKubernetesGoalEventData = generateKubernetesGoalEventData;
/**
 * Fetch [[KubernetesApplication]] from goal event.  If the goal event
 * does not contain Kubernetes application information in its data
 * property, `undefined` is returned.  If the value of the goal event
 * data cannot be parsed as JSON, an error is thrown.
 *
 * @param goalEvent SDM goal event to retrieve the Kubernetes application data from
 * @return Parsed [[KubernetesApplication]] object
 */
function getKubernetesGoalEventData(goalEvent) {
    let data;
    try {
        data = sdmGoal_1.goalData(goalEvent);
    }
    catch (e) {
        logger_1.logger.error(e.message);
        throw e;
    }
    return data[exports.sdmPackK8s];
}
exports.getKubernetesGoalEventData = getKubernetesGoalEventData;
/**
 * Given the goal event, [[KubernetesDeploy]] goal, and
 * handler context generate a default [[KubernetesApplication]] object.
 *
 * This function uses [[defaultImage]] to determine the image.
 *
 * @param goalEvent SDM Kubernetes deployment goal event
 * @param k8Deploy Kubernetes deployment goal configuration
 * @param context Handler context
 * @return a valid default KubernetesApplication for this SDM goal deployment event
 */
async function defaultKubernetesApplication(goalEvent, k8Deploy, context) {
    const workspaceId = context.workspaceId;
    const name = name_1.validName(goalEvent.repo.name);
    const image = await defaultImage(goalEvent, k8Deploy, context);
    return {
        workspaceId,
        name,
        image,
    };
}
exports.defaultKubernetesApplication = defaultKubernetesApplication;
/**
 * Parse Dockerfile and return port of first argument to the first
 * EXPOSE command.  it can suggessfully convert to an integer.  If
 * there are no EXPOSE commands or if it cannot successfully convert
 * the EXPOSE arguments to an integer, `undefined` is returned.
 *
 * @param p Project to look for Dockerfile in
 * @return port number or `undefined` if no EXPOSE commands are found
 */
async function dockerPort(p) {
    const dockerFile = await p.getFile("Dockerfile");
    if (dockerFile) {
        const dockerFileContents = await dockerFile.getContent();
        const commands = dockerfileParser.parse(dockerFileContents, { includeComments: false });
        const exposeCommands = commands.filter(c => c.name === "EXPOSE");
        for (const exposeCommand of exposeCommands) {
            if (Array.isArray(exposeCommand.args)) {
                for (const arg of exposeCommand.args) {
                    const port = parseInt(arg, 10);
                    if (!isNaN(port)) {
                        return port;
                    }
                }
            }
            else if (typeof exposeCommand.args === "string") {
                const port = parseInt(exposeCommand.args, 10);
                if (!isNaN(port)) {
                    return port;
                }
            }
            else {
                logger_1.logger.warn(`Unexpected EXPOSE argument type '${typeof exposeCommand.args}': ${stringify(exposeCommand.args)}`);
            }
        }
    }
    return undefined;
}
exports.dockerPort = dockerPort;
/** Package return of [[dockerPort]] in [[KubernetesApplication]]. */
async function dockerfileKubernetesApplication(p) {
    const port = await dockerPort(p);
    return (port) ? { port } : {};
}
/**
 * Read configured Kubernetes partial specs from repository.
 *
 * @param p Project to look for specs
 * @param registration Configuration of KubernetesDeploy goal
 * @return KubernetesApplication object with requested specs populated
 */
async function repoSpecsKubernetsApplication(p, r) {
    const deploymentSpec = (r.dataSources.includes(goal_1.KubernetesDeployDataSources.DeploymentSpec)) ?
        await spec_1.loadKubernetesSpec(p, "deployment") : undefined;
    const serviceSpec = (r.dataSources.includes(goal_1.KubernetesDeployDataSources.ServiceSpec)) ?
        await spec_1.loadKubernetesSpec(p, "service") : undefined;
    const ingressSpec = (r.dataSources.includes(goal_1.KubernetesDeployDataSources.IngressSpec)) ?
        await spec_1.loadKubernetesSpec(p, "ingress") : undefined;
    const roleSpec = (r.dataSources.includes(goal_1.KubernetesDeployDataSources.RoleSpec)) ?
        await spec_1.loadKubernetesSpec(p, "role") : undefined;
    const serviceAccountSpec = (r.dataSources.includes(goal_1.KubernetesDeployDataSources.ServiceAccountSpec)) ?
        await spec_1.loadKubernetesSpec(p, "service-account") : undefined;
    const roleBindingSpec = (r.dataSources.includes(goal_1.KubernetesDeployDataSources.RoleBindingSpec)) ?
        await spec_1.loadKubernetesSpec(p, "role-binding") : undefined;
    return {
        deploymentSpec,
        serviceSpec,
        ingressSpec,
        roleSpec,
        serviceAccountSpec,
        roleBindingSpec,
    };
}
exports.repoSpecsKubernetsApplication = repoSpecsKubernetsApplication;
/**
 * Remove any invalid characters from Docker image name component
 * `name` to make it a valid Docker image name component.  If
 * `hubOwner` is true, it ensures the name contains only alphanumeric
 * characters.
 *
 * From https://docs.docker.com/engine/reference/commandline/tag/:
 *
 * > An image name is made up of slash-separated name components,
 * > optionally prefixed by a registry hostname. The hostname must
 * > comply with standard DNS rules, but may not contain
 * > underscores. If a hostname is present, it may optionally be
 * > followed by a port number in the format :8080. If not present,
 * > the command uses Docker’s public registry located at
 * > registry-1.docker.io by default. Name components may contain
 * > lowercase letters, digits and separators. A separator is defined
 * > as a period, one or two underscores, or one or more dashes. A
 * > name component may not start or end with a separator.
 * >
 * > A tag name must be valid ASCII and may contain lowercase and
 * > uppercase letters, digits, underscores, periods and dashes. A tag
 * > name may not start with a period or a dash and may contain a
 * > maximum of 128 characters.
 *
 * @param name Name component to clean up.
 * @param hubOwner If `true` only allow characters valid for a Docker Hub user/org
 * @return Valid Docker image name component.
 */
function dockerImageNameComponent(name, hubOwner = false) {
    const cleanName = name.toLocaleLowerCase()
        .replace(/^[^a-z0-9]+/, "")
        .replace(/[^a-z0-9]+$/, "")
        .replace(/[^-_/.a-z0-9]+/g, "");
    if (hubOwner) {
        return cleanName.replace(/[^a-z0-9]+/g, "");
    }
    else {
        return cleanName;
    }
}
/**
 * Determine the best default value for the image property for this
 * Kubernetes deployment goal event.  If there is no image associated
 * with the after commit of the push, it checks if a Docker registry
 * is provided at `sdm.configuration.sdm.docker.registry` and uses
 * that and the repo name to return an image.  If neither of those
 * exist, a Docker Hub-like image name generated from the repository
 * owner and name.  In the latter two cases, it tries to read a
 * version for this commit from the graph.  If it exists it uses it at
 * the image tag.  If it does not, it uses the tag "latest".
 *
 * @param goalEvent SDM Kubernetes deployment goal event
 * @param k8Deploy Kubernetes deployment goal object
 * @param context Handler context
 * @return Docker image associated with goal push after commit, or best guess
 */
async function defaultImage(goalEvent, k8Deploy, context) {
    if (goalEvent.push && goalEvent.push.after && goalEvent.push.after.images && goalEvent.push.after.images.length > 0) {
        return goalEvent.push.after.images[0].imageName;
    }
    const slug = goal_1.goalEventSlug(goalEvent);
    let version;
    try {
        version = await projectVersioner_1.readSdmVersion(goalEvent.repo.owner, goalEvent.repo.name, goalEvent.repo.providerId, goalEvent.sha, goalEvent.branch, context);
    }
    catch (e) {
        logger_1.logger.warn(`Failed to read version for goal ${slug}:${goalEvent.sha}: ${e.message}`);
    }
    if (!version) {
        version = "latest";
    }
    const tag = version.replace(/^[-.]/, "").replace(/[^-.\w]+/, "").substring(0, 128);
    const dockerName = dockerImageNameComponent(goalEvent.repo.name);
    const registry = _.get(k8Deploy, "sdm.configuration.sdm.build.docker.registry", dockerImageNameComponent(goalEvent.repo.owner, true));
    return `${registry}/${dockerName}:${tag}`;
}
exports.defaultImage = defaultImage;
//# sourceMappingURL=data.js.map