3scale_toolbox 0.16.0 → 0.18.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/3scale_toolbox.gemspec +2 -2
  3. data/README.md +11 -8
  4. data/lib/3scale_toolbox.rb +3 -0
  5. data/lib/3scale_toolbox/3scale_client_factory.rb +3 -4
  6. data/lib/3scale_toolbox/cli/error_handler.rb +17 -14
  7. data/lib/3scale_toolbox/commands.rb +2 -0
  8. data/lib/3scale_toolbox/commands/backend_command/copy_command/copy_mapping_rules_task.rb +11 -27
  9. data/lib/3scale_toolbox/commands/backend_command/copy_command/copy_methods_task.rb +5 -10
  10. data/lib/3scale_toolbox/commands/backend_command/copy_command/copy_metrics_task.rb +4 -4
  11. data/lib/3scale_toolbox/commands/backend_command/copy_command/create_or_update_target_backend_task.rb +3 -2
  12. data/lib/3scale_toolbox/commands/backend_command/copy_command/task.rb +10 -32
  13. data/lib/3scale_toolbox/commands/import_command/issuer_type_transformer.rb +16 -0
  14. data/lib/3scale_toolbox/commands/import_command/openapi.rb +3 -0
  15. data/lib/3scale_toolbox/commands/import_command/openapi/create_activedocs_step.rb +3 -2
  16. data/lib/3scale_toolbox/commands/import_command/openapi/create_mapping_rule_step.rb +2 -1
  17. data/lib/3scale_toolbox/commands/import_command/openapi/create_method_step.rb +5 -14
  18. data/lib/3scale_toolbox/commands/import_command/openapi/step.rb +4 -0
  19. data/lib/3scale_toolbox/commands/import_command/openapi/update_service_proxy_step.rb +1 -0
  20. data/lib/3scale_toolbox/commands/methods_command/apply_command.rb +2 -4
  21. data/lib/3scale_toolbox/commands/methods_command/create_command.rb +0 -2
  22. data/lib/3scale_toolbox/commands/methods_command/delete_command.rb +1 -1
  23. data/lib/3scale_toolbox/commands/methods_command/list_command.rb +1 -9
  24. data/lib/3scale_toolbox/commands/metrics_command/list_command.rb +1 -1
  25. data/lib/3scale_toolbox/commands/plans_command/export/read_plan_limits_step.rb +1 -1
  26. data/lib/3scale_toolbox/commands/plans_command/export/read_plan_methods_step.rb +2 -2
  27. data/lib/3scale_toolbox/commands/plans_command/export/read_plan_metrics_step.rb +2 -2
  28. data/lib/3scale_toolbox/commands/plans_command/export/read_plan_pricing_rules_step.rb +1 -2
  29. data/lib/3scale_toolbox/commands/plans_command/export/step.rb +8 -20
  30. data/lib/3scale_toolbox/commands/plans_command/import/import_plan_limits_step.rb +12 -14
  31. data/lib/3scale_toolbox/commands/plans_command/import/import_plan_metrics_step.rb +6 -13
  32. data/lib/3scale_toolbox/commands/plans_command/import/import_plan_pricing_rules_step.rb +12 -20
  33. data/lib/3scale_toolbox/commands/plans_command/import/step.rb +2 -22
  34. data/lib/3scale_toolbox/commands/plans_command/list_command.rb +1 -1
  35. data/lib/3scale_toolbox/commands/plans_command/show_command.rb +1 -1
  36. data/lib/3scale_toolbox/commands/policies_command.rb +24 -0
  37. data/lib/3scale_toolbox/commands/policies_command/export_command.rb +98 -0
  38. data/lib/3scale_toolbox/commands/policies_command/import_command.rb +61 -0
  39. data/lib/3scale_toolbox/commands/product_command.rb +4 -0
  40. data/lib/3scale_toolbox/commands/product_command/copy_command.rb +7 -3
  41. data/lib/3scale_toolbox/commands/product_command/copy_command/copy_backends_task.rb +22 -5
  42. data/lib/3scale_toolbox/commands/product_command/export_command.rb +81 -0
  43. data/lib/3scale_toolbox/commands/product_command/import_command.rb +125 -0
  44. data/lib/3scale_toolbox/commands/proxy_config_command.rb +5 -0
  45. data/lib/3scale_toolbox/commands/proxy_config_command/deploy_command.rb +54 -0
  46. data/lib/3scale_toolbox/commands/proxy_config_command/export_command.rb +74 -0
  47. data/lib/3scale_toolbox/commands/proxy_config_command/helper.rb +15 -0
  48. data/lib/3scale_toolbox/commands/service_command/copy_command/copy_activedocs_task.rb +15 -12
  49. data/lib/3scale_toolbox/commands/service_command/copy_command/copy_app_plans_task.rb +15 -15
  50. data/lib/3scale_toolbox/commands/service_command/copy_command/copy_limits_task.rb +12 -13
  51. data/lib/3scale_toolbox/commands/service_command/copy_command/copy_mapping_rules_task.rb +11 -11
  52. data/lib/3scale_toolbox/commands/service_command/copy_command/copy_methods_task.rb +9 -12
  53. data/lib/3scale_toolbox/commands/service_command/copy_command/copy_metrics_task.rb +8 -8
  54. data/lib/3scale_toolbox/commands/service_command/copy_command/copy_policies_task.rb +1 -1
  55. data/lib/3scale_toolbox/commands/service_command/copy_command/copy_pricingrules_task.rb +15 -18
  56. data/lib/3scale_toolbox/commands/service_command/copy_command/copy_service_proxy_task.rb +17 -2
  57. data/lib/3scale_toolbox/commands/service_command/copy_command/create_or_update_service_task.rb +2 -1
  58. data/lib/3scale_toolbox/commands/service_command/copy_command/destroy_mapping_rules_task.rb +9 -5
  59. data/lib/3scale_toolbox/commands/service_command/copy_command/task.rb +20 -34
  60. data/lib/3scale_toolbox/commands/update_command.rb +1 -1
  61. data/lib/3scale_toolbox/commands/update_command/service_command.rb +3 -2
  62. data/lib/3scale_toolbox/commands/update_command/service_command/delete_activedocs_task.rb +1 -3
  63. data/lib/3scale_toolbox/crds.rb +16 -0
  64. data/lib/3scale_toolbox/crds/application_plan_dump.rb +19 -0
  65. data/lib/3scale_toolbox/crds/backend_dump.rb +39 -0
  66. data/lib/3scale_toolbox/crds/backend_mapping_rule_dump.rb +26 -0
  67. data/lib/3scale_toolbox/crds/backend_method_dump.rb +12 -0
  68. data/lib/3scale_toolbox/crds/backend_metric_dump.rb +13 -0
  69. data/lib/3scale_toolbox/crds/backend_parser.rb +55 -0
  70. data/lib/3scale_toolbox/crds/backend_usage_dump.rb +11 -0
  71. data/lib/3scale_toolbox/crds/limit_dump.rb +37 -0
  72. data/lib/3scale_toolbox/crds/mapping_rule_dump.rb +26 -0
  73. data/lib/3scale_toolbox/crds/method_dump.rb +12 -0
  74. data/lib/3scale_toolbox/crds/metric_dump.rb +13 -0
  75. data/lib/3scale_toolbox/crds/pricing_rule_dump.rb +38 -0
  76. data/lib/3scale_toolbox/crds/product_deployment_parser.rb +329 -0
  77. data/lib/3scale_toolbox/crds/product_dump.rb +157 -0
  78. data/lib/3scale_toolbox/crds/product_parser.rb +114 -0
  79. data/lib/3scale_toolbox/crds/remote.rb +682 -0
  80. data/lib/3scale_toolbox/entities.rb +3 -0
  81. data/lib/3scale_toolbox/entities/activedocs.rb +12 -0
  82. data/lib/3scale_toolbox/entities/application_plan.rb +74 -39
  83. data/lib/3scale_toolbox/entities/backend.rb +65 -30
  84. data/lib/3scale_toolbox/entities/backend_mapping_rule.rb +29 -3
  85. data/lib/3scale_toolbox/entities/backend_method.rb +25 -16
  86. data/lib/3scale_toolbox/entities/backend_metric.rb +12 -2
  87. data/lib/3scale_toolbox/entities/backend_usage.rb +7 -1
  88. data/lib/3scale_toolbox/entities/limit.rb +71 -0
  89. data/lib/3scale_toolbox/entities/mapping_rule.rb +90 -0
  90. data/lib/3scale_toolbox/entities/method.rb +33 -19
  91. data/lib/3scale_toolbox/entities/metric.rb +29 -18
  92. data/lib/3scale_toolbox/entities/pricing_rule.rb +63 -0
  93. data/lib/3scale_toolbox/entities/proxy_config.rb +0 -1
  94. data/lib/3scale_toolbox/entities/service.rb +149 -46
  95. data/lib/3scale_toolbox/error.rb +50 -0
  96. data/lib/3scale_toolbox/helper.rb +13 -16
  97. data/lib/3scale_toolbox/openapi/oas3.rb +1 -1
  98. data/lib/3scale_toolbox/proxy_logger.rb +4 -0
  99. data/lib/3scale_toolbox/remote_cache.rb +157 -0
  100. data/lib/3scale_toolbox/remotes.rb +2 -2
  101. data/lib/3scale_toolbox/version.rb +1 -1
  102. data/licenses.xml +113 -45
  103. metadata +37 -8
