opsup 0.0.1
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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +37 -0
- data/bin/opsup +8 -0
- data/lib/opsup.rb +12 -0
- data/lib/opsup/app.rb +66 -0
- data/lib/opsup/cli.rb +106 -0
- data/lib/opsup/config.rb +41 -0
- data/lib/opsup/error.rb +6 -0
- data/lib/opsup/logger.rb +22 -0
- data/lib/opsup/stack_operator.rb +99 -0
- data/lib/opsup/version.rb +5 -0
- metadata +83 -0
checksums.yaml
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
data/bin/opsup
ADDED
data/lib/opsup.rb
ADDED
@@ -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
|
data/lib/opsup/app.rb
ADDED
@@ -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
|
data/lib/opsup/cli.rb
ADDED
@@ -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
|
data/lib/opsup/config.rb
ADDED
@@ -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
|
data/lib/opsup/error.rb
ADDED
data/lib/opsup/logger.rb
ADDED
@@ -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
|
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: []
|