Class: Morpheus::Cli::Deploy

Inherits:
Object
  • Object
show all
Includes:
CliCommand, DeploymentsHelper
Defined in:
lib/morpheus/cli/deploy.rb

Instance Attribute Summary

Attributes included from CliCommand

#no_prompt

Instance Method Summary collapse

Methods included from DeploymentsHelper

#deployment_list_key, #deployment_object_key, #deployment_version_list_key, #deployment_version_object_key, #deployments_interface, #find_deployment_by_id, #find_deployment_by_name, #find_deployment_by_name_or_id, #find_deployment_type_by_name, #find_deployment_version_by_id, #find_deployment_version_by_name, #find_deployment_version_by_name_or_id, included

Methods included from CliCommand

#apply_options, #build_common_options, #build_option_type_options, #build_standard_add_options, #build_standard_delete_options, #build_standard_get_options, #build_standard_list_options, #build_standard_post_options, #build_standard_put_options, #build_standard_remove_options, #build_standard_update_options, #command_name, #default_refresh_interval, #default_subcommand, #establish_remote_appliance_connection, #full_command_usage, #get_subcommand_description, #handle_subcommand, included, #interactive?, #my_help_command, #my_terminal, #my_terminal=, #parse_bytes_param, #parse_id_list, #parse_list_options, #parse_list_subtitles, #parse_passed_options, #parse_payload, #parse_query_options, #print, #print_error, #println, #puts, #puts_error, #raise_args_error, #raise_command_error, #render_response, #run_command_for_each_arg, #subcommand_aliases, #subcommand_description, #subcommand_usage, #subcommands, #usage, #validate_outfile, #verify_args!, #visible_subcommands

Instance Method Details

#connect(opts) ⇒ Object



10
11
12
13
14
15
# File 'lib/morpheus/cli/deploy.rb', line 10

def connect(opts)
  @api_client = establish_remote_appliance_connection(opts)
  @instances_interface = @api_client.instances
  @deploy_interface = @api_client.deploy
  @deployments_interface = @api_client.deployments
end

#handle(args) ⇒ Object



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
# File 'lib/morpheus/cli/deploy.rb', line 17

def handle(args)
  options={}
  optparse = Morpheus::Cli::OptionParser.new do|opts|
    opts.banner = "Usage: morpheus deploy [environment]"
    build_common_options(opts, options, [:auto_confirm, :remote, :dry_run])
    opts.footer = <<-EOT
Deploy to an instance using the morpheus.yml file, located in the working directory.
[environment] is optional. Merge settings under environments.{environment}. Default is no environment.

First this parses the morpheus.yml file and merges the specified environment settings.
The specified instance must exist and the specified version must not exist.
If the settings are valid, the new deployment version will be created and 
all the specified files are uploaded to the new deployment version.
Finally, it deploys the new version to the instance.

The morpheus.yml should be located in the working directory.
This file contains the information necessary to perform a deployment via the cli.

File Settings
==================

* name - (required) The instance name we are deploying to and, by default, name of the deployment being created.
* version - (required) The version identifier of the deployment being created (userVersion)
* deployment - The name of the deployment being created, name is used by default
* script - The initial script to run before looking for files to upload.
* files - List of file patterns to use for uploading files and their target destination. 
        Each item should contain path and pattern, path may be relative to the working directory, default pattern is: '**/*'
* options - Map of deployment options depending on deployment type
* post_script - A post operation script to be run on the local machine
* stage_only - If set to true the deploy will only be staged and not actually run
* environments - Map of objects that contain nested properties for each environment name

It is possible to nest these properties in an "environments" map to override based on a passed environment.

Example
==================

name: neatsite
version: 5.0
script: "rake build"
files: 
- path: build
environments:
production:
  files:
  - path: production-build
