Potherca
10/31/2015 - 8:49 PM

Script to setup and install the KunstmaanBundlesCMS to Heroku.

Script to setup and install the KunstmaanBundlesCMS to Heroku.

#!/usr/bin/env bash

set -o nounset # exit on use of an uninitialised variable, same as -u
set -o errexit # exit on all and any errors, same as -e

readonly g_sVendor=$(echo "$USER" | tr '[:upper:]' '[:lower:]')
readonly g_sProject="${g_sVendor}-kunstmaan"
readonly g_sSource='kunstmaan/bundles-standard-edition'
readonly sPhpMemoryLimit='2048M'

# ==============================================================================
function printTopic() {
# ------------------------------------------------------------------------------
    echo
    echo "=====> $*"
}
# ==============================================================================


# ==============================================================================
function printStatus() {
# ------------------------------------------------------------------------------
    echo "-----> $*"
}
# ==============================================================================


# ==============================================================================
function setSymfonyDatabaseVariablesFromUrl() {
# ------------------------------------------------------------------------------
    local aParts
    local sCredentials
    local sHost
    local sPass
    local sUrl
    local sName
    local sUser

    sUrl="$1"

    sUrl=${sUrl#*'://'}  # will drop begin of string upto first occur of `SubStr`
    sUrl=${sUrl%'?'*}    # will drop part of string from last occur of `SubStr` to the end

    aParts=(${sUrl//@/ })

    sCredentials=${aParts[0]}
    aParts=(${aParts[1]//\// })

    sHost=${aParts[0]}
    # @FIXME: Check $sHost for ':' and split into sHost/sPort
    sName=${aParts[1]}

    aParts=(${sCredentials//:/ })

    sUser=${aParts[0]}
    sPass=${aParts[1]}

    # @TODO: Set PORT if present
    heroku config:set \
        SYMFONY__DATABASE__USER="${sUser}" \
        SYMFONY__DATABASE__PASSWORD="${sPass}" \
        SYMFONY__DATABASE__HOST="${sHost}" \
        SYMFONY__DATABASE__NAME="${sName}"
    }
# ==============================================================================


# ==============================================================================
function validateHerokuSetup() {
# ------------------------------------------------------------------------------
    printTopic 'Validating Heroku Setup'

    $(command -v "heroku" >/dev/null 2>&1) &&  printStatus 'Heroku toolbelt is installed' || (open 'https://toolbelt.heroku.com/' && exit)

    #echo -e '\nDo you need to sign up for an Heroku account? (y/n) '
    #read -s -n 1 -p '' sReply
    #
    #if [ "${sReply}" == "y" ] ; then
    #     open 'https://signup.heroku.com/php' && exit
    #else
        printStatus 'Validating Heroku Credentials'
        heroku auth:whoami || heroku login
    #fi
}
# ==============================================================================


# ==============================================================================
function createProject() {
# ------------------------------------------------------------------------------
    printTopic "Creating Project ${g_sProject}"

    if [ -f './composer.phar' ];then
        printStatus 'Composer PHAR file available'
    else
        printStatus 'Fetching Composer PHAR file...'
        curl -sS https://getcomposer.org/installer | php
    fi

    printStatus "Installing ${g_sSource}. This may take a while..."

    php -d memory_limit="${sPhpMemoryLimit}" composer.phar create-project --no-interaction "${g_sSource}" "${g_sProject}"

}
# ==============================================================================


# ==============================================================================
function commitProject() {
# ------------------------------------------------------------------------------
    printTopic 'Committing Project'

    git init
    git add .
    git commit -m "Clean install of '${g_sSource}'."
}
# ==============================================================================


# ==============================================================================
function addPhpExtensions() {
# ------------------------------------------------------------------------------
    printStatus 'Adding PHP Extensions to Composer'

    # @CHECKMEL Is there a way of add extendsion to composer.json without having to run `composer require` locally?
    php -d memory_limit="${sPhpMemoryLimit}" ../composer.phar require 'ext-apcu' '*'
    php -d memory_limit="${sPhpMemoryLimit}" ../composer.phar require 'ext-mbstring' '*'
#    php -d memory_limit="${sPhpMemoryLimit}" ../composer.phar require 'ext-intl' '*'

    printStatus 'Committing to local repository'
    git add 'composer.*'
    git commit -m 'Adds PHP Extensions as composer dependency.'
}
# ==============================================================================


# ==============================================================================
function createHerokuApp() {
# ------------------------------------------------------------------------------
    printTopic 'Creating Heroku App'
    heroku apps:create "${g_sProject}" --region 'eu'
    heroku config:set SYMFONY_ENV='prod'

    addBuildPacks
    addAddons
    addProcFile
}
# ==============================================================================


# ==============================================================================
function addAddons() {
# ------------------------------------------------------------------------------
    printTopic 'Adding Addons to Heroku'
    # @TODO: PostGreSQL -- heroku addons:create heroku-postgresql:hobby-dev
    # MySQL
    heroku addons:create cleardb:ignite
    # ElasticSearch
    #heroku addons:create searchbox:starter
}
# ==============================================================================


# ==============================================================================
function addBuildPacks() {
# ------------------------------------------------------------------------------
    printTopic 'Adding buildpacks to Heroku'
    heroku buildpacks:set 'https://github.com/heroku/heroku-buildpack-multi'

    printStatus 'Creating buildpack file.'
#    echo 'https://github.com/heroku/heroku-buildpack-ruby' >> '.buildpacks'
    echo 'https://github.com/heroku/heroku-buildpack-nodejs' >> '.buildpacks'
    echo 'https://github.com/heroku/heroku-buildpack-php' >> '.buildpacks'

    printStatus 'Committing to local repository'
    git add '.buildpacks'
    git commit -m "Creates buildpack file for Heroku."

}
# ==============================================================================


# ==============================================================================
function addProcFile() {
# ------------------------------------------------------------------------------
    printTopic 'Adding Procfile for Heroku'
    echo "web: bin/heroku-php-apache2 web/" > Procfile

    printStatus 'Committing to local repository'
    git add 'Procfile'
    git commit -m "Creates Procfile for Heroku."
}
# ==============================================================================


# ==============================================================================
function patchDatabaseConfig() {
# ------------------------------------------------------------------------------
    local sPath

    sPath='app/config/parameters.yml.dist'
    printTopic "Adjusting the database configuration in ${sPath}"

    # the `-i.bak` is a workaround for OSX :-(
    sed -i.bak 's/database_host:              127.0.0.1/database_host:              %database.host%/g' "${sPath}"
    sed -i.bak 's/database_name:              kunstmaanbundles/database_name:              %database.name%/g' "${sPath}"
    sed -i.bak 's/database_password:          ~/database_password:          %database.password%/g' "${sPath}"
    sed -i.bak 's/database_user:              travis/database_user:              %database.user%/g' "${sPath}"
    # @TODO: sed -i.bak 's/database_port:              ~/database_port:              %database.port%/g' "${sPath}"

    rm "${sPath}.bak"

    printStatus 'Committing to local repository'
    git add "${sPath}"
    git commit -m "Adjusts database configuration for Heroku."


    sPath='app/config/parameters.yml'
    printTopic "Adjusting the database configuration in ${sPath}"

    # @TODO: Either get the config once and parse it or "remember" the values from where they were set

    sed -i.bak "s/database_host: 127.0.0.1/database_host: $(heroku config:get SYMFONY__DATABASE__HOST)/g" "${sPath}"
    sed -i.bak "s/database_name: kunstmaanbundles/database_name: $(heroku config:get SYMFONY__DATABASE__NAME)/g" "${sPath}"
    sed -i.bak "s/database_password: null/database_password: $(heroku config:get SYMFONY__DATABASE__PASSWORD)/g" "${sPath}"
    sed -i.bak "s/database_user: travis/database_user: $(heroku config:get SYMFONY__DATABASE__USER)/g" "${sPath}"
    #sed -i.bak "s/database_port: null/database_port: $(heroku config:get SYMFONY__DATABASE__PORT)/g" "${sPath}"
}
# ==============================================================================


# ==============================================================================
function patchLogConfig() {
# ------------------------------------------------------------------------------
    printTopic 'Adjusting the log configuration'

    # the `-i.bak` is a workaround for OSX :-(
    sed -i.bak 's#path:  "%kernel.logs_dir%/%kernel.environment%.log"#path: "php://stderr"#g' 'app/config/config_prod.yml'
    rm 'app/config/config_prod.yml.bak'

    printStatus 'Committing to local repository'
    git add 'app/config/config_prod.yml'
    git commit -m "Adjusts log configuration for Heroku."
}
# ==============================================================================


# ==============================================================================
function deploy() {
# ------------------------------------------------------------------------------
    git push heroku master
}
# ==============================================================================


# ==============================================================================
function createBundle() {
# ------------------------------------------------------------------------------
    local sVendor

    sVendor="$(echo ${g_sVendor:0:1} | tr '[a-z]' '[A-Z]')${g_sVendor:1}"

    printTopic "Generating ${sVendor}\WebsiteBundle"
    app/console kuma:generate:bundle --no-interaction --namespace="${sVendor}\\WebsiteBundle" --dir=src/
    printStatus 'Committing to local repository'
    git add .
    git commit -m "Adds Website Bundle."

    printTopic 'Generating Site'
    app/console kuma:generate:default-site --no-interaction
    #@TODO: Fix broken fixture mapping and generate using the `--demosite` flag
    printStatus 'Committing to local repository'
    git add .
    git commit -m "Adds Site."
}
# ==============================================================================


# ==============================================================================
function openAdminPanel() {
# ------------------------------------------------------------------------------
    local sUrl="$(heroku apps:info | grep -oh 'https://.*')"
    open "${sUrl}en/admin/"
}
# ==============================================================================


# ==============================================================================
function initialiseDatabase() {
# ------------------------------------------------------------------------------
#    app/console doctrine:database:create
    printTopic 'Initialising Database'

    printStatus 'Creating Schema'
    php app/console doctrine:schema:create

    printStatus 'Loading Fixtures'
    # @TODO: Catch output and grep for admin user/password
    php app/console doctrine:fixtures:load --no-interaction
}
# ==============================================================================


# ==============================================================================
function patchComposer() {
# ------------------------------------------------------------------------------
    printTopic 'Adding compile scripts to composer'

    #    heroku run 'npm install -g bower'
    #    heroku run 'npm install -g gulp'
    #    heroku run 'npm install -g uglify-js'
    #    heroku run 'npm install -g uglifycss
    # @TODO: Validate these ara all run:
    #    heroku run 'bundle install'
    #    heroku run 'npm install'
    #    heroku run 'bower install'
    #
    #    heroku run 'gulp build'

    sPath='composer.json'
    sReplacement='"scripts": {\
        "compile": [\
            "app/console assets:install",\
            "php app/console assetic:dump --env=prod --no-debug"\
        ],'

    sed -i.bak "s#\"scripts\": {#${sReplacement}#g" "${sPath}"

    printStatus 'Committing to local repository'
    git add .
    git commit -m "Adds Site."
}
# ==============================================================================


# ==============================================================================
function patchPackageJon() {
# ------------------------------------------------------------------------------
    printTopic 'Patching package.json'

    sPath='package.json'
    sReplacement='"dependencies": {\
        "bower": "*",\
        "gulp": "*",\
        "uglifycss": "*",\
        "uglify-js": "*"\
    },\
    "devDependencies": {'

    sed -i.bak "s#\"devDependencies\": {#${sReplacement}#g" "${sPath}"

    printStatus 'Committing to local repository'
    git add .
    git commit -m "Adds Node dependencies."
}
# ==============================================================================


# ==============================================================================
function run() {
# ------------------------------------------------------------------------------
    validateHerokuSetup
    createProject

    cd "${g_sProject}"

    commitProject

    createHerokuApp

    # @CHECKME: Would using DATABAS_URL work?
    # heroku config:set $(heroku config --shell | grep CLEARDB_DATABASE_URL | sed -e 's/^CLEARDB_//')
    setSymfonyDatabaseVariablesFromUrl "$(heroku config:get CLEARDB_DATABASE_URL)"

    patchLogConfig
    patchDatabaseConfig
    patchComposer
    addPhpExtensions
    patchPackageJon

    createBundle
    initialiseDatabase


    deploy

    openAdminPanel

    heroku logs -t
}
# ==============================================================================

run

#EOF

For a hack-session at my current employer we thought it would be fun to see how quickly/easily we could get a version of the KunstMaan CMS deployed on Heroku.

This was in part to evaluate the CMS, but also to familiarize people with Heroku. In order to reproduce the process, I settled for creating a bash script and updating it with each new step I went through.

How hard could it be, right? Turns out that things weren't as simple as one might think.

The CMS does try to make it as easy as possible to get up and running. It offers a clean setup that can be gotten by running the composer create-project command.

From there, a few steps are needed to be made to get things in order to run on Heroku:

  • A Procfile needs to be created to tell Heroku which HTTP server to run on the web dyno (and which directory to serve documents from).
  • The log setting needs to be changed to send the output to the Heroku log (instead of log to file).
  • The SYMFONY_ENV variable needs to be added to the Heroku environment
  • APC and mbstring needs to be added as a dependency in the composer.json so Heroku will load them.

Creating an app on Heroku and adding a database was trivial.

The first hurdle, though Heroku provides an URL for a database (through an environmental variable), was that the Symfony framework wants to be fed each piece of that connection string (user, password, host, name) individually. It doesn't take much work to get this in order, but it is a bit of a shame that Symfony is not smart enough to simply accept a connection URL.

Some digging around brought to light that there is a way of setting just the URL, so this might be worth looking into at a later date.

To get things up and running, some commands had to be run locally against the remote database.

The next thing was that all sorts of front-end assets need to be generated. It doesn't do to generate these on Heroku but generating these locally and committing them to the repository is also a no-go.

As this will require more looking into how both Heroku and Symfony work, this will also have to wait until a later date.