Class: Morpheus::Cli::Deploy

Inherits:
Object
  • Object
show all
Includes:
CliCommand, DeploymentsHelper
Defined in:
lib/morpheus/cli/commands/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, #format_app_deploy_status, #format_deploy_type, #format_deployment_version_number, included

Methods included from CliCommand

#add_query_parameter, #apply_options, #build_common_options, #build_get_options, #build_list_options, #build_option_type_options, #build_standard_add_many_options, #build_standard_add_options, #build_standard_api_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_description, #command_name, #confirm, #confirm!, #default_refresh_interval, #default_sigdig, #default_subcommand, #establish_remote_appliance_connection, #execute_api, #execute_api_payload, #execute_api_request, #find_all, #find_all_json, #find_by_id, #find_by_name, #find_by_name_or_id, #find_record, #find_record_json, #full_command_usage, #get_interface, #get_list_key, #get_object_key, #get_subcommand_description, #handle_each_payload, #handle_subcommand, included, #interactive?, #my_help_command, #my_terminal, #my_terminal=, #parse_array, #parse_bytes_param, #parse_get_options!, #parse_id_list, #parse_labels, #parse_list_options, #parse_list_options!, #parse_list_subtitles, #parse_options, #parse_parameter_as_resource_id!, #parse_passed_options, #parse_payload, #parse_query_options, #print, #print_error, #println, #prog_name, #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/commands/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
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
# File 'lib/morpheus/cli/commands/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, :quiet, :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 the morpheus.yml YAML file is parsed, merging the specified environment's nested settings.
The specified instance must exist and the specified deployment version must not exist.
If the settings are valid, the new deployment version will be created.
If is a file type deployment, all the discovered files are uploaded to the new deployment version.
Finally, it deploys the new version to the instance using any specified config options.

The morpheus.yml should be located in the working directory.
This YAML file contains the settings that specify how to execute the deployment.

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

* name - (required) The instance name being deployed to, also the default name of the deployment.
* version - (required) The version identifier of the deployment being created (userVersion)
* deployment - The name of the deployment being created, name is used by default
* type - The type of deployment, file, 'git' or 'fetch', default is 'file'.
* script - The initial script to run, happens before finding the files to be uploaded.
* files - (required) 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: '**/*'
        only applies to type 'file'
* url - (required) The url to fetch files from, only applies to types 'git' and 'fetch'.
* ref - The git reference, default is master (main), only applies to type git.
* config - Map of deployment config options depending on deployment type
* options - alias for config
* 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: mysite
version: 5.0
script: "rake build"
files: 
- path: build
environments:
production:
  files:
  - path: production-build


Git Example
==================