EOT
  end
  optparse.parse!(args)
  verify_args!(args:args, optparse:optparse, min:0, max:1)
  options[:options]['name'] = args[0] if args[0]
  connect(options)
  payload = {}
  
  environment = default_deploy_environment
  if args.count > 0
    environment = args[0]
  end
  if load_deploy_file().nil?
    raise_command_error "Morpheus Deploy File `morpheus.yml` not detected. Please create one and try again."
  end

  # Parse and validate config, need instance + deployment + version + files
  # name can be specified as a single value for both instance and deployment

  deploy_args = merged_deploy_args(environment)

  instance_name = deploy_args['name']
  if deploy_args['instance'].is_a?(String)
    instance_name = deploy_args['instance']
  end
  if instance_name.nil?
    raise_command_error "Instance not specified. Please specify the instance name and try again."
  end

  deployment_name = deploy_args['name'] || instance_name
  if deploy_args['deployment'].is_a?(String)
    deployment_name = deploy_args['deployment']
  end
  
  version_number = deploy_args['version']
  if version_number.nil?
    raise_command_error "Version not specified. Please specify the version and try again."
  end

  instance_results = @instances_interface.list(name: instance_name)
  if instance_results['instances'].empty?
    raise_command_error "Instance not found by name '#{instance_name}'"
  end
  instance = instance_results['instances'][0]
  instance_id = instance['id']

  # ok do it
  # fetch/create deployment, create deployment version, upload files, and deploy it to instance.

  print_h1 "Morpheus Deployment"

  columns = {
    "Instance" => :name,
    "Deployment" => :deployment,
    "Version" => :version,
    "Script" => :script,
    "Post Script" => :post_script,
    "Files" => :files,
    "Environment" => :environment,
  }
  pretty_file_config = deploy_args['files'].collect {|it|
    [(it['path'] ? "path: #{it['path']}" : nil), (it['pattern'] ? "pattern: #{it['pattern']}" : nil)].compact.join(", ")
  }.join(", ")
  deploy_settings = {
    :name => instance_name,
    :deployment => deployment_name,
    :version => version_number,
    :script => deploy_args['script'],
    :post_script => deploy_args['post_script'],
    :files => pretty_file_config,
    # :files => deploy_args['files'],
    # :files => deploy_files.size,
    # :file_config => (deploy_files.size == 1 ? deploy_files[0][:destination] : deploy_args['files'])
    :environment => environment
  }
  columns.delete("Script") if deploy_settings[:script].nil?
  columns.delete("Post Script") if deploy_settings[:post_script].nil?
  columns.delete("Environment") if deploy_settings[:environment].nil?
  print_description_list(columns, deploy_settings)
  print reset, "\n"

  if !deploy_args['script'].nil?
    # do this for dry run too since this is usually what creates the files to be uploaded
    print cyan, "Executing Pre Deploy Script...", reset, "\n"
    puts "running command: #{deploy_args['script']}"
    if !system(deploy_args['script'])
      raise_command_error "Error executing pre script..."
    end
  end

  # Find Files to Upload
  deploy_files = []
  if deploy_args['files'].nil? || deploy_args['files'].empty? || !deploy_args['files'].is_a?(Array)
    raise_command_error "Files not specified. Please specify files array, each item may specify a path or pattern of file(s) to upload"
  else
    #print "\n",cyan, "Finding Files...", reset, "\n"
    current_working_dir = Dir.pwd
    deploy_args['files'].each do |fmap|
      Dir.chdir(fmap['path'] || current_working_dir)
      files = Dir.glob(fmap['pattern'] || '**/*')
      files.each do |file|
        if File.file?(file)
          destination = file.split("/")[0..-2].join("/")
          # deploy_files << {filepath: File.expand_path(file), destination: destination}
          deploy_files << {filepath: File.expand_path(file), destination: file}
        end
      end
    end
    #print cyan, "Found #{deploy_files.size} Files to Upload!", reset, "\n"
    Dir.chdir(current_working_dir)
  end

  if deploy_files.empty?
    raise_command_error "0 files found for: #{deploy_args['files'].inspect}"
  else
    print cyan, "Found #{deploy_files.size} Files to Upload!", reset, "\n"
  end

  unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to create deployment version #{version_number} (#{deploy_files.size} #{deploy_files.size == 1 ? 'file' : 'files'}) and deploy it to instance #{instance['name']}?")
    return 9, "aborted command"
  end
  
  # Find or Create Deployment
  deployment = nil
  deployments = @deployments_interface.list(name: deployment_name)['deployments']

  @instances_interface.setopts(options)
  @deploy_interface.setopts(options)
  @deployments_interface.setopts(options)

  if deployments.size > 1
    raise_command_error "#{deployments.size} deployment versions found by deployment '#{name}'"
  elsif deployments.size == 1
    deployment = deployments[0]
    # should update here, eg description
  else
    # create it
    payload = {
      'deployment' => {
        'name' => deployment_name
      } 
    }
    payload['deployment']['description'] = deploy_args['description'] if deploy_args['description']
    
    if options[:dry_run]
      print_dry_run @deployments_interface.dry.create(payload)
      # return 0, nil
      deployment = {'id' => ':deploymentId', 'name' => deployment_name}
    else
      json_response = @deployments_interface.create(payload)
      deployment = json_response['deployment']
    end
  end

  # Find or Create Deployment Version
  # Actually, for now this this errors if the version already exists, but it should update it.

  @deployments_interface = @api_client.deployments
  deployment_version = nil
  if options[:dry_run]
    print_dry_run @deployments_interface.dry.list_versions(deployment['id'], {userVersion: version_number})
    # return 0, nil
    #deployment_versions =[{'id' => ':versionId', 'version' => version_number}]
    deployment_versions = []
  else
    deployment_versions = @deployments_interface.list_versions(deployment['id'], {userVersion: version_number})['versions']
    @deployments_interface.setopts(options)
  end
  

  if deployment_versions.size > 0
    raise_command_error "Deployment '#{deployment['name']}' version '#{version_number}' already exists. Specify a new version or delete the existing version."
  # if deployment_versions.size > 1
  #   raise_command_error "#{deployment_versions.size} versions found by version '#{name}'"
  # elsif deployment_versions.size == 1
  #   deployment_version = deployment_versions[0]
  #   # should update here, eg description
  else
    # create it
    payload = {
      'version' => {
        'userVersion' => version_number,
        'deployType' => (deploy_args['type'] || deploy_args['deployType'] || 'file')
      } 
    }
    payload['version']['fetchUrl'] = deploy_args['fetchUrl'] if deploy_args['fetchUrl']
    payload['version']['gitUrl'] = deploy_args['gitUrl'] if deploy_args['gitUrl']
    payload['version']['gitRef'] = deploy_args['gitRef'] if deploy_args['gitRef']
    
    if options[:dry_run]
      print_dry_run @deployments_interface.dry.create_version(deployment['id'], payload)
      # return 0, nil
      deployment_version = {'id' => ':versionId', 'version' => version_number}
    else
      json_response = @deployments_interface.create_version(deployment['id'], payload)
      deployment_version = json_response['version']
    end
  end

  
  # Upload Files
  if deploy_files && !deploy_files.empty?
    print "\n",cyan, "Uploading #{deploy_files.size} Files...", reset, "\n"
    current_working_dir = Dir.pwd
    deploy_files.each do |f|
      destination = f[:destination]
      if options[:dry_run]
        print_dry_run @deployments_interface.upload_file(deployment['id'], deployment_version['id'], f[:filepath], f[:destination])
      else
        print cyan,"  - Uploading #{f[:destination]} ...", reset if !options[:quiet]
        upload_result = @deployments_interface.upload_file(deployment['id'], deployment_version['id'], f[:filepath], f[:destination])
        #print green + "SUCCESS" + reset + "\n" if !options[:quiet]
        print reset, "\n" if !options[:quiet]
      end
    end
    print cyan, "Upload Complete!", reset, "\n"
    Dir.chdir(current_working_dir)
  else
    print "\n",cyan, "0 files to upload", reset, "\n"
  end

  # TODO: support deploying other deployTypes too, git and fetch

  if !deploy_args['post_script'].nil?
    print cyan, "Executing Post Script...", reset, "\n"
    puts "running command: #{deploy_args['post_script']}"
    if !system(deploy_args['post_script'])
      raise_command_error "Error executing post script..."
    end
  end

  # JD: restart for evars eh?
  if deploy_args['env']
    evars = []
    deploy_args['env'].each_pair do |key, value|
      evars << {name: key, value: value, export: false}
    end
    payload = {envs: evars}
    if options[:dry_run]
      print_dry_run @instances_interface.dry.create_env(instance_id, payload)
      print_dry_run @instances_interface.dry.restart(instance_id)
    else
      @instances_interface.create_env(instance_id, payload)
      @instances_interface.restart(instance_id)
    end
  end
  # Create the AppDeploy, this does the deploy async (as of 4.2.2-3)
  payload = {'appDeploy' => {} }
  payload['appDeploy']['versionId'] = deployment_version['id']
  if deploy_args['options']
    payload['appDeploy']['config'] = deploy_args['options']
  end
  # stageOnly means do not actually deploy yet, can invoke @deploy_interface.deploy(deployment['id']) later
  # there is no cli command for that yet though..
  stage_only = deploy_args['stage_deploy'] || deploy_args['stage_only'] || deploy_args['stageOnly']
  if stage_only
    payload['appDeploy']['stageOnly'] = true
  end
  app_deploy_id = nil
  if options[:dry_run]
    print_dry_run @deploy_interface.dry.create(instance_id, payload)
    # return 0, nil
    app_deploy_id = ':appDeployId'
  else
    # Create a new appDeploy record, without stageOnly, this actually does the deployment
    print cyan, "Deploying #{deployment_name} version #{version_number} to instance #{instance_name} ...", reset, "\n"
    deploy_result = @deploy_interface.create(instance_id, payload)
    app_deploy = deploy_result['appDeploy']
    app_deploy_id = app_deploy['id']
    print_green_success "Deploy Successful!"
  end
  return 0, nil
end