@@ -0,0 +1,98 @@
1
+ module ThreeScaleToolbox
2
+ module Commands
3
+ module PoliciesCommand
4
+ class ExportSubcommand < Cri::CommandRunner
5
+ include ThreeScaleToolbox::Command
6
+
7
+ class JSONSerializer
8
+ def call(object)
9
+ JSON.pretty_generate(object)
10
+ end
11
+ end
12
+
13
+ class YAMLSerializer
14
+ def call(object)
15
+ YAML.dump(object)
16
+ end
17
+ end
18
+
19
+ class SerializerTransformer
20
+ def call(output_format)
21
+ raise unless %w[yaml json].include?(output_format)
22
+
23
+ case output_format
24
+ when 'yaml'
25
+ YAMLSerializer.new
26
+ when 'json'
27
+ JSONSerializer.new
28
+ end
29
+ end
30
+ end
31
+
32
+ def self.command
33
+ Cri::Command.define do
34
+ name 'export'
35
+ usage 'export [opts] <remote> <product>'
36
+ summary 'export product policy chain'
37
+ description 'export product policy chain'
38
+
39
+ option :f, :file, 'Write to file instead of stdout', argument: :required
40
+ option :o, :output, 'Output format. One of: json|yaml', argument: :required, transform: SerializerTransformer.new
41
+ param :remote
42
+ param :service_ref
43
+
44
+ runner ExportSubcommand
45
+ end
46
+ end
47
+
48
+ def run
49
+ select_output do |output|
50
+ output.write(serializer.call(product.policies))
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def remote
57
+ @remote ||= threescale_client(arguments[:remote])
58
+ end
59
+
60
+ def product
61
+ @product ||= find_product
62
+ end
63
+
64
+ def service_ref
65
+ arguments[:service_ref]
66
+ end
67
+
68
+ def find_product
69
+ Entities::Service.find(remote: remote,
70
+ ref: service_ref).tap do |svc|
71
+ raise ThreeScaleToolbox::Error, "Product #{service_ref} does not exist" if svc.nil?
72
+ end
73
+ end
74
+
75
+ def file
76
+ options[:file]
77
+ end
78
+
79
+ def select_output
80
+ ios = if file
81
+ File.open(file, 'w')
82
+ else
83
+ $stdout
84
+ end
85
+ begin
86
+ yield(ios)
87
+ ensure
88
+ ios.close
89
+ end
90
+ end
91
+
92
+ def serializer
93
+ options.fetch(:output, YAMLSerializer.new)
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,61 @@
1
+ module ThreeScaleToolbox
2
+ module Commands
3
+ module PoliciesCommand
4
+ class ImportSubcommand < Cri::CommandRunner
5
+ include ThreeScaleToolbox::Command
6
+ include ThreeScaleToolbox::ResourceReader
7
+
8
+ def self.command
9
+ Cri::Command.define do
10
+ name 'import'
11
+ usage 'import [opts] <remote> <product>'
12
+ summary 'import product policy chain'
13
+ description 'import product policy chain'
14
+
15
+ option :f, :file, 'Read from file', argument: :required
16
+ option :u, :url, 'Read from url', argument: :required
17
+ param :remote
18
+ param :service_ref
19
+
20
+ runner ImportSubcommand
21
+ end
22
+ end
23
+
24
+ def run
25
+ res = product.update_policies('policies_config' => policies)
26
+ if res.is_a?(Hash) && (errors = res['errors'])
27
+ raise ThreeScaleToolbox::Error, "Product policies have not been imported. #{errors}"
28
+ end
29
+ if res.is_a?(Array) && (error_item = res.find { |i| i.key?('errors') })
30
+ raise ThreeScaleToolbox::Error, "Product policies have not been imported. #{error_item['errors']}"
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def remote
37
+ @remote ||= threescale_client(arguments[:remote])
38
+ end
39
+
40
+ def service_ref
41
+ arguments[:service_ref]
42
+ end
43
+
44
+ def product
45
+ @product ||= find_product
46
+ end
47
+
48
+ def find_product
49
+ Entities::Service.find(remote: remote,
50
+ ref: service_ref).tap do |svc|
51
+ raise ThreeScaleToolbox::Error, "Product #{service_ref} does not exist" if svc.nil?
52
+ end
53
+ end
54
+
55
+ def policies
56
+ @policies ||= load_resource(options[:file] || options[:url] || '-')
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -1,4 +1,6 @@
1
1
  require '3scale_toolbox/commands/product_command/copy_command'
