opsup 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6cc178acdd70bf25302010af889f45a52d95d2f1f1fa841ab7f6534cb98d024a
4
+ data.tar.gz: 3a063657be7b128f9b4dc47e0c91d4f42a0e0dce978e71d4f89fee9c94f22ca3
5
+ SHA512:
6
+ metadata.gz: b29e1e1e7842bda942d3ff0c0921b09f47a69aab108f34481e73e7bf520d87820a36d02bc9c6c49e793bb0e16d4fab7688f1281e5e15b39a7b79c627abd6ed63
7
+ data.tar.gz: 70231d0c02c3e1fd9c135bceae0e41e39cc601bc03e8875e6c50f6c3be88c6c572fbcb37714a05520f75d6269270a8e1cb3b5b3e7be4e73b2a7ef5b4a02b6ab0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 ryym
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 all
13
+ 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 THE
21
+ SOFTWARE.
@@ -0,0 +1,37 @@
1
+ # Opsup
2
+
3
+ Opsup is a small command line tool to run commands for [AWS OpsWorks][aws-opsworks].
4
+
5
+ I created this as an internal tool for my work.
6
+
7
+ [aws-opsworks]: https://aws.amazon.com/jp/opsworks/
8
+
9
+ ## Installation
10
+
11
+ ```ruby
12
+ gem install opsup
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ Currently Opsup can run these commands:
18
+
19
+ - `update_cookbooks`
20
+ - `setup`
21
+ - `configure`
22
+ - `deploy`
23
+
24
+ Example:
25
+
26
+ ```bash
27
+ $ opsup --stack $YOUR_STACK_NAME --aws-cred $AWS_KEY,$AWS_SECRET deploy
28
+ ```
29
+
30
+ Opsup waits until the command completes.
31
+
32
+ ### TODO
33
+
34
+ - Add a command to build cookbooks and upload them to S3
35
+ - Write tests
36
+ - (maybe) Load options from environment varibles or a configuration file
37
+ - (maybe) Add commands to create, start, stop, and delete instances
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/opsup'
5
+
6
+ cli = Opsup::CLI.create
7
+ ok = cli.run(ARGV)
8
+ exit(ok ? 0 : 1)
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'opsup/version'
4
+ require_relative 'opsup/error'
5
+ require_relative 'opsup/config'
6
+ require_relative 'opsup/logger'
7
+ require_relative 'opsup/stack_operator'
8
+ require_relative 'opsup/app'
9
+ require_relative 'opsup/cli'
10
+
11
+ module Opsup
12
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aws-sdk-opsworks'
4
+
5
+ module Opsup
6
+ class App
7
+ private_class_method :new
8
+
9
+ def self.create
10
+ new(
11
+ logger: Opsup::Logger.instance,
12
+ )
13
+ end
14
+
15
+ def initialize(logger:)
16
+ @logger = logger
17
+ end
18
+
19
+ AVAILABLE_COMMANDS = %w[
20
+ update_cookbooks
21
+ setup
22
+ configure
23
+ deploy
24
+ ].freeze
25
+
26
+ def available_commands
27
+ AVAILABLE_COMMANDS
28
+ end
29
+
30
+ def run(commands, config)
31
+ validate_commands(commands)
32
+ @logger.warn('Started in DRYRUN MODE') if config.dryrun
33
+ @logger.debug("Running #{commands} with #{config.to_h}")
34
+
35
+ opsworks = new_opsworks_client(config)
36
+ opsworks_commands = commands.map { |c| command_to_opsworks_command(c) }
37
+
38
+ stack_operator = Opsup::StackOperator.create(opsworks: opsworks)
39
+ stack_operator.run_commands(
40
+ opsworks_commands,
41
+ stack_name: config.stack_name,
42
+ mode: config.running_mode,
43
+ dryrun: config.dryrun,
44
+ )
45
+ ensure
46
+ @logger.warn('Finished in DRYRUN MODE') if config.dryrun
47
+ end
48
+
49
+ private def validate_commands(commands)
50
+ raise Opsup::Error, 'No commands specified' if commands.empty?
51
+
52
+ unknown_cmds = commands - AVAILABLE_COMMANDS
53
+ raise Opsup::Error, "Unknown commands: #{unknown_cmds.join(' ')}" unless unknown_cmds.empty?
54
+ end
55
+
56
+ private def new_opsworks_client(config)
57
+ creds = Aws::Credentials.new(config.aws_access_key_id, config.aws_secret_access_key)
58
+ Aws::OpsWorks::Client.new(region: config.opsworks_region, credentials: creds)
59
+ end
60
+
61
+ # Assumes the command is a valid value.
62
+ private def command_to_opsworks_command(command)
63
+ command == 'update_cookbooks' ? 'update_custom_cookbooks' : command
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+
5
+ module Opsup
6
+ class CLI
7
+ private_class_method :new
8
+
9
+ def self.create
10
+ new(
11
+ app: Opsup::App.create,
12
+ option_builder: Opsup::CLI::OptionBuilder.create,
13
+ )
14
+ end
15
+
16
+ def initialize(app:, option_builder:)
17
+ @app = app
18
+ @option_builder = option_builder
19
+ end
20
+
21
+ def run(argv)
22
+ parser = create_parser
23
+ @option_builder.define_options(parser)
24
+
25
+ options = {}
26
+ begin
27
+ # It automatically exits with a help message if necessary.
28
+ commands = parser.parse(argv, into: options)
29
+ rescue OptionParser::MissingArgument => e
30
+ puts e.message
31
+ return false
32
+ end
33
+
34
+ begin
35
+ config = @option_builder.generate_config(options)
36
+ @app.run(commands, config)
37
+ rescue Opsup::Error => e
38
+ puts "Error: #{e.message}"
39
+ return false
40
+ end
41
+
42
+ true
43
+ end
44
+
45
+ private def create_parser
46
+ # ref: https://docs.ruby-lang.org/en/2.1.0/OptionParser.html
47
+ OptionParser.new do |p|
48
+ p.version = Opsup::VERSION
49
+ p.banner = <<~BANNER
50
+ CLI to run Chef commands easily for your OpsWorks stacks.
51
+ Usage:
52
+ opsup [options] [commands...]
53
+ Commands:
54
+ #{@app.available_commands.join(', ')}
55
+ Example:
56
+ opsup -s stack-name deploy
57
+
58
+ Options:
59
+ BANNER
60
+ end
61
+ end
62
+
63
+ class OptionBuilder
64
+ private_class_method :new
65
+
66
+ def self.create
67
+ new
68
+ end
69
+
70
+ DEFAULT_OPSWORKS_REGION = 'ap-northeast-1'
71
+
72
+ def define_options(parser)
73
+ parser.tap do |p|
74
+ p.on('-s', '--stack STACK_NAME', 'target stack name')
75
+ p.on('-m', '--mode MODE', Opsup::Config::MODES.join(' | ').to_s)
76
+ p.on('--aws-cred KEY_ID,SECRET_KEY', 'AWS credentials')
77
+ p.on('--opsworks-region REGION', "default: #{DEFAULT_OPSWORKS_REGION}")
78
+ p.on('-d', '--dryrun')
79
+ end
80
+ end
81
+
82
+ def generate_config(options)
83
+ %w[stack aws-cred].each do |key|
84
+ raise Opsup::Error, "missing required option: --#{key}" unless options[key.to_sym]
85
+ end
86
+
87
+ aws_key_id, aws_secret = options[:"aws-cred"].split(',')
88
+ if aws_key_id.nil? || aws_secret.nil?
89
+ raise Opsup::Error, "aws-cred must be 'key_id,secret_key' format"
90
+ end
91
+
92
+ mode = options[:mode]&.to_sym
93
+ raise Opsup::Error, "invalid mode: #{mode}" if mode && !Opsup::Config::MODES.include?(mode)
94
+
95
+ Opsup::Config.new(
96
+ stack_name: options[:stack],
97
+ aws_access_key_id: aws_key_id,
98
+ aws_secret_access_key: aws_secret,
99
+ opsworks_region: options[:"opsworks-region"] || DEFAULT_OPSWORKS_REGION,
100
+ running_mode: mode,
101
+ dryrun: options[:dryrun] || false,
102
+ )
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opsup
4
+ class Config
5
+ attr_reader :stack_name
6
+ attr_reader :aws_access_key_id
7
+ attr_reader :aws_secret_access_key
8
+ attr_reader :opsworks_region
9
+ attr_reader :running_mode
10
+ attr_reader :dryrun
11
+
12
+ MODES = %i[parallel serial one_then_all].freeze
13
+
14
+ def initialize(
15
+ stack_name:,
16
+ aws_access_key_id:,
17
+ aws_secret_access_key:,
18
+ opsworks_region:,
19
+ running_mode: nil,
20
+ dryrun: false
21
+ )
22
+ @stack_name = stack_name
23
+ @aws_access_key_id = aws_access_key_id
24
+ @aws_secret_access_key = aws_secret_access_key
25
+ @opsworks_region = opsworks_region
26
+ @running_mode = running_mode || MODES[0]
27
+ @dryrun = dryrun
28
+ end
29
+
30
+ def to_h
31
+ {
32
+ stack_name: stack_name,
33
+ aws_access_key_id: aws_access_key_id,
34
+ aws_secret_access_key: aws_secret_access_key,
35
+ opsworks_region: opsworks_region,
36
+ running_mode: running_mode,
37
+ dryrun: dryrun,
38
+ }
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opsup
4
+ class Error < StandardError
5
+ end
6
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module Opsup
6
+ class Logger
7
+ def self.instance
8
+ env_log_level = ENV['OPSUP_LOG_LEVEL']
9
+ log_level =
10
+ if env_log_level && ::Logger.const_defined?(env_log_level)
11
+ ::Logger.const_get(env_log_level)
12
+ else
13
+ ::Logger::INFO
14
+ end
15
+
16
+ # Should be able to change the output device.
17
+ @instance ||= ::Logger.new(STDOUT).tap do |logger|
18
+ logger.level = log_level
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opsup
4
+ class StackOperator
5
+ private_class_method :new
6
+
7
+ def self.create(opsworks:)
8
+ new(
9
+ opsworks: opsworks,
10
+ logger: Opsup::Logger.instance,
11
+ )
12
+ end
13
+
14
+ def initialize(opsworks:, logger:)
15
+ @opsworks = opsworks
16
+ @logger = logger
17
+ end
18
+
19
+ def run_commands(commands, stack_name:, mode:, dryrun: false)
20
+ # Find the target stack.
21
+ @logger.debug('Verifying the specified stack exists...')
22
+ stacks = @opsworks.describe_stacks.stacks
23
+ stack = stacks.find { |s| s.name == stack_name }
24
+ raise Opsup::Error, "Stack #{stack_name} does not exist" if stack.nil?
25
+
26
+ # Find the stack's apps.
27
+ @logger.debug('Verifying the stack has at least one app...')
28
+ apps = @opsworks.describe_apps(stack_id: stack.stack_id).apps
29
+ raise Opsup::Error, "#{stack_name} has no apps" if apps.empty?
30
+
31
+ # Find the instances to be updated.
32
+ @logger.debug('Finding all working instances in the stack...')
33
+ instances = @opsworks.describe_instances(stack_id: stack.stack_id).instances
34
+ instances = instances.reject { |inst| inst.status == 'stopped' }
35
+ @logger.debug(
36
+ "#{instances.size} #{instances.size == 1 ? 'instance is' : 'instances are'} found",
37
+ )
38
+
39
+ # Currently Opsup deploys only the first app by default.
40
+ app = apps.first
41
+ instance_ids = instances.map(&:instance_id)
42
+
43
+ # Run the commands sequentially.
44
+ commands.each do |command|
45
+ @logger.info("Running #{command} command in #{mode} mode...")
46
+ run_command(
47
+ command,
48
+ dryrun: dryrun,
49
+ mode: mode,
50
+ stack: stack,
51
+ app: app,
52
+ instance_ids: instance_ids,
53
+ )
54
+ end
55
+ end
56
+
57
+ private def run_command(command, dryrun:, mode:, stack:, app:, instance_ids:)
58
+ case mode
59
+ when :parallel
60
+ @logger.info("Creating single deployment for the #{instance_ids.size} instances...")
61
+ create_deployment(command, stack, app, instance_ids) unless dryrun
62
+ when :serial
63
+ instance_ids.each.with_index do |id, i|
64
+ @logger.info("Creating deployment for instances[#{i}] (#{id})...")
65
+ create_deployment(command, stack, app, [id]) unless dryrun
66
+ end
67
+ when :one_then_all
68
+ @logger.info("Creating deployment for the first instance (#{instance_ids[0]})...")
69
+ create_deployment(command, stack, app, [instance_ids[0]]) unless dryrun
70
+
71
+ rest = instance_ids[1..-1]
72
+ if !rest.empty?
73
+ @logger.info("Creating deployment for the other #{rest.size} instances...")
74
+ create_deployment(command, stack, app, rest) unless dryrun
75
+ else
76
+ @logger.info('No other instances exist.')
77
+ end
78
+ else
79
+ raise "Unknown running mode: #{mode}"
80
+ end
81
+ end
82
+
83
+ private def create_deployment(command, stack, app, instance_ids)
84
+ res = @opsworks.create_deployment(
85
+ stack_id: stack.stack_id,
86
+ app_id: app.app_id,
87
+ instance_ids: instance_ids,
88
+ command: { name: command, args: {} },
89
+ )
90
+
91
+ @logger.info("Waiting deployment #{res.deployment_id}...")
92
+ @opsworks.wait_until(:deployment_successful, {
93
+ deployment_ids: [res.deployment_id],
94
+ })
95
+
96
+ nil
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opsup
4
+ VERSION = '0.0.1'
5
+ end
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: opsup
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - ryym
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-07-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: aws-sdk-opsworks
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rubocop
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.71'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.71'
41
+ description:
42
+ email:
43
+ - ryym.64@gmail.com
44
+ executables:
45
+ - opsup
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - LICENSE
50
+ - README.md
51
+ - bin/opsup
52
+ - lib/opsup.rb
53
+ - lib/opsup/app.rb
54
+ - lib/opsup/cli.rb
55
+ - lib/opsup/config.rb
56
+ - lib/opsup/error.rb
57
+ - lib/opsup/logger.rb
58
+ - lib/opsup/stack_operator.rb
59
+ - lib/opsup/version.rb
60
+ homepage: https://github.com/ryym/opsup
61
+ licenses:
62
+ - MIT
63
+ metadata: {}
64
+ post_install_message:
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '2.6'
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ requirements: []
79
+ rubygems_version: 3.0.3
80
+ signing_key:
81
+ specification_version: 4
82
+ summary: CLI to run commands for AWS OpsWorks
83
+ test_files: []