Class: Jackal::Stacks::Builder

Inherits:
Callback
  • Object
show all
Includes:
StackCommon
Defined in:
lib/jackal-stacks/builder.rb

Overview

Stack builder

Instance Method Summary collapse

Methods included from StackCommon

#determine_namespace, #stack_name, #stacks_api

Instance Method Details

#allowed?(payload) ⇒ TrueClass, FalseClass

Check if this payload is allowed to be processed based on defined restrictions within the configuration

Parameters:

  • payload (Smash)

Returns:

  • (TrueClass, FalseClass)


176
177
178
# File 'lib/jackal-stacks/builder.rb', line 176

def allowed?(payload)
  !!determine_namespace(payload)
end

#build_stack_args(payload, directory) ⇒ Smash

Build configuration arguments for Sfn::Command execution

Parameters:

  • payload (Smash)
  • directory (String)

    directory to unpacked asset

Returns:

  • (Smash)

    stack command options hash



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/jackal-stacks/builder.rb', line 86

def build_stack_args(payload, directory)
  Smash.new(
    :base_directory => File.join(directory, 'cloudformation'),
    :parameters => load_stack_parameters(payload, directory),
    :ui => Bogo::Ui.new(
      :app_name => 'JackalStacks',
      :defaults => true,
      :output_to => StringIO.new('')
    ),
    :interactive_parameters => false,
    :nesting_bucket => config.get(:orchestration, :bucket_name),
    :apply_nesting => true,
    :processing => true,
    :options => {
      :disable_rollback => true,
      :capabilities => ['CAPABILITY_IAM']
    },
    :credentials => config.get(:orchestration, :api, :credentials),
    :file => payload.fetch(:data, :stacks, :template, config.get(:default_template_path)),
    :file_path_prompt => false,
    :poll => false
  )
end

#execute(message) ⇒ Object

Build or update stacks

Parameters:

  • message (Carnivore::Message)


51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/jackal-stacks/builder.rb', line 51

def execute(message)
  failure_wrap(message) do |payload|
    directory = asset_store.unpack(asset_store.get(payload.get(:data, :stacks, :asset)), workspace(payload))
    begin
      unless(payload.get(:data, :stacks, :name))
        payload.set(:data, :stacks, :name, stack_name(payload))
      end
      store_stable_asset(payload, directory)
      begin
        stack = stacks_api.stacks.get(payload.get(:data, :stacks, :name))
      rescue
        stack = nil
      end
      if(stack)
        info "Stack currently exists. Applying update [#{stack}]"
        run_stack(payload, directory, :update)
        payload.set(:data, :stacks, :updated, true)
      else
        info "Stack does not currently exist. Building new stack [#{payload.get(:data, :stacks, :name)}]"
        init_provider(payload)
        run_stack(payload, directory, :create)
        payload.set(:data, :stacks, :created, true)
      end
    ensure
      FileUtils.rm_rf(directory)
    end
    job_completed(:stacks, payload, message)
  end
end

#init_provider(payload) ⇒ Object

Note:

this currently init’s chef related items

Initialize provider if instructed via config

Parameters:

  • payload (Smash)


184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/jackal-stacks/builder.rb', line 184

def init_provider(payload)
  if(config.get(:init, :chef, :validator) || config.get(:init, :chef, :encrypted_secret))
    bucket = stacks_api.api_for(:storage).buckets.get(config.get(:orchestration, :bucket_name))
    validator_name = name_for(payload, 'validator.pem')
    if(config.get(:init, :chef, :validator) && bucket.files.get(validator_name).nil?)
      file = bucket.files.build(:name => validator_name)
      file.body = OpenSSL::PKey::RSA.new(2048).export
      file.save
    end
    secret_name = name_for(payload, 'encrypted_data_bag_secret')
    if(config.get(:init, :chef, :encrypted_secret) && bucket.files.get(secret_name).nil?)
      file = bucket.files.build(:name => secret_name)
      file.body = SecureRandom.base64(2048)
      file.save
    end
  end
end

#load_stack_parameters(payload, directory) ⇒ Object

Note:

parameter precedence:

  • Hook URL parameters

  • Payload parameters

  • Stacks file parameters

  • Service configuration parameters

Extract any custom parameters from asset store if available, and merge any parameters provided via payload, and finally merge any parameters provided via configuration

Parameters:

  • payload (Smash)
  • directory (String)


121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/jackal-stacks/builder.rb', line 121

def load_stack_parameters(payload, directory)
  params = Smash.new
  stacks_file = load_stacks_file(payload, directory)
  s_namespace = determine_namespace(payload)
  template = payload.get(:data, :stacks, :template)
  params.deep_merge!(payload.fetch(:data, :webhook, :query, :stacks, :parameters, Smash.new))
  params.deep_merge!(payload.fetch(:data, :stacks, :parameters, Smash.new))
  params.deep_merge!(
    stacks_file.fetch(s_namespace, template, :parameters,
      stacks_file.fetch(:default, template, :parameters, Smash.new)
    )
  )
  params.deep_merge!(
    config.fetch(:parameter_overrides, s_namespace, template,
      config.fetch(:parameter_overrides, :default, template, Smash.new)
    )
  )
  params