2
+ require '3scale_toolbox/commands/product_command/export_command'
3
+ require '3scale_toolbox/commands/product_command/import_command'
2
4
 
3
5
  module ThreeScaleToolbox
4
6
  module Commands
@@ -17,6 +19,8 @@ module ThreeScaleToolbox
17
19
  end
18
20
  end
19
21
  add_subcommand(CopySubcommand)
22
+ add_subcommand(ExportSubcommand)
23
+ add_subcommand(ImportSubcommand)
20
24
  end
21
25
  end
22
26
  end
@@ -22,8 +22,8 @@ module ThreeScaleToolbox
22
22
  \nproduct methods&metrics: Only missing metrics&methods will be created.
23
23
  \nproduct mapping rules: Only missing mapping rules will be created.
24
24
  \nproduct application plans & pricing rules & limits: Only missing application plans & pricing rules & limits will be created.
25
- \nproduct application usage rules
26
- \nproduct policies
25
+ \nproduct application usage rules
26
+ \nproduct policies
27
27
  \nproduct backends: Only missing backends will be created.
28
28
  \nproduct activedocs: Only missing activedocs will be created.
29
29
  HEREDOC
@@ -37,7 +37,7 @@ module ThreeScaleToolbox
37
37
  end
38
38
  end
39
39
 
