├── .gitignore ├── .rspec ├── .travis.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── exe └── kumogata2 ├── kumogata2.gemspec ├── lib ├── kumogata2.rb └── kumogata2 │ ├── cli │ └── option_parser.rb │ ├── client.rb │ ├── ext │ ├── coderay_ext.rb │ └── string_ext.rb │ ├── logger.rb │ ├── plugin.rb │ ├── plugin │ ├── json.rb │ └── yaml.rb │ ├── utils.rb │ └── version.rb └── spec ├── kumogata2_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.json 11 | *.yaml 12 | test.rb 13 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.2.4 4 | before_install: gem install bundler -v 1.11.2 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in kumogata2.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Genki Sugawara 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kumogata2 2 | 3 | Kumogata2 is a tool for [AWS CloudFormation](https://aws.amazon.com/cloudformation/). 4 | 5 | This is a `format converter` + `useful tool`. 6 | 7 | [![Gem Version](https://badge.fury.io/rb/kumogata2.png?201406152020)](http://badge.fury.io/rb/kumogata2) 8 | 9 | ## Installation 10 | 11 | Add this line to your application's Gemfile: 12 | 13 | ```ruby 14 | gem 'kumogata2' 15 | ``` 16 | 17 | And then execute: 18 | 19 | $ bundle 20 | 21 | Or install it yourself as: 22 | 23 | $ gem install kumogata2 24 | 25 | ## Usage 26 | 27 | ``` 28 | Usage: kumogata2 [args] [options] 29 | 30 | Commands: 31 | describe STACK_NAME Describe a specified stack 32 | create PATH_OR_URL [STACK_NAME] Create resources as specified in the template 33 | update PATH_OR_URL STACK_NAME Update a stack as specified in the template 34 | delete STACK_NAME Delete a specified stack 35 | deploy PATH_OR_URL STACK_NAME Create a change set and executes it 36 | validate PATH_OR_URL Validate a specified template 37 | list [STACK_NAME] List summary information for stacks 38 | export STACK_NAME Export a template from a specified stack 39 | convert PATH_OR_URL Convert a template format 40 | diff PATH_OR_URL1 PATH_OR_URL2 Compare templates logically (file, http://..., stack://...) 41 | dry-run PATH_OR_URL STACK_NAME Create a change set and show it 42 | show-events STACK_NAME Show events for a specified stack 43 | show-outputs STACK_NAME Show outputs for a specified stack 44 | show-resources STACK_NAME Show resources for a specified stack 45 | template-summary PATH_OR_URL Show template information for a specified stack 46 | 47 | Support Format: 48 | json, js, template 49 | 50 | Options: 51 | -k, --access-key ACCESS_KEY 52 | -s, --secret-key SECRET_KEY 53 | -r, --region REGION 54 | --profile PROFILE 55 | --credentials-path PATH 56 | --output-format FORMAT 57 | -p, --parameters KEY_VALUES 58 | -j, --json-parameters JSON 59 | --[no-]deletion-policy-retain 60 | --[no-]disable-rollback 61 | --timeout-in-minutes TIMEOUT_IN_MINUTES 62 | --notification-arns NOTIFICATION_ARNS 63 | --capabilities CAPABILITIES 64 | --resource-types RESOURCE_TYPES 65 | --on-failure ON_FAILURE 66 | --stack-policy-body STACK_POLICY_BODY 67 | --stack-policy-url STACK_POLICY_URL 68 | --[no-]use-previous-template 69 | --stack-policy-during-update-body STACK_POLICY_DURING_UPDATE_BODY 70 | --stack-policy-during-update-url STACK_POLICY_DURING_UPDATE_URL 71 | --tags TAGS 72 | --result-log PATH 73 | --command-result-log PATH 74 | --[no-]detach 75 | --[no-]force 76 | --[no-]color 77 | --[no-]ignore-all-space 78 | --[no-]debug 79 | ``` 80 | 81 | ### Environment variables 82 | 83 | ```sh 84 | export AWS_SECRET_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE 85 | export AWS_ACCESS_KEY_ID=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY 86 | export AWS_REGION=us-east-1 87 | ``` 88 | 89 | ### Create resources 90 | 91 | $ kumogata2 create template.rb 92 | 93 | If you want to save the stack, please specify the stack name: 94 | 95 | $ kumogata2 create template.rb any_stack_name 96 | 97 | If you want to pass parameters, please use `-p` option: 98 | 99 | $ kumogata2 create template.rb -p "InstanceType=m1.large,KeyName=any_other_key" 100 | 101 | 102 | **Notice** 103 | 104 | **The stack will be delete if you do not specify the stack name explicitly.** 105 | (And only the resources will remain) 106 | 107 | ## Plugin 108 | 109 | Kumogata2 can be extended with plug-ins, such as the following: 110 | 111 | ```ruby 112 | class Kumogata2::Plugin::JSON 113 | Kumogata2::Plugin.register(:json, ['json', 'js', 'template'], self) 114 | 115 | def initialize(options) 116 | @options = options 117 | end 118 | 119 | def parse(str) 120 | JSON.parse(str) 121 | end 122 | 123 | def dump(hash) 124 | JSON.pretty_generate(hash).colorize_as(:json) 125 | end 126 | end 127 | ``` 128 | 129 | see [kumogata2-plugin-ruby](https://github.com/winebarrel/kumogata2-plugin-ruby). 130 | 131 | ## Similar tools 132 | * [Codenize.tools](http://codenize.tools/) 133 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "kumogata2" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /exe/kumogata2: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $: << File.expand_path('../../lib', __FILE__) 3 | require 'kumogata2' 4 | require 'kumogata2/cli/option_parser' 5 | 6 | Version = Kumogata2::VERSION 7 | 8 | def main(argv) 9 | debug = ARGV.include?('--debug') 10 | 11 | begin 12 | Kumogata2::Plugin.load_plugins 13 | 14 | parsed = Kumogata2::CLI::OptionParser.parse!(argv) 15 | command, arguments, options, output_result = parsed 16 | debug = options.debug? 17 | 18 | out = Kumogata2::Client.new(options).send(command, *arguments) 19 | 20 | if [:create, :update, :delete].include?(command) and options.detach? 21 | puts '[detached]' 22 | elsif output_result and out 23 | puts out 24 | end 25 | rescue Exception => e 26 | raise e if e.kind_of?(SystemExit) 27 | 28 | if not e.kind_of?(Interrupt) 29 | $stderr.puts("[ERROR] #{e.message}".red) 30 | end 31 | 32 | if debug 33 | raise e 34 | else 35 | backtrace = Kumogata2::Utils.filter_backtrace(e.backtrace) 36 | 37 | unless backtrace.empty? 38 | $stderr.puts " from #{backtrace.first}".red 39 | end 40 | end 41 | 42 | exit 1 43 | end 44 | end 45 | 46 | main(ARGV) 47 | -------------------------------------------------------------------------------- /kumogata2.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'kumogata2/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'kumogata2' 8 | spec.version = Kumogata2::VERSION 9 | spec.authors = ['Genki Sugawara'] 10 | spec.email = ['sugawara@cookpad.com'] 11 | 12 | spec.summary = %q{Kumogata2 is a tool for AWS CloudFormation.} 13 | spec.description = %q{Kumogata2 is a tool for AWS CloudFormation.} 14 | spec.homepage = 'https://github.com/winebarrel/kumogata2' 15 | spec.license = 'MIT' 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 18 | spec.bindir = 'exe' 19 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 20 | spec.require_paths = ['lib'] 21 | 22 | spec.add_dependency 'aws-sdk', '~> 3.0' 23 | spec.add_dependency 'coderay', '~> 1.1' 24 | spec.add_dependency 'diffy', '~> 3.2' 25 | spec.add_dependency 'hashie', '~> 3.5' 26 | spec.add_dependency 'highline', '~> 2.0' 27 | spec.add_dependency 'term-ansicolor', '~> 1.6' 28 | 29 | spec.add_development_dependency 'bundler', '~> 1.16' 30 | spec.add_development_dependency 'rake', '~> 12.3' 31 | spec.add_development_dependency 'rspec', '~> 3.0' 32 | spec.add_development_dependency 'kumogata2-plugin-ruby', '~> 0.1' 33 | end 34 | -------------------------------------------------------------------------------- /lib/kumogata2.rb: -------------------------------------------------------------------------------- 1 | require 'aws-sdk' 2 | require 'coderay' 3 | require 'diffy' 4 | require 'hashie' 5 | require 'highline/import' 6 | require 'json' 7 | require 'logger' 8 | require 'open-uri' 9 | require 'optparse' 10 | require 'securerandom' 11 | require 'singleton' 12 | require 'term/ansicolor' 13 | 14 | require 'kumogata2/version' 15 | require 'kumogata2/ext/coderay_ext' 16 | require 'kumogata2/ext/string_ext' 17 | require 'kumogata2/logger' 18 | require 'kumogata2/utils' 19 | 20 | require 'kumogata2/plugin' 21 | require 'kumogata2/plugin/json' 22 | require 'kumogata2/plugin/yaml' 23 | 24 | require 'kumogata2/client' 25 | -------------------------------------------------------------------------------- /lib/kumogata2/cli/option_parser.rb: -------------------------------------------------------------------------------- 1 | module Kumogata2::CLI 2 | class OptionParser 3 | DEFAULT_OPTIONS = { 4 | result_log: File.join(Dir.pwd, 'result.yaml'), 5 | color: $stdout.tty?, 6 | } 7 | DEFAULT_OPTION_RETRY_LIMIT = 100 8 | 9 | COMMANDS = { 10 | describe: { 11 | description: 'Describe a specified stack', 12 | arguments: [:stack_name], 13 | }, 14 | create: { 15 | description: 'Create resources as specified in the template', 16 | arguments: [:path_or_url, :stack_name?], 17 | output: false, 18 | }, 19 | update: { 20 | description: 'Update a stack as specified in the template', 21 | arguments: [:path_or_url, :stack_name], 22 | output: false, 23 | }, 24 | delete: { 25 | description: 'Delete a specified stack', 26 | arguments: [:stack_name], 27 | output: false, 28 | }, 29 | deploy: { 30 | description: 'Create a change set and executes it', 31 | arguments: [:path_or_url, :stack_name], 32 | output: false, 33 | }, 34 | validate: { 35 | description: 'Validate a specified template', 36 | arguments: [:path_or_url], 37 | output: false, 38 | }, 39 | list: { 40 | description: 'List summary information for stacks', 41 | arguments: [:stack_name?], 42 | }, 43 | export: { 44 | description: 'Export a template from a specified stack', 45 | arguments: [:stack_name], 46 | }, 47 | convert: { 48 | description: 'Convert a template format', 49 | arguments: [:path_or_url], 50 | }, 51 | diff: { 52 | description: 'Compare templates logically (file, http://..., stack://...)', 53 | arguments: [:path_or_url1, :path_or_url2], 54 | }, 55 | dry_run: { 56 | description: 'Create a change set and show it', 57 | arguments: [:path_or_url, :stack_name], 58 | }, 59 | show_events: { 60 | description: 'Show events for a specified stack', 61 | arguments: [:stack_name], 62 | }, 63 | show_outputs: { 64 | description: 'Show outputs for a specified stack', 65 | arguments: [:stack_name], 66 | }, 67 | show_resources: { 68 | description: 'Show resources for a specified stack', 69 | arguments: [:stack_name], 70 | }, 71 | template_summary: { 72 | description: 'Show template information for a specified stack', 73 | arguments: [:path_or_url], 74 | }, 75 | } 76 | 77 | class << self 78 | def parse!(argv) 79 | self.new.parse!(argv) 80 | end 81 | end # of class methods 82 | 83 | def parse!(argv) 84 | command = nil 85 | arguments = nil 86 | # https://github.com/aws/aws-sdk-ruby/blob/v2.3.11/aws-sdk-core/lib/aws-sdk-core/plugins/regional_endpoint.rb#L18 87 | region_keys = %w(AWS_REGION AMAZON_REGION AWS_DEFAULT_REGION) 88 | options = { aws: { region: ENV.values_at(*region_keys).compact.first, 89 | retry_limit: DEFAULT_OPTION_RETRY_LIMIT } } 90 | 91 | opt = ::OptionParser.new 92 | opt.summary_width = 65535 93 | set_usage!(opt) 94 | 95 | opt.on('-k', '--access-key ACCESS_KEY') {|v| options[:aws][:access_key_id] = v } 96 | opt.on('-s', '--secret-key SECRET_KEY') {|v| options[:aws][:secret_access_key] = v } 97 | opt.on('-r', '--region REGION') {|v| options[:aws][:region] = v } 98 | opt.on('', '--retry-limit LIMIT') {|v| options[:aws][:retry_limit] = v } 99 | 100 | opt.on('', '--profile PROFILE') do |v| 101 | options[:aws][:credentials] ||= {} 102 | options[:aws][:credentials][:profile_name] = v 103 | end 104 | 105 | opt.on('', '--credentials-path PATH') do |v| 106 | options[:aws][:credentials] ||= {} 107 | options[:aws][:credentials][:path] = v 108 | end 109 | 110 | plugin_exts = Kumogata2::Plugin.plugins.flat_map(&:ext).uniq 111 | opt.on('' , '--output-format FORMAT', plugin_exts) do |v| 112 | options[:output_format] = v 113 | end 114 | 115 | opt.on('-p', '--parameters KEY_VALUES', Array) {|v| options[:parameters] = v } 116 | opt.on('-j', '--json-parameters JSON') {|v| options[:json_parameters] = v } 117 | opt.on('' , '--[no-]deletion-policy-retain') {|v| options[:deletion_policy_retain] = v } 118 | 119 | { 120 | disable_rollback: :boolean, 121 | timeout_in_minutes: Integer, 122 | notification_arns: Array, 123 | capabilities: Array, 124 | resource_types: Array, 125 | on_failure: nil, 126 | stack_policy_body: nil, 127 | stack_policy_url: nil, 128 | use_previous_template: :boolean, 129 | stack_policy_during_update_body: nil, 130 | stack_policy_during_update_url: nil, 131 | tags: Array, 132 | }.each do |key, type| 133 | opt_str = key.to_s.gsub('_', '-') 134 | opt_val = key.to_s.upcase 135 | 136 | case type 137 | when :boolean 138 | opt.on('', "--[no-]#{opt_str}") {|v| options[key] = v } 139 | when nil 140 | opt.on('', "--#{opt_str} #{opt_val}") {|v| options[key] = v } 141 | else 142 | opt.on('', "--#{opt_str} #{opt_val}", type) {|v| options[key] = v } 143 | end 144 | end 145 | 146 | opt.on('' , '--result-log PATH') {|v| options[:result_log] = v } 147 | opt.on('' , '--command-result-log PATH') {|v| options[:command] = v } 148 | opt.on('' , '--[no-]detach') {|v| options[:detach] = v } 149 | opt.on('' , '--[no-]force') {|v| options[:force] = v } 150 | opt.on('' , '--[no-]color') {|v| options[:color] = v } 151 | opt.on('' , '--[no-]ignore-all-space') {|v| options[:ignore_all_space] = v } 152 | opt.on('' , '--[no-]debug') {|v| options[:debug] = v } 153 | 154 | opt.parse! 155 | 156 | unless (command = argv.shift) 157 | puts opt.help 158 | exit_parse!(1) 159 | end 160 | 161 | orig_command = command 162 | command = command.gsub('-', '_').to_sym 163 | command = find_command(command) 164 | 165 | unless command 166 | raise "Unknown command: #{orig_command}" 167 | end 168 | 169 | arguments = argv.dup 170 | validate_arguments(command, arguments) 171 | 172 | options = DEFAULT_OPTIONS.merge(options) 173 | options = Hashie::Mash.new(options) 174 | 175 | if options[:aws][:credentials] 176 | credentials = Aws::SharedCredentials.new(options[:aws][:credentials]) 177 | options[:aws][:credentials] = credentials 178 | end 179 | 180 | Aws.config.update(options[:aws].dup) 181 | options = Hashie::Mash.new(options) 182 | 183 | String.colorize = options.color? 184 | Diffy::Diff.default_format = options.color? ? :color : :text 185 | 186 | if options.debug? 187 | Kumogata2::Logger.instance.set_debug(options.debug?) 188 | 189 | Aws.config.update( 190 | http_wire_trace: true, 191 | logger: Kumogata2::Logger.instance 192 | ) 193 | end 194 | 195 | update_parameters(options) 196 | output = COMMANDS.fetch(command).fetch(:output, true) 197 | 198 | options.command = command 199 | options.arguments = arguments 200 | options.output_result = output 201 | 202 | [command, arguments, options, output] 203 | end 204 | 205 | private 206 | 207 | def find_command(command) 208 | selected = COMMANDS.keys.select {|i| i =~ /\A#{command}/ } 209 | 210 | if selected.length == 1 211 | selected.first 212 | else 213 | nil 214 | end 215 | end 216 | 217 | def exit_parse!(exit_code) 218 | exit(exit_code) 219 | end 220 | 221 | def set_usage!(opt) 222 | opt.banner = "Usage: kumogata2 [args] [options]" 223 | opt.separator '' 224 | opt.separator 'Commands:' 225 | 226 | cmd_max_len = COMMANDS.keys.map {|i| i.to_s.length }.max 227 | 228 | cmd_arg_descs = COMMANDS.map do |command, attributes| 229 | command = command.to_s.gsub('_', '-') 230 | description = attributes.fetch(:description) 231 | arguments = attributes.fetch(:arguments) 232 | 233 | [ 234 | '%-*s %s' % [cmd_max_len, command, arguments_to_message(arguments)], 235 | description, 236 | ] 237 | end 238 | 239 | cmd_arg_max_len = cmd_arg_descs.map {|i| i[0].length }.max 240 | 241 | opt.separator(cmd_arg_descs.map {|cmd_arg, desc| 242 | ' %-*s %-s' % [cmd_arg_max_len, cmd_arg, desc] 243 | }.join("\n")) 244 | 245 | opt.separator '' 246 | opt.separator 'Plugins: ' 247 | 248 | Kumogata2::Plugin.plugins.each do |plugin| 249 | opt.separator " #{plugin.name}: #{plugin.ext.join(', ')}" 250 | end 251 | 252 | opt.separator '' 253 | opt.separator 'Options:' 254 | end 255 | 256 | def arguments_to_message(arguments) 257 | arguments.map {|i| i.to_s.sub(/(.+)\?\z/) { "[#{$1}]" }.upcase }.join(' ') 258 | end 259 | 260 | def validate_arguments(command, arguments) 261 | expected = COMMANDS[command][:arguments] || [] 262 | 263 | min = expected.count {|i| i.to_s !~ /\?\z/ } 264 | max = expected.length 265 | 266 | if arguments.length < min or max < arguments.length 267 | raise "Usage: kumogata2 #{command} #{arguments_to_message(expected)} [options]" 268 | end 269 | end 270 | 271 | def update_parameters(options) 272 | parameters = {} 273 | 274 | (options.parameters || []).each do |i| 275 | key, value = i.split('=', 2) 276 | parameters[key] = value 277 | end 278 | 279 | if options.json_parameters 280 | parameters.merge!(JSON.parse(options.json_parameters)) 281 | end 282 | 283 | options.parameters = parameters 284 | end 285 | end # OptionParser 286 | end # Kumogata2::CLI 287 | -------------------------------------------------------------------------------- /lib/kumogata2/client.rb: -------------------------------------------------------------------------------- 1 | class Kumogata2::Client 2 | include Kumogata2::Logger::Helper 3 | 4 | def initialize(options) 5 | @options = options.kind_of?(Hashie::Mash) ? options : Hashie::Mash.new(options) 6 | @client = nil 7 | @resource = nil 8 | @plugin_by_ext = {} 9 | end 10 | 11 | def describe(stack_name) 12 | stack_name = normalize_stack_name(stack_name) 13 | validate_stack_name(stack_name) 14 | stack = describe_stack(stack_name) 15 | JSON.pretty_generate(stack).colorize_as(:json) 16 | end 17 | 18 | def create(path_or_url, stack_name = nil) 19 | stack_name = normalize_stack_name(stack_name) 20 | validate_stack_name(stack_name) if stack_name 21 | template = open_template(path_or_url) 22 | update_deletion_policy(template, delete_stack: !stack_name) 23 | 24 | outputs = create_stack(template, stack_name) 25 | 26 | unless @options.detach? 27 | post_process(path_or_url, outputs) 28 | end 29 | end 30 | 31 | def update(path_or_url, stack_name) 32 | stack_name = normalize_stack_name(stack_name) 33 | validate_stack_name(stack_name) 34 | template = open_template(path_or_url) 35 | update_deletion_policy(template, update_metadate: true) 36 | 37 | outputs = update_stack(template, stack_name) 38 | 39 | unless @options.detach? 40 | post_process(path_or_url, outputs) 41 | end 42 | end 43 | 44 | def delete(stack_name) 45 | stack_name = normalize_stack_name(stack_name) 46 | validate_stack_name(stack_name) 47 | get_resource.stack(stack_name).stack_status 48 | 49 | if @options.force? or agree("Are you sure you want to delete `#{stack_name}`? ".yellow) 50 | delete_stack(stack_name) 51 | end 52 | end 53 | 54 | def deploy(path_or_url, stack_name = nil) 55 | stack_name = normalize_stack_name(stack_name) 56 | validate_stack_name(stack_name) if stack_name 57 | template = open_template(path_or_url) 58 | update_deletion_policy(template, delete_stack: !stack_name) 59 | 60 | change_set = create_change_set(template, stack_name) 61 | execute_change_set(change_set) 62 | end 63 | 64 | def validate(path_or_url) 65 | template = open_template(path_or_url) 66 | validate_template(template) 67 | end 68 | 69 | def list(stack_name = nil) 70 | stack_name = normalize_stack_name(stack_name) 71 | validate_stack_name(stack_name) if stack_name 72 | stacks = describe_stacks(stack_name) 73 | JSON.pretty_generate(stacks).colorize_as(:json) 74 | end 75 | 76 | def export(stack_name) 77 | stack_name = normalize_stack_name(stack_name) 78 | validate_stack_name(stack_name) 79 | template = export_template(stack_name) 80 | convert0(template) 81 | end 82 | 83 | def convert(path_or_url) 84 | template = open_template(path_or_url) 85 | convert0(template) 86 | end 87 | 88 | def diff(path_or_url1, path_or_url2) 89 | templates = [path_or_url1, path_or_url2].map do |path_or_url| 90 | template = nil 91 | 92 | if path_or_url =~ %r|\Astack://(.*)| 93 | stack_name = $1 || '' 94 | validate_stack_name(stack_name) 95 | template = export_template(stack_name) 96 | else 97 | template = open_template(path_or_url) 98 | end 99 | 100 | template = Kumogata2::Utils.stringify(template) 101 | JSON.pretty_generate(template) 102 | end 103 | 104 | diff_opts = @options.ignore_all_space? ? '-uw' : '-u' 105 | opts = {:include_diff_info => true, :diff => diff_opts} 106 | diff = Diffy::Diff.new(*templates, opts).to_s 107 | 108 | diff.sub(/^(\e\[\d+m)?\-\-\-(\s+)(\S+)/m) { "#{$1}---#{$2}#{path_or_url1}"} 109 | .sub(/^(\e\[\d+m)?\+\+\+(\s+)(\S+)/m) { "#{$1}+++#{$2}#{path_or_url2}"} 110 | end 111 | 112 | def dry_run(path_or_url, stack_name = nil) 113 | stack_name = normalize_stack_name(stack_name) 114 | validate_stack_name(stack_name) if stack_name 115 | template = open_template(path_or_url) 116 | update_deletion_policy(template, delete_stack: !stack_name) 117 | changes = show_change_set(template, stack_name) 118 | changes = JSON.pretty_generate(changes).colorize_as(:json) if changes 119 | changes 120 | end 121 | 122 | def show_events(stack_name) 123 | stack_name = normalize_stack_name(stack_name) 124 | validate_stack_name(stack_name) 125 | events = describe_events(stack_name) 126 | JSON.pretty_generate(events).colorize_as(:json) 127 | end 128 | 129 | def show_outputs(stack_name) 130 | stack_name = normalize_stack_name(stack_name) 131 | validate_stack_name(stack_name) 132 | outputs = describe_outputs(stack_name) 133 | JSON.pretty_generate(outputs).colorize_as(:json) 134 | end 135 | 136 | def show_resources(stack_name) 137 | stack_name = normalize_stack_name(stack_name) 138 | validate_stack_name(stack_name) 139 | resources = describe_resources(stack_name) 140 | JSON.pretty_generate(resources).colorize_as(:json) 141 | end 142 | 143 | def template_summary(path_or_url) 144 | params = {} 145 | 146 | if path_or_url =~ %r|\Astack://(.*)| 147 | stack_name = $1 || '' 148 | validate_stack_name(stack_name) 149 | params[:stack_name] = stack_name 150 | else 151 | template = open_template(path_or_url) 152 | params[:template_body] = JSON.pretty_generate(template) 153 | end 154 | 155 | summary = describe_template_summary(params) 156 | JSON.pretty_generate(summary).colorize_as(:json) 157 | end 158 | 159 | private 160 | 161 | def get_client 162 | return @client unless @client.nil? 163 | 164 | # https://github.com/aws/aws-sdk-ruby/blob/v2.3.11/aws-sdk-core/lib/aws-sdk-core/plugins/regional_endpoint.rb#L29 165 | unless @options[:aws][:region] 166 | raise "missing region; use '--region' option or export region name to ENV['AWS_REGION']" 167 | end 168 | 169 | @client = Aws::CloudFormation::Client.new(@options.aws) 170 | end 171 | 172 | def get_resource 173 | return @resource unless @resource.nil? 174 | get_client if @client.nil? 175 | @resource = Aws::CloudFormation::Resource.new(client: @client) 176 | end 177 | 178 | def describe_stack(stack_name) 179 | resp = get_client.describe_stacks(stack_name: stack_name) 180 | resp.stacks.first.to_h 181 | end 182 | 183 | def create_stack(template, stack_name) 184 | stack_will_be_deleted = !stack_name 185 | 186 | unless stack_name 187 | stack_name = random_stack_name 188 | end 189 | 190 | log(:info, "Creating stack: #{stack_name}", color: :cyan) 191 | 192 | params = { 193 | stack_name: stack_name, 194 | template_body: convert_output_value(template, false), 195 | parameters: parameters_array, 196 | } 197 | 198 | params.merge!(set_api_params(params, 199 | :disable_rollback, 200 | :timeout_in_minutes, 201 | :notification_arns, 202 | :capabilities, 203 | :resource_types, 204 | :on_failure, 205 | :stack_policy_body, 206 | :stack_policy_url, 207 | :tags) 208 | ) 209 | 210 | stack = get_resource.create_stack(params) 211 | 212 | return if @options.detach? 213 | 214 | completed = wait(stack, 'CREATE_COMPLETE') 215 | 216 | unless completed 217 | raise_stack_error!(stack, 'Create failed') 218 | end 219 | 220 | outputs = outputs_for(stack) 221 | summaries = resource_summaries_for(stack) 222 | 223 | if stack_will_be_deleted 224 | delete_stack(stack_name) 225 | end 226 | 227 | output_result(stack_name, outputs, summaries) 228 | 229 | outputs 230 | end 231 | 232 | def update_stack(template, stack_name) 233 | stack = get_resource.stack(stack_name) 234 | stack.stack_status 235 | 236 | log(:info, "Updating stack: #{stack_name}", color: :green) 237 | 238 | params = { 239 | stack_name: stack_name, 240 | template_body: convert_output_value(template, false), 241 | parameters: parameters_array, 242 | } 243 | 244 | params.merge!(set_api_params(params, 245 | :use_previous_template, 246 | :stack_policy_during_update_body, 247 | :stack_policy_during_update_url, 248 | :notification_arns, 249 | :capabilities, 250 | :resource_types, 251 | :stack_policy_body, 252 | :stack_policy_url, 253 | :tags) 254 | ) 255 | 256 | event_log = create_event_log(stack) 257 | stack.update(params) 258 | 259 | return if @options.detach? 260 | 261 | # XXX: Reacquire the stack 262 | stack = get_resource.stack(stack_name) 263 | completed = wait(stack, 'UPDATE_COMPLETE', event_log) 264 | 265 | unless completed 266 | raise_stack_error!(stack, 'Update failed') 267 | end 268 | 269 | outputs = outputs_for(stack) 270 | summaries = resource_summaries_for(stack) 271 | 272 | output_result(stack_name, outputs, summaries) 273 | 274 | outputs 275 | end 276 | 277 | def delete_stack(stack_name) 278 | stack = get_resource.stack(stack_name) 279 | stack.stack_status 280 | 281 | log(:info, "Deleting stack: #{stack_name}", color: :red) 282 | event_log = create_event_log(stack) 283 | stack.delete 284 | 285 | return if @options.detach? 286 | 287 | completed = false 288 | 289 | begin 290 | # XXX: Reacquire the stack 291 | stack = get_resource.stack(stack_name) 292 | completed = wait(stack, 'DELETE_COMPLETE', event_log) 293 | rescue Aws::CloudFormation::Errors::ValidationError 294 | # Handle `Stack does not exist` 295 | completed = true 296 | end 297 | 298 | unless completed 299 | raise_stack_error!(stack, 'Delete failed') 300 | end 301 | 302 | log(:info, 'Delete stack successfully') 303 | end 304 | 305 | def validate_template(template) 306 | get_client.validate_template(template_body: convert_output_value(template, false)) 307 | log(:info, 'Template validated successfully', color: :green) 308 | end 309 | 310 | def describe_stacks(stack_name) 311 | params = {} 312 | params[:stack_name] = stack_name if stack_name 313 | 314 | get_resource.stacks(params).map do |stack| 315 | { 316 | 'StackName' => stack.name, 317 | 'CreationTime' => stack.creation_time, 318 | 'StackStatus' => stack.stack_status, 319 | 'Description' => stack.description, 320 | } 321 | end 322 | end 323 | 324 | def export_template(stack_name) 325 | stack = get_resource.stack(stack_name) 326 | stack.stack_status 327 | template = stack.client.get_template(stack_name: stack_name).template_body 328 | JSON.parse(template) 329 | end 330 | 331 | def create_change_set(template, stack_name) 332 | change_set_name = [stack_name, SecureRandom.uuid].join('-') 333 | 334 | begin 335 | stack = describe_stack(stack_name) 336 | case stack[:stack_status] 337 | when /_FAILED\z/ 338 | log(:error, "Stack #{stack_name} status is now failed - #{stack[:stack_status]}", color: :red) 339 | return 340 | when /^DELETE_/ 341 | log(:error, "Stack #{stack_name} statis is now delete - #{stack[:stack_status]}", color: :red) 342 | return 343 | when 'REVIEW_IN_PROGRESS' 344 | change_set_type = 'CREATE' 345 | else 346 | change_set_type = 'UPDATE' 347 | end 348 | rescue 349 | change_set_type = 'CREATE' 350 | end 351 | 352 | log(:info, "Creating ChangeSet: #{change_set_name} for #{stack_name}", color: :cyan) 353 | 354 | params = { 355 | stack_name: stack_name, 356 | change_set_name: change_set_name, 357 | template_body: convert_output_value(template, false), 358 | parameters: parameters_array, 359 | change_set_type: change_set_type, 360 | } 361 | 362 | params.merge!(set_api_params(params, 363 | :use_previous_template, 364 | :notification_arns, 365 | :capabilities, 366 | :resource_types, 367 | :tags) 368 | ) 369 | 370 | resp = get_client.create_change_set(params) 371 | change_set_arn = resp.id 372 | 373 | completed, change_set = wait_change_set(change_set_arn, 'CREATE_COMPLETE') 374 | 375 | unless completed 376 | log(:error, "Create ChangeSet failed: #{change_set.status_reason}", color: :red) 377 | end 378 | 379 | change_set 380 | end 381 | 382 | def execute_change_set(change_set) 383 | if change_set.status != 'CREATE_COMPLETE' 384 | log(:info, "ChangeSet status is #{change_set.status}", color: :red) 385 | delete_change_set(change_set) 386 | return 387 | end 388 | 389 | log(:info, "Executing ChangeSet: #{change_set.change_set_name} for #{change_set.stack_name}", color: :cyan) 390 | 391 | get_client.execute_change_set(change_set_name: change_set.change_set_name, 392 | stack_name: change_set.stack_name) 393 | 394 | stack = get_resource.stack(change_set.stack_name) 395 | 396 | event_log = create_event_log(stack) 397 | 398 | completed = wait(stack, stack.stack_status, event_log) 399 | 400 | unless completed 401 | raise_stack_error!(stack, 'executing change set failed') 402 | end 403 | end 404 | 405 | def delete_change_set(change_set) 406 | log(:info, "Deleting ChangeSet: #{change_set.change_set_name}", color: :red) 407 | 408 | get_client.delete_change_set(stack_name: change_set.stack_name, 409 | change_set_name: change_set.change_set_id) 410 | 411 | begin 412 | completed, _ = wait_change_set(change_set.change_set_id, 'DELETE_COMPLETE') 413 | rescue Aws::CloudFormation::Errors::ChangeSetNotFound 414 | # Handle `ChangeSet does not exist` 415 | completed = true 416 | end 417 | 418 | completed 419 | end 420 | 421 | def show_change_set(template, stack_name) 422 | output = nil 423 | 424 | change_set = create_change_set(template, stack_name) 425 | output = changes_for(change_set) unless change_set.nil? 426 | 427 | delete_change_set(change_set) 428 | 429 | stack = get_resource.stack(stack_name) 430 | delete_stack(stack_name) if stack.stack_status == 'REVIEW_IN_PROGRESS' 431 | 432 | output 433 | end 434 | 435 | def describe_events(stack_name) 436 | stack = get_resource.stack(stack_name) 437 | stack.stack_status 438 | events_for(stack) 439 | end 440 | 441 | def describe_outputs(stack_name) 442 | stack = get_resource.stack(stack_name) 443 | stack.stack_status 444 | outputs_for(stack) 445 | end 446 | 447 | def describe_resources(stack_name) 448 | stack = get_resource.stack(stack_name) 449 | stack.stack_status 450 | resource_summaries_for(stack) 451 | end 452 | 453 | def describe_template_summary(params) 454 | resp = get_client.get_template_summary(params) 455 | resp.to_h 456 | end 457 | 458 | def get_output_format 459 | @options.output_format || 'template' 460 | end 461 | 462 | def convert0(template) 463 | ext = get_output_format 464 | plugin = find_or_create_plugin('xxx.' + ext) 465 | 466 | if plugin 467 | plugin.dump(template) 468 | else 469 | raise "Unknown format: #{ext}" 470 | end 471 | end 472 | 473 | def open_template(path_or_url) 474 | plugin = find_or_create_plugin(path_or_url) 475 | 476 | if plugin 477 | @options.path_or_url = path_or_url 478 | plugin.parse(open(path_or_url, &:read)) 479 | else 480 | raise "Unknown format: #{path_or_url}" 481 | end 482 | end 483 | 484 | def find_or_create_plugin(path_or_url) 485 | ext = File.extname(path_or_url).sub(/\A\./, '') 486 | 487 | if @plugin_by_ext.has_key?(ext) 488 | return @plugin_by_ext.fetch(ext) 489 | end 490 | 491 | plugin_class = Kumogata2::Plugin.find_by_ext(ext) 492 | plugin = plugin_class ? plugin_class.new(@options) : nil 493 | @plugin_by_ext[ext] = plugin 494 | end 495 | 496 | def update_deletion_policy(template, options = {}) 497 | if options[:delete_stack] or @options.deletion_policy_retain? 498 | template['Resources'].each do |k, v| 499 | next if /\AAWS::CloudFormation::/ =~ v['Type'] 500 | v['DeletionPolicy'] ||= 'Retain' 501 | 502 | if options[:update_metadate] 503 | v['Metadata'] ||= {} 504 | v['Metadata']['DeletionPolicyUpdateKeyForKumogata'] = "DeletionPolicyUpdateValueForKumogata#{Time.now.to_i}" 505 | end 506 | end 507 | end 508 | end 509 | 510 | def validate_stack_name(stack_name) 511 | unless /\A[a-zA-Z][-a-zA-Z0-9]*\Z/i =~ stack_name 512 | raise "1 validation error detected: Value '#{stack_name}' at 'stackName' failed to satisfy constraint: Member must satisfy regular expression pattern: [a-zA-Z][-a-zA-Z0-9]*" 513 | end 514 | end 515 | 516 | def parameters_array 517 | @options.parameters.map do |key, value| 518 | {parameter_key: key, parameter_value: value} 519 | end 520 | end 521 | 522 | def set_api_params(params, *keys) 523 | {}.tap do |h| 524 | keys.each do |k| 525 | @options[k].collect! do |v| 526 | key, value = v.split('=') 527 | { key: key, value: value } 528 | end if k == :tags and not @options[k].nil? 529 | h[k] = @options[k] if @options[k] 530 | end 531 | end 532 | end 533 | 534 | def wait(stack, complete_status, event_log = {}) 535 | before_wait = proc do |attempts, response| 536 | print_event_log(stack, event_log) 537 | end 538 | 539 | stack.wait_until(before_wait: before_wait, max_attempts: nil, delay: 1) do |s| 540 | s.stack_status !~ /_IN_PROGRESS\z/ 541 | end 542 | 543 | print_event_log(stack, event_log) 544 | 545 | completed = (stack.stack_status == complete_status) 546 | log(:info, completed ? 'Success' : 'Failure') 547 | 548 | completed 549 | end 550 | 551 | def wait_change_set(change_set_name, complete_status) 552 | change_set = nil 553 | 554 | loop do 555 | change_set = get_client.describe_change_set(change_set_name: change_set_name) 556 | 557 | if change_set.status !~ /(_PENDING|_IN_PROGRESS)\z/ 558 | break 559 | end 560 | 561 | sleep 1 562 | end 563 | 564 | completed = (change_set.status == complete_status) 565 | [completed, change_set] 566 | end 567 | 568 | def print_event_log(stack, event_log) 569 | events_for(stack).sort_by {|i| i['Timestamp'] }.each do |event| 570 | event_id = event['EventId'] 571 | 572 | unless event_log[event_id] 573 | event_log[event_id] = event 574 | 575 | timestamp = event['Timestamp'] 576 | summary = {} 577 | 578 | ['LogicalResourceId', 'ResourceStatus', 'ResourceStatusReason'].map do |k| 579 | summary[k] = event[k] 580 | end 581 | 582 | puts [ 583 | timestamp.getlocal.strftime('%Y/%m/%d %H:%M:%S %Z'), 584 | summary.to_json.colorize_as(:json), 585 | ].join(': ') 586 | end 587 | end 588 | end 589 | 590 | def create_event_log(stack) 591 | event_log = {} 592 | 593 | events_for(stack).sort_by {|i| i['Timestamp'] }.each do |event| 594 | event_id = event['EventId'] 595 | event_log[event_id] = event 596 | end 597 | 598 | return event_log 599 | end 600 | 601 | def events_for(stack) 602 | stack.events.map do |event| 603 | event_hash = {} 604 | 605 | [ 606 | :event_id, 607 | :logical_resource_id, 608 | :physical_resource_id, 609 | :resource_properties, 610 | :resource_status, 611 | :resource_status_reason, 612 | :resource_type, 613 | :stack_id, 614 | :stack_name, 615 | :timestamp, 616 | ].each do |k| 617 | event_hash[Kumogata2::Utils.camelize(k)] = event.send(k) 618 | end 619 | 620 | event_hash 621 | end 622 | end 623 | 624 | def outputs_for(stack) 625 | outputs_hash = {} 626 | 627 | stack.outputs.each do |output| 628 | outputs_hash[output.output_key] = output.output_value 629 | end 630 | 631 | outputs_hash 632 | end 633 | 634 | def resource_summaries_for(stack) 635 | stack.resource_summaries.map do |summary| 636 | summary_hash = {} 637 | 638 | [ 639 | :logical_resource_id, 640 | :physical_resource_id, 641 | :resource_type, 642 | :resource_status, 643 | :resource_status_reason, 644 | :last_updated_timestamp 645 | ].each do |k| 646 | summary_hash[Kumogata2::Utils.camelize(k)] = summary.send(k) 647 | end 648 | 649 | summary_hash 650 | end 651 | end 652 | 653 | def changes_for(change_set) 654 | change_set.changes.map do |change| 655 | resource_change = change.resource_change 656 | change_hash = {} 657 | 658 | [ 659 | :action, 660 | :logical_resource_id, 661 | :physical_resource_id, 662 | :resource_type, 663 | ].each do |k| 664 | change_hash[Kumogata2::Utils.camelize(k)] = resource_change[k] 665 | end 666 | 667 | change_hash['Details'] = resource_change.details.map do |detail| 668 | { 669 | attribute: detail.target.attribute, 670 | name: detail.target.name, 671 | } 672 | end 673 | 674 | change_hash 675 | end 676 | end 677 | 678 | def output_result(stack_name, outputs, summaries) 679 | puts <<-EOS 680 | 681 | Stack Resource Summaries: 682 | #{JSON.pretty_generate(summaries).colorize_as(:json)} 683 | 684 | Outputs: 685 | #{JSON.pretty_generate(outputs).colorize_as(:json)} 686 | EOS 687 | 688 | if @options.result_log? 689 | logname = get_output_filename(@options.result_log, stack_name) 690 | puts <<-EOS 691 | 692 | (Save to `#{logname}`) 693 | EOS 694 | 695 | open(logname, 'wb') do |f| 696 | f.puts convert_output_value({ 697 | 'StackName' => stack_name, 698 | 'StackResourceSummaries' => summaries, 699 | 'Outputs' => outputs, 700 | }) 701 | end 702 | end 703 | end 704 | 705 | def convert_output_value(value, color = true) 706 | ext = get_output_format 707 | Kumogata2::Plugin.plugin_by_name.each do |type, plugin| 708 | next unless plugin[:ext].include? ext 709 | 710 | plugin_instance = find_or_create_plugin('xxx.' + ext) 711 | case type 712 | when 'json' 713 | return plugin_instance.dump(value, color) 714 | when 'yaml' 715 | return plugin_instance.dump(value, color) 716 | end 717 | end 718 | value 719 | end 720 | 721 | def get_output_filename(value, stack_name = '') 722 | ext = get_output_format 723 | Kumogata2::Plugin.plugin_by_name.each do |type, plugin| 724 | if plugin[:ext].include? ext 725 | plugin_ext = plugin[:ext].first 726 | filename = stack_name.empty? ? File.basename(value, '.yaml') : stack_name 727 | return "#{filename}.#{plugin_ext}" 728 | end 729 | end 730 | value 731 | end 732 | 733 | def post_process(path_or_url, outputs) 734 | plugin = find_or_create_plugin(path_or_url) 735 | 736 | if plugin and plugin.respond_to?(:post) 737 | plugin.post(outputs) 738 | end 739 | end 740 | 741 | def raise_stack_error!(stack, message) 742 | errmsgs = [message] 743 | errmsgs << stack.name 744 | errmsgs << stack.stack_status_reason if stack.stack_status_reason 745 | raise errmsgs.join(': ') 746 | end 747 | 748 | def random_stack_name 749 | stack_name = ['kumogata'] 750 | user_host = Kumogata2::Utils.get_user_host 751 | stack_name << user_host if user_host 752 | stack_name << SecureRandom.uuid 753 | stack_name = stack_name.join('-') 754 | stack_name.gsub(/[^-a-zA-Z0-9]+/, '-').gsub(/-+/, '-') 755 | end 756 | 757 | def normalize_stack_name(stack_name) 758 | if %r|\Astack://| =~ stack_name 759 | stack_name.sub(%r|\Astack://|, '') 760 | else 761 | stack_name 762 | end 763 | end 764 | end 765 | -------------------------------------------------------------------------------- /lib/kumogata2/ext/coderay_ext.rb: -------------------------------------------------------------------------------- 1 | { 2 | constant: "\e[1;34m", 3 | float: "\e[36m", 4 | integer: "\e[36m", 5 | keyword: "\e[1;31m", 6 | 7 | key: { 8 | self: "\e[1;34m", 9 | char: "\e[1;34m", 10 | delimiter: "\e[1;34m", 11 | }, 12 | 13 | string: { 14 | self: "\e[32m", 15 | modifier: "\e[1;32m", 16 | char: "\e[1;32m", 17 | delimiter: "\e[1;32m", 18 | escape: "\e[1;32m", 19 | }, 20 | 21 | error: "\e[0m", 22 | }.each do |key, value| 23 | CodeRay::Encoders::Terminal::TOKEN_COLORS[key] = value 24 | end 25 | -------------------------------------------------------------------------------- /lib/kumogata2/ext/string_ext.rb: -------------------------------------------------------------------------------- 1 | module Kumogata2::Ext 2 | module StringExt 3 | module ClassMethods 4 | def colorize=(value) 5 | @colorize = value 6 | end 7 | 8 | def colorize 9 | @colorize 10 | end 11 | end # ClassMethods 12 | 13 | Term::ANSIColor::Attribute.named_attributes.each do |attribute| 14 | class_eval(<<-EOS, __FILE__, __LINE__ + 1) 15 | def #{attribute.name} 16 | if String.colorize 17 | Term::ANSIColor.send(#{attribute.name.inspect}, self) 18 | else 19 | self 20 | end 21 | end 22 | EOS 23 | end 24 | 25 | def colorize_as(lang) 26 | if String.colorize 27 | CodeRay.scan(self, lang).terminal 28 | else 29 | self 30 | end 31 | end 32 | end # StringExt 33 | end # Kumogata2::Ext 34 | 35 | String.send(:include, Kumogata2::Ext::StringExt) 36 | String.extend(Kumogata2::Ext::StringExt::ClassMethods) 37 | -------------------------------------------------------------------------------- /lib/kumogata2/logger.rb: -------------------------------------------------------------------------------- 1 | class Kumogata2::Logger < ::Logger 2 | include Singleton 3 | 4 | def initialize 5 | super($stdout) 6 | 7 | self.formatter = proc do |severity, datetime, progname, msg| 8 | "#{msg}\n" 9 | end 10 | 11 | self.level = Logger::INFO 12 | end 13 | 14 | def set_debug(value) 15 | self.level = value ? Logger::DEBUG : Logger::INFO 16 | end 17 | 18 | module Helper 19 | def log(level, message, log_options = {}) 20 | globa_options = @options || {} 21 | message = "[#{level.to_s.upcase}] #{message}" unless level == :info 22 | message = message.send(log_options[:color]) if log_options[:color] 23 | logger = globa_options[:logger] || Kumogata2::Logger.instance 24 | logger.send(level, message) 25 | end 26 | module_function :log 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/kumogata2/plugin.rb: -------------------------------------------------------------------------------- 1 | module Kumogata2::Plugin 2 | class << self 3 | def register(name, exts, klass) 4 | name = name.to_s 5 | @plugins ||= Hashie::Mash.new 6 | 7 | if @plugins.has_key?(name) 8 | Kumogata2::Logger::Helper.log(:warn, "Plugin has already been registered: #{name}", color: :yellow) 9 | end 10 | 11 | @plugins[name] = { 12 | name: name, 13 | type: klass, 14 | ext: exts.map(&:to_s), 15 | } 16 | end 17 | 18 | def find_by_ext(ext) 19 | plgn = self.plugins.reverse.find do |i| 20 | i.ext.include?(ext) 21 | end 22 | 23 | plgn ? plgn.type : nil 24 | end 25 | 26 | def plugin_by_name 27 | @plugins 28 | end 29 | 30 | def plugins 31 | @plugins.map {|_, v| v } 32 | end 33 | 34 | def load_plugins 35 | plgns = Gem::Specification.find_all.select {|i| i.name =~ /\Akumogata2-plugin-/ } 36 | 37 | plgns.each do |plgns_spec| 38 | name = plgns_spec.name 39 | path = File.join(name.split('-', 3)) 40 | 41 | begin 42 | require path 43 | rescue LoadError => e 44 | Kumogata2::Logger::Helper.log(:warn, "Cannot load plugin: #{name}: #{e}", color: :yellow) 45 | end 46 | end 47 | end 48 | end # of class methods 49 | end # Kumogata2::Plugin 50 | -------------------------------------------------------------------------------- /lib/kumogata2/plugin/json.rb: -------------------------------------------------------------------------------- 1 | class Kumogata2::Plugin::JSON 2 | Kumogata2::Plugin.register(:json, ['json', 'js'], self) 3 | 4 | def initialize(options) 5 | @options = options 6 | end 7 | 8 | def parse(str) 9 | JSON.parse(str) 10 | end 11 | 12 | def dump(hash, color = true) 13 | if color 14 | JSON.pretty_generate(hash).colorize_as(:json) 15 | else 16 | JSON.pretty_generate(hash) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/kumogata2/plugin/yaml.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | 3 | class Kumogata2::Plugin::YAML 4 | Kumogata2::Plugin.register(:yaml, ['yaml', 'yml', 'template'], self) 5 | 6 | def initialize(options) 7 | @options = options 8 | end 9 | 10 | def parse(str) 11 | YAML.load(str) 12 | end 13 | 14 | def dump(hash, color = true) 15 | Hashie.stringify_keys!(hash) 16 | if color 17 | YAML.dump(hash).colorize_as(:yaml) 18 | else 19 | YAML.dump(hash) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/kumogata2/utils.rb: -------------------------------------------------------------------------------- 1 | class Kumogata2::Utils 2 | class << self 3 | def camelize(str) 4 | str.to_s.split(/[-_]/).map {|i| 5 | i[0, 1].upcase + i[1..-1].downcase 6 | }.join 7 | end 8 | 9 | def filter_backtrace(backtrace) 10 | filter_path = ['(eval)'] 11 | 12 | if defined?(Gem) 13 | filter_path.concat(Gem.path) 14 | filter_path << Gem.bindir 15 | end 16 | 17 | RbConfig::CONFIG.select {|k, v| 18 | k.to_s =~ /libdir/ 19 | }.each {|k, v| filter_path << v } 20 | 21 | filter_path = filter_path.map {|i| /\A#{Regexp.escape(i)}/ } 22 | 23 | backtrace.select do |path| 24 | path = path.split(':', 2).first 25 | not filter_path.any? {|i| i =~ path } 26 | end 27 | end 28 | 29 | def get_user_host 30 | user = `whoami`.strip rescue '' 31 | host = `hostname`.strip rescue '' 32 | user_host = [user, host].select {|i| not i.empty? }.join('-') 33 | user_host.empty? ? nil : user_host 34 | end 35 | 36 | def stringify(obj) 37 | case obj 38 | when Array 39 | obj.map {|v| stringify(v) } 40 | when Hash 41 | hash = {} 42 | 43 | obj.each do |k, v| 44 | hash[stringify(k)] = stringify(v) 45 | end 46 | 47 | hash 48 | else 49 | obj.to_s 50 | end 51 | end 52 | end # of class methods 53 | end 54 | -------------------------------------------------------------------------------- /lib/kumogata2/version.rb: -------------------------------------------------------------------------------- 1 | module Kumogata2 2 | VERSION = '0.1.17' 3 | end 4 | -------------------------------------------------------------------------------- /spec/kumogata2_spec.rb: -------------------------------------------------------------------------------- 1 | describe Kumogata2 do 2 | it do 3 | expect(1).to eq 1 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'kumogata2' 3 | require 'kumogata2/cli/option_parser' 4 | --------------------------------------------------------------------------------