end

#load_stacks_file(payload, directory) ⇒ Smash

Parse the ‘.stacks` file if available

Parameters:

  • payload (Smash)
  • directory (String)

    path to unpacked asset directory

Returns:

  • (Smash)


146
147
148
149
150
151
152
153
# File 'lib/jackal-stacks/builder.rb', line 146

def load_stacks_file(payload, directory)
  stacks_path = File.join(directory, '.stacks')
  if(File.exists?(stacks_path))
    Bogo::Config.new(file_path).data
  else
    Smash.new
  end
end

#name_for(payload, asset_name) ⇒ String

Note:

this is currently a no-op and thus are shared across stacks. currently is stubbed for completion of template and interaction logic

Provide prefixed key name for asset

Parameters:

  • payload (Smash)
  • asset_name (String)

    sset_name [String

Returns:

  • (String)


238
239
240
241
# File 'lib/jackal-stacks/builder.rb', line 238

def name_for(payload, asset_name)
  File.join(determine_namespace(payload), asset_name)
  asset_name
end

#run_stack(payload, directory, action) ⇒ TrueClass

Perform stack action

Parameters:

  • payload (Smash)
  • directory (String)

    directory to unpacked asset

  • action (Symbol, String)

    :create or :update

Returns:

  • (TrueClass)


161
162
163
164
165
166
167
168
169
# File 'lib/jackal-stacks/builder.rb', line 161

def run_stack(payload, directory, action)
  unless([:create, :update].include?(action.to_sym))
    abort ArgumentError.new("Invalid action argument `#{action}`. Expecting `create` or `update`!")
  end
  args = build_stack_args(payload, directory)
  stack_name = payload.get(:data, :stacks, :name)
  Sfn::Command.const_get(action.to_s.capitalize).new(args, [stack_name]).execute!
  true
end

#setup(*_) ⇒ Object

Setup callback



11
12
13
14
15
16
17
18
19
20
# File 'lib/jackal-stacks/builder.rb', line 11

def setup(*_)
  require 'sfn'
  require 'bogo-ui'
  require 'stringio'
  require 'openssl'
  require 'fileutils'
  require 'batali'
  # Ensure we can build the API at startup
  stacks_api
end

#store_stable_asset(payload, directory) ⇒ Object

Store stable asset in object store

Parameters:

  • payload (Smash)


205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/jackal-stacks/builder.rb', line 205

def store_stable_asset(payload, directory)
  if(config.get(:init, :stable))
    ['.batali', 'Gemfile', 'Gemfile.lock'].each do |file|
      file_path = File.join(directory, file)
      if(File.exists?(file_path))
        debug "Removing file from infra directory: #{file}"
        FileUtils.rm(file_path)
      end
    end
    if(File.exists?(File.join(directory, 'batali.manifest')))
      debug 'Installing cookbooks from Batali manifest'
      Dir.chdir(directory) do
        Batali::Command::Install.new({}, []).execute!
      end
    end
    debug "Starting stable asset upload for #{payload[:id]}"
    bucket = stacks_api.api_for(:storage).buckets.get(config.get(:orchestration, :bucket_name))
    stable_name = name_for(payload, 'stable.zip')
    file = bucket.files.get(stable_name) || bucket.files.build(:name => stable_name)
    file.body = asset_store.pack(directory)
    file.save
    debug "Completed stable asset upload for #{payload[:id]}"
  end
end

#valid?(message) ⇒ Truthy, Falsey

Determine validity of message

Parameters:

  • message (Carnivore::Message)

Returns:

  • (Truthy, Falsey)


26
27
28
29
30
31
32
33
# File 'lib/jackal-stacks/builder.rb', line 26

def valid?(message)
  super do |payload|
    payload.get(:data, :stacks, :builder) &&
      payload.get(:data, :stacks, :template) &&
      payload.get(:data, :stacks, :asset) &&
      allowed?(payload)
  end
end

#working_directoryString

Returns working directory.

Returns:

  • (String)

    working directory



36
37
38
39
40
41
# File 'lib/jackal-stacks/builder.rb', line 36

def working_directory
  memoize(:working_directory, :direct) do
    FileUtils.mkdir_p(dir = config.fetch(:working_directory, '/tmp/jackal-stack-builder'))
    dir
  end
end

#workspace(payload) ⇒ String

Returns fresh working directory.

Returns:

  • (String)

    fresh working directory



44
45
46
# File 'lib/jackal-stacks/builder.rb', line 44

def workspace(payload)
  File.join(working_directory, payload[:id])
end