40
- def run
40
+ def self.workflow(context)
41
41
  tasks = []
42
42
  tasks << ThreeScaleToolbox::Commands::ServiceCommand::CopyCommand::CreateOrUpdateTargetServiceTask.new(context)
43
43
  tasks << CopyCommand::DeleteExistingTargetBackendUsagesTask.new(context)
@@ -58,6 +58,10 @@ module ThreeScaleToolbox
58
58
  ThreeScaleToolbox::Commands::ServiceCommand::CopyCommand::BumpProxyVersionTask.new(service: context[:target]).call
59
59
  end
60
60
 
61
+ def run
62
+ self.class.workflow(context)
63
+ end
64
+
61
65
  private
62
66
 
63
67
  def context
@@ -13,14 +13,14 @@ module ThreeScaleToolbox
13
13
  def call
14
14
  backend_list = source.backend_usage_list
15
15
  backend_list.each(&method(:create_backend))
16
- puts "created/upated #{backend_list.size} backends"
16
+ logger.info "created/upated #{backend_list.size} backends"
17
17
  end
18
18
 
19
19
  private
20
20
 
21
21
  def create_backend(backend_usage)
22
22
  source_backend = Entities::Backend.new(id: backend_usage.backend_id, remote: source_remote)
23
- backend_context = create_backend_context(source_backend.system_name)
23
+ backend_context = create_backend_context(source_backend)
24
24
 
