cf-node-client
momo auto_awesome BUY CLAUDE KIT WITH 20% OFF coffee BUY ME COFFEE

Source: model/cloudcontroller/AppsDeployment.js

"use strict";

const CloudControllerBase = require("./CloudControllerBase");
const HttpUtils = require("../../utils/HttpUtils");
const fs = require("fs");

/**
 * AppsDeployment — deployment, runtime info, and service-binding operations.
 * Methods: upload, getStats, getInstances, getAppRoutes, associateRoute,
 *          getServiceBindings, removeServiceBindings, getEnvironmentVariables,
 *          setEnvironmentVariables, restage, getDroplets, getPackages, getProcesses
 *
 * @class
 */
class AppsDeployment extends CloudControllerBase {

    constructor(endPoint, options = {}) {
        super(endPoint, options);
    }

    /**
     * Upload application source code.
     * @param {String} appGuid
     * @param {String} filePath - Local file path to zip
     * @param {Boolean} [async=false]
     * @return {Promise}
     */
    upload(appGuid, filePath, async) {
        const stats = fs.statSync(filePath);
        const fileSizeInBytes = stats.size;
        const asyncFlag = typeof async === "boolean" && async;

        if (this.isUsingV3()) {
            return this._uploadV3(appGuid, filePath, fileSizeInBytes, asyncFlag);
        }
        return this._uploadV2(appGuid, filePath, fileSizeInBytes, asyncFlag);
    }

    /** @private */
    _uploadV2(appGuid, filePath, fileSizeInBytes, asyncFlag) {
        const url = `${this.API_URL}/v2/apps/${appGuid}/bits`;
        const options = {
            multipart: true,
            accessToken: this.UAA_TOKEN.access_token,
            query: { guid: appGuid, async: asyncFlag },
            data: {
                resources: JSON.stringify([]),
                application: HttpUtils.file(filePath, "application/zip", fileSizeInBytes)
            }
        };
        return this.REST.upload(url, options, this.HttpStatus.CREATED, false);
    }

    /** @private */
    _uploadV3(appGuid, filePath, fileSizeInBytes, asyncFlag) {
        const self = this;
        const token = this.getAuthorizationHeader();

        // Step 1: Create a package of type "bits" linked to the app
        const createOpts = {
            method: "POST",
            url: `${this.API_URL}/v3/packages`,
            headers: { Authorization: token, "Content-Type": "application/json" },
            body: JSON.stringify({
                type: "bits",
                relationships: { app: { data: { guid: appGuid } } }
            })
        };

        return this.REST.request(createOpts, this.HttpStatus.CREATED, true)
            .then(function (pkg) {
                // Step 2: Upload bits to the newly created package
                const uploadUrl = `${self.API_URL}/v3/packages/${pkg.guid}/upload`;
                const uploadOpts = {
                    method: "POST",
                    multipart: true,
                    accessToken: self.UAA_TOKEN.access_token,
                    query: { async: asyncFlag },
                    data: {
                        bits: HttpUtils.file(filePath, "application/zip", fileSizeInBytes)
                    }
                };
                return self.REST.upload(uploadUrl, uploadOpts, self.HttpStatus.OK, true);
            });
    }

    /**
     * Get detailed stats for a started app.
     * @param {String} appGuid
     * @return {Promise}
     */
    getStats(appGuid) {
        const token = this.getAuthorizationHeader();
        // v3: stats are per-process; use the "web" process shortcut
        const url = this.isUsingV3()
            ? `${this.API_URL}/v3/apps/${appGuid}/processes/web/stats`
            : `${this.API_URL}/v2/apps/${appGuid}/stats`;
        const options = { method: "GET", url, headers: { Authorization: token } };
        return this.REST.request(options, this.HttpStatus.OK, true);
    }

    /**
     * Get instance information.
     * @param {String} appGuid
     * @return {Promise}
     */
    getInstances(appGuid) {
        const token = this.getAuthorizationHeader();
        const url = this.isUsingV3()
            ? `${this.API_URL}/v3/apps/${appGuid}/processes`
            : `${this.API_URL}/v2/apps/${appGuid}/instances`;
        const options = { method: "GET", url, headers: { Authorization: token } };
        return this.REST.request(options, this.HttpStatus.OK, true);
    }

    /**
     * Get routes for an app.
     * @param {String} appGuid
     * @return {Promise}
     */
    getAppRoutes(appGuid) {
        const token = this.getAuthorizationHeader();
        const url = this.isUsingV3()
            ? `${this.API_URL}/v3/apps/${appGuid}/routes`
            : `${this.API_URL}/v2/apps/${appGuid}/routes`;
        const options = { method: "GET", url, headers: { Authorization: token } };
        return this.REST.request(options, this.HttpStatus.OK, true);
    }

    /**
     * Associate a route with an app.
     * @param {String} appGuid
     * @param {String} routeGuid
     * @return {Promise}
     */
    associateRoute(appGuid, routeGuid) {
        const token = this.getAuthorizationHeader();
        if (this.isUsingV3()) {
            // v3: map app to route via destinations endpoint
            const options = {
                method: "POST",
                url: `${this.API_URL}/v3/routes/${routeGuid}/destinations`,
                headers: { Authorization: token, "Content-Type": "application/json" },
                body: JSON.stringify({ destinations: [{ app: { guid: appGuid } }] })
            };
            return this.REST.request(options, this.HttpStatus.OK, true);
        }
        const options = {
            method: "PUT",
            url: `${this.API_URL}/v2/apps/${appGuid}/routes/${routeGuid}`,
            headers: { Authorization: token }
        };
        return this.REST.request(options, this.HttpStatus.CREATED, true);
    }

