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.
@@ -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: []