25
25
  tasks = []
26
26
  tasks << Commands::BackendCommand::CopyCommand::CreateOrUpdateTargetBackendTask.new(backend_context)
@@ -32,13 +32,16 @@ module ThreeScaleToolbox
32
32
 
33
33
  # CreateOrUpdate task will keep reference of the target backend in
34
34
  # backend_context[:target_backend]
35
+ target_backend = backend_context[:target_backend]
35
36
  attrs = {
36
- 'backend_api_id' => backend_context[:target_backend].id,
37
+ 'backend_api_id' => target_backend.id,
37
38
  'path' => backend_usage.path
38
39
  }
39
40
  # It is assumed there is no target backend usage with this backend_source's path
40
41
  # DeleteExistingTargetBackendUsagesTask should provide that
41
42
  Entities::BackendUsage.create(product: target, attrs: attrs)
43
+
44
+ backends_report.merge!(target_backend.system_name => backend_context.fetch(:report))
42
45
  end
43
46
 
44
47
  def source
@@ -57,11 +60,25 @@ module ThreeScaleToolbox
57
60
  context[:target_remote]
58
61
  end
59
62
 
60
- def create_backend_context(source_backend_system_name)
63
+ def backends_report
64
+ report['backends'] ||= {}
65
+ end
66
+
67
+ def report
68
+ context.fetch(:report)
69
+ end
70
+
71
+ def logger
72
+ context.fetch(:logger)
73
+ end
74
+
75
+ def create_backend_context(source_backend)
61
76
  {
62
77
  source_remote: source_remote,
63
78
  target_remote: target_remote,
64
- source_backend_ref: source_backend_system_name
79
+ source_backend: source_backend,
80
+ source_backend_ref: source_backend.id,
81
+ logger: logger
65
82
  }
66
83
  end
67
84
  end