    /**
     * Get service bindings for an app.
     * @param {String} appGuid
     * @param {Object} [filter]
     * @return {Promise}
     */
    getServiceBindings(appGuid, filter) {
        const token = this.getAuthorizationHeader();
        if (this.isUsingV3()) {
            // v3: no nested route; use top-level endpoint with app_guids filter
            const qs = Object.assign({ app_guids: appGuid }, filter || {});
            const options = {
                method: "GET",
                url: `${this.API_URL}/v3/service_credential_bindings`,
                headers: { Authorization: token, "Content-Type": "application/json" },
                qs: qs
            };
            return this.REST.request(options, this.HttpStatus.OK, true);
        }
        const url = `${this.API_URL}/v2/apps/${appGuid}/service_bindings`;
        const options = { method: "GET", url, headers: { Authorization: token }, qs: filter || {} };
        return this.REST.request(options, this.HttpStatus.OK, true);
    }

    /**
     * Remove a service binding from an app.
     * v3: DELETE /v3/service_credential_bindings/:guid → 202 (managed, async) or 204 (key/UPS, sync)
     * v2: DELETE /v2/apps/:guid/service_bindings/:guid → 204
     * @param {String} appGuid
     * @param {String} serviceBindingGuid
     * @return {Promise}
     */
    removeServiceBindings(appGuid, serviceBindingGuid) {
        const token = this.getAuthorizationHeader();
        if (this.isUsingV3()) {
            const options = {
                method: "DELETE",
                url: `${this.API_URL}/v3/service_credential_bindings/${serviceBindingGuid}`,
                headers: { "Content-Type": "application/json", Authorization: token }
            };
            // Managed bindings return 202 (async job); key/UPS bindings return 204
            return this.REST.request(options, [this.HttpStatus.ACCEPTED, this.HttpStatus.NO_CONTENT], false);
        }
        const options = {
            method: "DELETE",
            url: `${this.API_URL}/v2/apps/${appGuid}/service_bindings/${serviceBindingGuid}`,
            headers: { "Content-Type": "application/x-www-form-urlencoded", Authorization: token }
        };
        return this.REST.request(options, this.HttpStatus.NO_CONTENT, false);
    }

    /**
     * Get environment variables for an app.
     * @param {String} appGuid
     * @return {Promise}
     */
    getEnvironmentVariables(appGuid) {
        const token = this.getAuthorizationHeader();
        const url = this.isUsingV3()
            ? `${this.API_URL}/v3/apps/${appGuid}/environment_variables`
            : `${this.API_URL}/v2/apps/${appGuid}/env`;
        const options = { method: "GET", url, headers: { Authorization: token } };
        return this.REST.request(options, this.HttpStatus.OK, true);
    }

    /**
     * Set environment variables for an app.
     * NOTE: v2 path calls this.update() — only works when mixed into Apps facade.
     * @param {String} appGuid
     * @param {Object} variables
     * @return {Promise}
     */
    setEnvironmentVariables(appGuid, variables) {
        const token = this.getAuthorizationHeader();
        if (this.isUsingV3()) {
            const options = {
                method: "PATCH",
                url: `${this.API_URL}/v3/apps/${appGuid}/environment_variables`,
                headers: { "Content-Type": "application/json", Authorization: token },
                body: JSON.stringify({ var: variables })
            };
            return this.REST.request(options, this.HttpStatus.OK, true);
        }
        // v2 path — delegates to update() which comes from AppsCore via Apps facade
        if (typeof this.update !== "function") {
            throw new Error(
                "setEnvironmentVariables() v2 requires the Apps facade (mixin with AppsCore). " +
                "Use the Apps class instead, or switch to v3 mode."
            );
        }
        return this.update(appGuid, { environment_json: variables });
    }

    /**
     * Restage an application (v2 only).
     * @param {String} appGuid
     * @return {Promise}
     */
    restage(appGuid) {
        if (this.isUsingV3()) {
            throw new Error("Restage is not directly supported in v3 API. Use package/droplet endpoints instead.");
        }
        const token = this.getAuthorizationHeader();
        const options = {
            method: "POST",
            url: `${this.API_URL}/v2/apps/${appGuid}/restage`,
            headers: { Authorization: token }
        };
        return this.REST.request(options, this.HttpStatus.CREATED, true);
    }

    /**
     * Get droplets for an app (v3 only).
     * @param {String} appGuid
     * @return {Promise}
     */
    getDroplets(appGuid) {
        if (!this.isUsingV3()) throw new Error("getDroplets is only available in v3 API");
        const token = this.getAuthorizationHeader();
        const options = {
            method: "GET",
            url: `${this.API_URL}/v3/apps/${appGuid}/droplets`,
            headers: { Authorization: token }
        };
        return this.REST.request(options, this.HttpStatus.OK, true);
    }

    /**
     * Get packages for an app (v3 only).
     * @param {String} appGuid
     * @return {Promise}
     */
    getPackages(appGuid) {
        if (!this.isUsingV3()) throw new Error("getPackages is only available in v3 API");
        const token = this.getAuthorizationHeader();
        const options = {
            method: "GET",
            url: `${this.API_URL}/v3/apps/${appGuid}/packages`,
            headers: { Authorization: token }
        };
        return this.REST.request(options, this.HttpStatus.OK, true);
    }

    /**
     * Get processes for an app (v3 only).
     * @param {String} appGuid
     * @return {Promise}
     */
    getProcesses(appGuid) {
        if (!this.isUsingV3()) throw new Error("getProcesses is only available in v3 API");
        const token = this.getAuthorizationHeader();
        const options = {
            method: "GET",
            url: `${this.API_URL}/v3/apps/${appGuid}/processes`,
            headers: { Authorization: token }
        };
        return this.REST.request(options, this.HttpStatus.OK, true);
    }
}

module.exports = AppsDeployment;