cloudamatic/mu

View on GitHub
bin/mu-deploy

Summary

Maintainability
Test Coverage
#!/usr/local/ruby-current/bin/ruby
# Copyright:: Copyright (c) 2014 eGlobalTech, Inc., all rights reserved
#
# Licensed under the BSD-3 license (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License in the root of the project or at
#
#     http://egt-labs.com/mu/LICENSE.html
#
# 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.


require File.realpath(File.expand_path(File.dirname(__FILE__)+"/mu-load-config.rb"))
# now we have our global config available as the read-only hash $MU_CFG

require 'rubygems'
require 'bundler/setup'
require 'json'
require 'erb'
require 'optimist'
require 'json-schema'
require 'mu'

$opts = Optimist::options do
  banner <<-EOS
Usage:
#{$0} [-e <environment>] [-r region] [-v] [-d] [-w] [-c] [-n] [-s] [-j] [-p parameter=value] /path/to/stack/config.[json|yaml] [-u deploy_id [-l]] [-r deploy_id]
  EOS
  opt :environment, "Environment to set on creation.", :require => false, :default => "dev"
  opt :region, "Default region for newly-created cloud resources.", :require => false, :default => MU.myRegion, :type => :string
  opt :nocleanup, "Skip cleaning up resources on failed deployments. Used for debugging.", :require => false, :default => false
  opt :web, "Generate web-friendly (HTML) output.", :require => false, :default => false, :type => :boolean
  opt :dryrun, "Do not build a stack, only run configuration validation.", :require => false, :default => false, :type => :boolean
  opt :skipinitialupdates, "Node bootstrapping normally runs an internal recipe that does a full system update. This disables that behavior.", :require => false, :default => false, :type => :boolean
  opt :parameter, "Pass a parameter to the configuration parser (Name=Value). This will be presented to your config file as the ERB variable $Name.", :require => false, :type => :string, :multi => true
  opt :update, "Update the stored configuration of an existing deployment, instead of creating a new deploy.", :require => false, :type => :string
  opt :cloud, "Pass a parameter named 'cloud' to the stack, and set it as the default cloud platform for all resources if not explicitly declared. Must be one of: #{MU::Cloud.availableClouds.join(", ")}", :require => false, :type => :string
  opt :cloudformation, "Emit Amazon Web Services targets into a CloudFormation template instead of building real services.", :require => false, :default => false, :type => :boolean
  opt :cloudformation_output, "When emitting a CloudFormation template, put the final product in this location instead of in /tmp. Takes a local file path or an s3:// URI. S3 uploads will be given AUTHENTICATED-READ permissions.", :require => false, :type => :string
  opt :verbose, "Display debugging output.", :require => false, :default => false, :type => :boolean
  opt :quiet, "Display minimal output.", :require => false, :default => false, :type => :boolean
  opt :color, "Display log output in human-friendly colors.", :require => false, :default => true, :type => :boolean
  opt :credentials, "Set the default credential set to use for resources which do not specify a default", :require => false, :type => :string
end
verbosity = MU::Logger::NORMAL
verbosity = MU::Logger::LOUD if $opts[:verbose]
verbosity = MU::Logger::QUIET if $opts[:quiet]

if $opts[:verbose] and $opts[:quiet]
  MU.log "Cannot set both --verbose and --quiet", MU::ERR
  exit 1
end

if $opts[:liveupdate] and !$opts[:update]
  MU.log "--liveupdate only valid when combined with --update", MU::ERR
  exit 1
end

MU.setVar("curRegion", $opts[:region]) if $opts[:region]
MU.setLogging(verbosity, $opts[:web], STDOUT, $opts[:color])

# Parse any paramater options into something useable.
params = Hash.new
$opts[:parameter].each { |param|
  name, value = param.split(/\s*=\s*/, 2)
  params[name] = value
}

# We want our config files (which can be ERB templates) to have this variable
# available to them.
$environment = $opts[:environment]

if !ARGV[0] or ARGV[0].empty?
  MU.log("You must specify a stack configuration file!", MU::ERR, html: $opts[:web])
  exit 1
end

begin
  config = File.realdirpath(ARGV[0])
  File.read(config)
rescue Errno::ENOENT => e
  MU.log "#{e.message}", MU::ERR, html: $opts[:web]
  exit 1
end


MU.log "Loading #{config}", html: $opts[:web], details: $opts

conf_engine = MU::Config.new(config, $opts[:skipinitialupdates], params: params, updating: $opts[:update], default_credentials: $opts[:credentials], cloud: $opts[:cloud])
stack_conf = conf_engine.config

if $opts[:dryrun] or $opts[:verbose]
  puts MU::Config.stripConfig(stack_conf).to_yaml
  conf_engine.visualizeDependencies
end

if $opts[:dryrun]
  MU.log("#{$config} loaded successfully.", html: $opts[:web])
  if MU::Cloud::AWS.hosted # XXX actually, check whether we're targeting AWS resources
    # I do not understand why this is necessary, but here we are.
    Thread.handle_interrupt(MU::Cloud::MuCloudResourceNotImplemented => :never) {
      begin
        Thread.handle_interrupt(MU::Cloud::MuCloudResourceNotImplemented => :immediate) {
          MU.log "Cost calculator not available for this stack, as it uses a resource not implemented in Mu's CloudFormation layer.", MU::NOTICE, verbosity: MU::Logger::NORMAL
          Thread.current.exit
        }
      ensure
      end
    }
    begin
      cost_dummy_deploy = MU::Deploy.new(
        $opts[:environment],
        verbosity: MU::Logger::SILENT,
        color: $opts[:color],
        force_cloudformation: true,
        cloudformation_path: "/dev/null",
        nocleanup: false,
        no_artifacts: true,
        stack_conf: stack_conf
      )
      cost_dummy_deploy.run
    rescue MU::Cloud::MuCloudResourceNotImplemented, MU::Cloud::MuCloudFlagNotImplemented
      MU.log "Cost calculator not available for this stack, as it uses a resource not implemented in Mu's CloudFormation layer.", MU::NOTICE, verbosity: MU::Logger::NORMAL
    end
  end
  exit
end

if $opts[:update]
  deploy = MU::MommaCat.new($opts[:update])
# TODO consider whether this is useful/valid
#  old_conf = JSON.parse(File.read(deploy.deploy_dir+"/basket_of_kittens.json"))
#  stack_conf = old_conf.merge(stack_conf)
  deploy.updateBasketofKittens(stack_conf, skip_validation: true)
  deployer = MU::Deploy.new(
    deploy.environment,
    verbosity: verbosity,
    color: $opts[:color],
    webify_logs: $opts[:web],
    nocleanup: true, # don't accidentally blow up an existing deploy
    stack_conf: stack_conf,
    deploy_id: $opts[:update],
    deploy_obj: deploy
  )
  deployer.run
  exit 0
end

$application_cookbook = stack_conf["application_cookbook"]
Dir.chdir(MU.installDir)

cfm_path = "/tmp/cloudformation-#{stack_conf['appname']}.json"
if !$opts[:cloudformation_output].nil?
  cfm_path = $opts[:cloudformation_output]
end

deployer = MU::Deploy.new(
  $opts[:environment],
  verbosity: verbosity,
  color: $opts[:color],
  webify_logs: $opts[:web],
  nocleanup: $opts[:nocleanup],
  cloudformation_path: cfm_path,
  force_cloudformation: $opts[:cloudformation],
  stack_conf: stack_conf
)

deployer.run