@@ -0,0 +1,81 @@
1
+ module ThreeScaleToolbox
2
+ module Commands
3
+ module ProductCommand
4
+ class ExportSubcommand < Cri::CommandRunner
5
+ include ThreeScaleToolbox::Command
6
+
7
+ def self.command
8
+ Cri::Command.define do
9
+ name 'export'
10
+ usage 'export [opts] <remote> <product>'
11
+ summary 'Export product to serialized format'
12
+ description 'This command serializes the referenced product and associated backends into a yaml format'
13
+
14
+ option :f, :file, 'Write to file instead of stdout', argument: :required
15
+ param :remote
16
+ param :product_ref
17
+
18
+ runner ExportSubcommand
19
+ end
20
+ end
21
+
22
+ def run
23
+ select_output do |output|
24
+ output.write(serialized_object.to_yaml)
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def remote
31
+ @remote ||= threescale_client(arguments[:remote])
32
+ end
33
+
34
+ def serialized_object
35
+ {
36
+ 'apiVersion' => 'v1',
37
+ 'kind' => 'List',
38
+ 'items' => [product.to_cr] + product_backends.map(&:to_cr)
39
+ }
40
+ end
41
+
42
+ def select_output
43
+ ios = if file
44
+ File.open(file, 'w')
45
+ else
46
+ $stdout
47
+ end
48
+ begin
49
+ yield(ios)
50
+ ensure
51
+ ios.close
52
+ end
53
+ end
54
+
55
+ def product
56
+ @product ||= find_product
57
+ end
58
+
59
+ def product_backends
60
+ product.backend_usage_list.map do |backend_usage|
61
+ Entities::Backend.new(id: backend_usage.backend_id, remote: remote)
62
+ end
63
+ end
64
+
65
+ def product_ref
66
+ arguments[:product_ref]
67
+ end
68
+
69
+ def find_product
70
+ Entities::Service.find(remote: remote, ref: product_ref).tap do |prd|
71
+ raise ThreeScaleToolbox::Error, "Product #{product_ref} does not exist" if prd.nil?
72
+ end
73
+ end
74
+
75
+ def file
76
+ options[:file]
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,125 @@
1
+ module ThreeScaleToolbox
2
+ module Commands
3
+ module ProductCommand
4
+ class ImportSubcommand < Cri::CommandRunner
5
+ include ThreeScaleToolbox::Command
6
+ include ThreeScaleToolbox::ResourceReader
7
+
8
+ def self.command
9
+ Cri::Command.define do
10
+ name 'import'
11
+ usage 'import [opts] <remote>'
12
+ summary 'Import product from serialized format'
13
+ description 'This command deserializes one product and associated backends'
14
+
15
+ option :f, :file, 'Read from file instead of stdin', argument: :required
16
+ ThreeScaleToolbox::CLI.output_flag(self)
17
+ param :remote
18
+
19
+ runner ImportSubcommand
20
+ end
21
+ end
22
+
23
+ def run
24
+ validate_artifacts_resource!
25
+
26
+ product_list.each do |product|
27
+ context = {
28
+ target_remote: remote,
29
+ source_remote: crd_remote,
30
+ source_service_ref: product.system_name,
31
+ logger: Logger.new(File::NULL)
32
+ }
33
+
34
+ Commands::ProductCommand::CopySubcommand.workflow(context)
35
+
36
+ report[product.system_name] = context.fetch(:report)
37
+ end
38
+
39
+ printer.print_collection report
40
+ end
41
+
42
+ private
43
+
44
+ def crd_remote
45
+ @crd_remote ||= CRD::Remote.new(product_list, backend_list)
46
+ end
47
+
48
+ def product_list
49
+ @product_list ||= product_resources.map do |product_cr|
50
+ CRD::ProductParser.new product_cr
51
+ end
52
+ end
53
+
54
+ def backend_list
55
+ @backend_list ||= backend_resources.map do |backend_cr|
56
+ CRD::BackendParser.new backend_cr
57
+ end
58
+ end
59
+
60
+ def validate_artifacts_resource!
61
+ # TODO: Add openapiV3 validation
62
+ # https://github.com/3scale/3scale-operator/blob/3scale-2.10.0-CR2/deploy/crds/capabilities.3scale.net_backends_crd.yaml
63
+ # https://github.com/3scale/3scale-operator/blob/3scale-2.10.0-CR2/deploy/crds/capabilities.3scale.net_products_crd.yaml
64
+ validate_api_version!
65
+
66
+ validate_kind!
67
+ end
68
+
69
+ def validate_api_version!
70
+ artifacts_resource.fetch('apiVersion') do
71
+ raise ThreeScaleToolbox::Error, 'Invalid content. apiVersion not found'
72
+ end
73
+
74
+ raise ThreeScaleToolbox::Error, 'Invalid content. apiVersion wrong value ' unless artifacts_resource.fetch('apiVersion') == 'v1'
75
+ end
76
+
77
+ def validate_kind!
78
+ artifacts_resource.fetch('kind') do
79
+ raise ThreeScaleToolbox::Error, 'Invalid content. kind not found'
80
+ end
81
+
82
+ raise ThreeScaleToolbox::Error, 'Invalid content. kind wrong value ' unless artifacts_resource.fetch('kind') == 'List'
83
+ end
84
+
85
+ def artifacts_resource_items
86
+ artifacts_resource.fetch('items') do
87
+ raise ThreeScaleToolbox::Error, 'Invalid content. items not found'
88
+ end
89
+ end
90
+
91
+ def product_resources
92
+ artifacts_resource_items.select do |item|
93
+ item.respond_to?(:has_key?) &&
94
+ item.fetch('apiVersion', '').include?('capabilities.3scale.net') &&
95
+ item['kind'] == 'Product'
96
+ end
97
+ end
98
+
99
+ def backend_resources
100
+ artifacts_resource_items.select do |item|
101
+ item.respond_to?(:has_key?) &&
102
+ item.fetch('apiVersion', '').include?('capabilities.3scale.net') &&
103
+ item['kind'] == 'Backend'
104
+ end
105
+ end
106
+
107
+ def artifacts_resource
108
+ @artifacts_resource ||= load_resource(options[:file] || '-')
109
+ end
110
+
111
+ def report
112
+ @report ||= {}
113
+ end
114
+
115
+ def remote
116
+ @remote ||= threescale_client(arguments[:remote])
117
+ end
118
+
119
+ def printer
120
+ options.fetch(:output, CLI::JsonPrinter.new)
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end