name: morpheus-apidoc
version: 5.0.0
type: git
url: "https://github.com/gomorpheus/morpheus-apidoc"

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']

  # auto detect type, default to file
  deploy_type = deploy_args['type'] || deploy_args['deployType']
  if deploy_type.nil?
    if deploy_args['gitUrl']
      deploy_type = 'git'
    elsif deploy_args['fetchUrl'] || deploy_args['url']
      deploy_type = 'fetch'
    end
  end
  if deploy_type.nil?
    deploy_type = "file"
  end
  deploy_url = deploy_args['url'] || deploy_args['fetchUrl'] || deploy_args['gitUrl']
  if deploy_url.nil? && (deploy_type == "git" || deploy_type == "fetch")
    raise_command_error "Deploy type '#{deploy_type}' requires a url to be specified"
  end
  #deploy_type = "file" if deploy_type.to_s.downcase == "files"

  deploy_config = deploy_args['options'] || deploy_args['config']

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

  unless options[:quiet]

    print_h1 "Morpheus Deployment", options

    columns = {
      "Instance" => :name,
      "Deployment" => :deployment,
      "Version" => :version,
      "Deploy Type" => :type,
      "Script" => :script,
      "Post Script" => :post_script,
      "Files" => :files,
      "Git Url" => :git_url,
      "Git Ref" => :git_ref,
      "Fetch Url" => :fetch_url,
      "Environment" => :environment,
    }
    pretty_file_config = deploy_args['files'] ? deploy_args['files'].collect {|it|
      [(it['path'] ? "path: #{it['path']}" : nil), (it['pattern'] ? "pattern: #{it['pattern']}" : nil)].compact.join(", ")
    }.join(", ") : "(none)"
    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,
      :type => format_deploy_type(deploy_type),
      :git_url => deploy_args['gitUrl'] || (deploy_type == "git" ? deploy_args['url'] : nil),
      :git_ref => deploy_args['gitRef'] || (deploy_type == "git" ? deploy_args['ref'] : nil),
      :fetch_url => deploy_args['fetchUrl'] || (deploy_type == "fetch" ? deploy_args['url'] : nil),
      # :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?
    columns.delete("Files") if deploy_type != "file" && deploy_type != "files"
    columns.delete("Git Url") if deploy_settings[:git_url].nil?
    columns.delete("Git Ref") if deploy_settings[:git_ref].nil?
    columns.delete("Fetch Url") if deploy_settings[:fetch_url].nil?
    print_description_list(columns, deploy_settings)
    print reset, "\n"

    if deploy_config
      print_h2 "Config Options", options
      print cyan
      puts as_json(deploy_config)
      print "\n\n", reset
    end

  end # unless options[:quiet]

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

  # Find Files to Upload
  deploy_files = []
  if deploy_type == "file" || deploy_type == "files"
    if deploy_args['files'].nil? || deploy_args['files'].empty? || !deploy_args['files'].is_a?(Array)
      raise_command_error "Files not specified. Please specify the files to include, 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
      unless options[:quiet]
        print cyan, "Found #{deploy_files.size} Files to Upload!", reset, "\n"
      end
    end
  elsif deploy_type == "git"
    # make it work with simpler config, url instead of gitUrl
    if deploy_args['gitUrl'].nil? && deploy_args['url']
      deploy_args['gitUrl'] = deploy_args['url'] # .delete('url') maybe?
    end
    if deploy_args['gitRef'].nil? && deploy_args['ref']
      deploy_args['gitRef'] = deploy_args['ref'] # .delete('ref') maybe?
    end
    if deploy_args['gitRef'].nil?
      raise_command_error "fetchUrl not specified. Please specify the git url to fetch the deploy files from."
    end
    if deploy_args['gitRef'].nil?
      #raise_command_error "gitRef not specified. Please specify the git reference to use. eg. main"
      # deploy_args['gitRef'] = "main"
    end
  elsif deploy_type == "git"
    # make it work with simpler config, url instead of fetchUrl
    if deploy_args['fetchUrl'].nil? && deploy_args['url']
      deploy_args['fetchUrl'] = deploy_args['url'] # .delete('url') maybe?
    end
    if deploy_args['fetchUrl'].nil?
      raise_command_error "fetchUrl not specified. Please specify the url to fetch the deploy files from."
    end
    
  end

  confirm_warning = ""
  confirm_message = "Are you sure you want to perform this action?"
  if deploy_type == "file" || deploy_type == "files"
    confirm_warning = "This will create deployment #{deployment_name} version #{version_number} and deploy it to instance #{instance['name']}."
  elsif deploy_type == "git"
    confirm_warning = "This will create deployment #{deployment_name} version #{version_number} and deploy it to instance #{instance['name']}."
  elsif deploy_type == "fetch"
    confirm_warning = "This will create deployment #{deployment_name} version #{version_number} and deploy it to instance #{instance['name']}."
  end
  puts confirm_warning if !options[:quiet]
  unless options[:yes] || Morpheus::Cli::OptionTypes.confirm(confirm_message)
    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_type
      } 
    }
    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_type == "file" || deploy_type == "files"
    if deploy_files && !deploy_files.empty?
      print "\n",cyan, "Uploading #{deploy_files.size} Files...", reset, "\n" if !options[:quiet]
      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" if !options[:quiet]
      Dir.chdir(current_working_dir)
    else
      print "\n",cyan, "0 files to upload", reset, "\n" if !options[:quiet]
    end
  end

  if !deploy_args['post_script'].nil?
    print cyan, "Executing Post Script...", reset, "\n" if !options[:quiet]
    puts "running command: #{deploy_args['post_script']}" if !options[:quiet]
    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_args['stage_deploy'] || deploy_args['stage_only'] || deploy_args['stageOnly']
  if stage_only
    payload['appDeploy']['stageOnly'] = true
  end
  # config/options to apply to deployment
  if deploy_config
    payload['appDeploy']['config'] = deploy_config
  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']
    if !options[:quiet]
      if app_deploy['status'] == 'staged'
        print_green_success "Staged Deploy #{deployment_name} version #{version_number} to instance #{instance_name}"
      else
        print_green_success "Deploying #{deployment_name} version #{version_number} to instance #{instance_name}"
      end
    end
  end
  return 0, nil
end