ecs-solo 0.1.0

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: c078f359d1c062293eca0caeb456d4f1ceb68c632fcc2cda74aab06cf967da3b
4
+ data.tar.gz: 4fc23a43f739db124d9aa470bc19bebe713979894e3eed467c5336e68a0b5d5c
5
+ SHA512:
6
+ metadata.gz: 1d84bb9d91991e3427b7b1d97d798545bbfa30bada05bbd3205fc819c3d948c2188e535c0b89bd10b9a8de89279a1f4dee916e065d43881b401b2bf9f6e834b8
7
+ data.tar.gz: 74bf6a982800c9db88fb0f6225a56569ff11ebd35bced9b0e4a9e187353daf1ef6fbe6fd5fc4dfba6b17a3c19afa23a2638c343102ac1cadb5ac6baa20b54844
@@ -0,0 +1,7 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ This project *tries* to adhere to [Semantic Versioning](http://semver.org/), even before v1.0.
5
+
6
+ ## [0.1.0]
7
+ - Initial release.
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem dependencies in ecs_solo.gemspec
4
+ gemspec
5
+
6
+ gem "codeclimate-test-reporter", group: :test, require: nil
@@ -0,0 +1,89 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ ecs-solo (0.1.0)
5
+ activesupport
6
+ aws-sdk-cloudformation
7
+ aws-sdk-ecs
8
+ memoist
9
+ rainbow
10
+ thor
11
+ zeitwerk
12
+
13
+ GEM
14
+ remote: https://rubygems.org/
15
+ specs:
16
+ activesupport (6.0.2.1)
17
+ concurrent-ruby (~> 1.0, >= 1.0.2)
18
+ i18n (>= 0.7, < 2)
19
+ minitest (~> 5.1)
20
+ tzinfo (~> 1.1)
21
+ zeitwerk (~> 2.2)
22
+ aws-eventstream (1.0.3)
23
+ aws-partitions (1.278.0)
24
+ aws-sdk-cloudformation (1.30.0)
25
+ aws-sdk-core (~> 3, >= 3.71.0)
26
+ aws-sigv4 (~> 1.1)
27
+ aws-sdk-core (3.90.1)
28
+ aws-eventstream (~> 1.0, >= 1.0.2)
29
+ aws-partitions (~> 1, >= 1.239.0)
30
+ aws-sigv4 (~> 1.1)
31
+ jmespath (~> 1.0)
32
+ aws-sdk-ecs (1.57.0)
33
+ aws-sdk-core (~> 3, >= 3.71.0)
34
+ aws-sigv4 (~> 1.1)
35
+ aws-sigv4 (1.1.1)
36
+ aws-eventstream (~> 1.0, >= 1.0.2)
37
+ byebug (11.1.1)
38
+ cli_markdown (0.1.0)
39
+ codeclimate-test-reporter (1.0.9)
40
+ simplecov (<= 0.13)
41
+ concurrent-ruby (1.1.6)
42
+ diff-lcs (1.3)
43
+ docile (1.1.5)
44
+ i18n (1.8.2)
45
+ concurrent-ruby (~> 1.0)
46
+ jmespath (1.4.0)
47
+ json (2.3.0)
48
+ memoist (0.16.2)
49
+ minitest (5.14.0)
50
+ rainbow (3.0.0)
51
+ rake (13.0.1)
52
+ rspec (3.9.0)
53
+ rspec-core (~> 3.9.0)
54
+ rspec-expectations (~> 3.9.0)
55
+ rspec-mocks (~> 3.9.0)
56
+ rspec-core (3.9.1)
57
+ rspec-support (~> 3.9.1)
58
+ rspec-expectations (3.9.0)
59
+ diff-lcs (>= 1.2.0, < 2.0)
60
+ rspec-support (~> 3.9.0)
61
+ rspec-mocks (3.9.1)
62
+ diff-lcs (>= 1.2.0, < 2.0)
63
+ rspec-support (~> 3.9.0)
64
+ rspec-support (3.9.2)
65
+ simplecov (0.13.0)
66
+ docile (~> 1.1.0)
67
+ json (>= 1.8, < 3)
68
+ simplecov-html (~> 0.10.0)
69
+ simplecov-html (0.10.2)
70
+ thor (1.0.1)
71
+ thread_safe (0.3.6)
72
+ tzinfo (1.2.6)
73
+ thread_safe (~> 0.1)
74
+ zeitwerk (2.2.2)
75
+
76
+ PLATFORMS
77
+ ruby
78
+
79
+ DEPENDENCIES
80
+ bundler
81
+ byebug
82
+ cli_markdown
83
+ codeclimate-test-reporter
84
+ ecs-solo!
85
+ rake
86
+ rspec
87
+
88
+ BUNDLED WITH
89
+ 2.1.4
@@ -0,0 +1,19 @@
1
+ guard "bundler", cmd: "bundle" do
2
+ watch("Gemfile")
3
+ watch(/^.+\.gemspec/)
4
+ end
5
+
6
+ guard :rspec, cmd: "bundle exec rspec" do
7
+ require "guard/rspec/dsl"
8
+ dsl = Guard::RSpec::Dsl.new(self)
9
+
10
+ # RSpec files
11
+ rspec = dsl.rspec
12
+ watch(rspec.spec_helper) { rspec.spec_dir }
13
+ watch(rspec.spec_support) { rspec.spec_dir }
14
+ watch(rspec.spec_files)
15
+
16
+ # Ruby files
17
+ ruby = dsl.ruby
18
+ dsl.watch_spec_files_for(ruby.lib_files)
19
+ end
@@ -0,0 +1,42 @@
1
+ # ecs-solo
2
+
3
+ Deploy Docker image from ECS service to the current instance. This is useful if you want to deploy the current running ECS docker image onto an EC2 instance outside of ECS purview. It can also be useful to run the Docker container locally and exam it.
4
+
5
+ ## Usage
6
+
7
+ You may have to login to pull from the Docker regsitry first. Here's an example ECR login command:
8
+
9
+ eval $(aws ecr get-login --no-include-email --region us-west-2)
10
+
11
+ There are 2 forms that the `ecs-solo deploy` understands:
12
+
13
+ ecs-solo deploy STACK_NAME # CloudFormation template AWS::ECS::Service resource
14
+ ecs-solo deploy ECS_SERVICE --cluster development
15
+
16
+ * When passed the CloudFormation stack name, the ECS Cluster and Cluster is automatically infer. The cluster option is not needed and not used at all.
17
+ * When passed the ECS Service, the --cluster optional will probably be needed since the ECS service may not be running in the default development cluster.
18
+
19
+ ## Override command
20
+
21
+ You can override the default command in the Dockerfile with the `-c` option.
22
+
23
+ ecs-solo deploy demo-web-development -c bin/loop.sh
24
+
25
+ ## Example
26
+
27
+ Here's an example with output:
28
+
29
+ $ ecs-solo deploy demo-web-development
30
+ Finding Docker image associated with service
31
+ Found task definition with Docker image
32
+ => docker ps -a -f name=ecs-demo-web-web --format '{{.Names}}' | grep ecs-demo-web-web
33
+ => docker run --name ecs-demo-web-web -d 112233445566.dkr.ecr.us-west-2.amazonaws.com/demo/sinatra:ufo-2020-02-28T20-11-55-f6ef0e0
34
+ d39bc08598bb133c36690b1bfe2c321c1fc219d3e8526174cb9b8e281f161f24
35
+ $ docker ps
36
+ CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
37
+ d39bc08598bb 112233445566.dkr.ecr.us-west-2.amazonaws.com/demo/sinatra:ufo-2020-02-28T20-11-55-f6ef0e0 "bin/web" 3 seconds ago Up 2 seconds 8080/tcp ecs-demo-web-web
38
+ $
39
+
40
+ ## Installation
41
+
42
+ gem install ecs-solo
@@ -0,0 +1,14 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ task default: :spec
5
+
6
+ RSpec::Core::RakeTask.new
7
+
8
+ require_relative "lib/ecs_solo"
9
+ require "cli_markdown"
10
+ desc "Generates cli reference docs as markdown"
11
+ task :docs do
12
+ mkdir_p "docs/_includes"
13
+ CliMarkdown::Creator.create_all(cli_class: EcsSolo::CLI, cli_name: "ecs_solo")
14
+ end
@@ -0,0 +1,45 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "ecs_solo/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "ecs-solo"
8
+ spec.version = EcsSolo::VERSION
9
+ spec.authors = ["Tung Nguyen"]
10
+ spec.email = ["tongueroo@gmail.com"]
11
+ spec.summary = "Deploy Docker image from ECS service to current instance"
12
+ spec.homepage = "https://github.com/tongueroo/ecs-solo"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = Dir.glob("**/*")
16
+ spec.bindir = "exe"
17
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "activesupport"
22
+ spec.add_dependency "aws-sdk-cloudformation"
23
+ spec.add_dependency "aws-sdk-ecs"
24
+ spec.add_dependency "memoist"
25
+ spec.add_dependency "rainbow"
26
+ spec.add_dependency "thor"
27
+ spec.add_dependency "zeitwerk"
28
+
29
+ spec.add_development_dependency "bundler"
30
+ spec.add_development_dependency "byebug"
31
+ spec.add_development_dependency "cli_markdown"
32
+ spec.add_development_dependency "rake"
33
+ spec.add_development_dependency "rspec"
34
+
35
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
36
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
37
+ if spec.respond_to?(:metadata)
38
+ # spec.metadata["allowed_push_host"] = ""
39
+ spec.metadata["homepage_uri"] = spec.homepage
40
+ spec.metadata["source_code_uri"] = "https://github.com/tongueroo/ecs-deploy"
41
+ spec.metadata["changelog_uri"] = "https://github.com/tongueroo/ecs-deploy/blob/master/CHANGELOG.md"
42
+ else
43
+ raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
44
+ end
45
+ end
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Trap ^C
4
+ Signal.trap("INT") {
5
+ puts "\nCtrl-C detected. Exiting..."
6
+ sleep 0.1
7
+ exit
8
+ }
9
+
10
+ $:.unshift(File.expand_path("../../lib", __FILE__))
11
+ require "ecs_solo"
12
+ require "ecs_solo/cli"
13
+
14
+ EcsSolo::CLI.start(ARGV)
@@ -0,0 +1 @@
1
+ require_relative "ecs_solo"
@@ -0,0 +1,11 @@
1
+ $:.unshift(File.expand_path("../", __FILE__))
2
+ require "ecs_solo/version"
3
+ require "memoist"
4
+ require "rainbow/ext/string"
5
+
6
+ require "ecs_solo/autoloader"
7
+ EcsSolo::Autoloader.setup
8
+
9
+ module EcsSolo
10
+ class Error < StandardError; end
11
+ end
@@ -0,0 +1,10 @@
1
+ module EcsSolo
2
+ class AbstractBase
3
+ include AwsServices
4
+ extend Memoist
5
+
6
+ def initialize(options={})
7
+ @options = options
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,22 @@
1
+ require "zeitwerk"
2
+
3
+ module EcsSolo
4
+ class Autoloader
5
+ class Inflector < Zeitwerk::Inflector
6
+ def camelize(basename, _abspath)
7
+ map = { cli: "CLI", version: "VERSION" }
8
+ map[basename.to_sym] || super
9
+ end
10
+ end
11
+
12
+ class << self
13
+ def setup
14
+ loader = Zeitwerk::Loader.new
15
+ loader.inflector = Inflector.new
16
+ loader.push_dir(File.dirname(__dir__)) # lib
17
+ loader.ignore("#{File.dirname(__dir__)}/ecs-solo.rb")
18
+ loader.setup
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,18 @@
1
+ require "aws-sdk-cloudformation"
2
+ require "aws-sdk-ecs"
3
+
4
+ module EcsSolo
5
+ module AwsServices
6
+ extend Memoist
7
+
8
+ def cloudformation
9
+ Aws::CloudFormation::Client.new
10
+ end
11
+ memoize :cloudformation
12
+
13
+ def ecs
14
+ Aws::ECS::Client.new
15
+ end
16
+ memoize :ecs
17
+ end
18
+ end
@@ -0,0 +1,32 @@
1
+ module EcsSolo
2
+ class CLI < Command
3
+ class_option :verbose, type: :boolean
4
+ class_option :noop, type: :boolean
5
+
6
+ desc "deploy IDENTIFIER", "Deploys Docker image associated with ECS service"
7
+ long_desc Help.text(:deploy)
8
+ option :cluster, default: "development", desc: "ECS Cluster"
9
+ option :force_new, type: :boolean, desc: "Force new container if container name is already in use"
10
+ option :command, aliases: :c, desc: "Command to use as part of docker run. Overrides default in the Dockerfile"
11
+ def deploy(identifier)
12
+ Deploy.new(options.merge(identifier: identifier)).run
13
+ end
14
+
15
+ desc "completion *PARAMS", "Prints words for auto-completion."
16
+ long_desc Help.text(:completion)
17
+ def completion(*params)
18
+ Completer.new(CLI, *params).run
19
+ end
20
+
21
+ desc "completion_script", "Generates a script that can be eval to setup auto-completion."
22
+ long_desc Help.text(:completion_script)
23
+ def completion_script
24
+ Completer::Script.generate
25
+ end
26
+
27
+ desc "version", "prints version"
28
+ def version
29
+ puts VERSION
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,89 @@
1
+ require "thor"
2
+
3
+ # Override thor's long_desc identation behavior
4
+ # https://github.com/erikhuda/thor/issues/398
5
+ class Thor
6
+ module Shell
7
+ class Basic
8
+ def print_wrapped(message, options = {})
9
+ message = "\n#{message}" unless message[0] == "\n"
10
+ stdout.puts message
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ module EcsSolo
17
+ class Command < Thor
18
+ class << self
19
+ def dispatch(m, args, options, config)
20
+ # Allow calling for help via:
21
+ # ecs_solo command help
22
+ # ecs_solo command -h
23
+ # ecs_solo command --help
24
+ # ecs_solo command -D
25
+ #
26
+ # as well thor's normal way:
27
+ #
28
+ # ecs_solo help command
29
+ help_flags = Thor::HELP_MAPPINGS + ["help"]
30
+ if args.length > 1 && !(args & help_flags).empty?
31
+ args -= help_flags
32
+ args.insert(-2, "help")
33
+ end
34
+
35
+ # ecs_solo version
36
+ # ecs_solo --version
37
+ # ecs_solo -v
38
+ version_flags = ["--version", "-v"]
39
+ if args.length == 1 && !(args & version_flags).empty?
40
+ args = ["version"]
41
+ end
42
+
43
+ super
44
+ end
45
+
46
+ # Override command_help to include the description at the top of the
47
+ # long_description.
48
+ def command_help(shell, command_name)
49
+ meth = normalize_command_name(command_name)
50
+ command = all_commands[meth]
51
+ alter_command_description(command)
52
+ super
53
+ end
54
+
55
+ def alter_command_description(command)
56
+ return unless command
57
+
58
+ # Add description to beginning of long_description
59
+ long_desc = if command.long_description
60
+ "#{command.description}\n\n#{command.long_description}"
61
+ else
62
+ command.description
63
+ end
64
+
65
+ # add reference url to end of the long_description
66
+ unless website.empty?
67
+ full_command = [command.ancestor_name, command.name].compact.join('-')
68
+ url = "#{website}/reference/ecs_solo-#{full_command}"
69
+ long_desc += "\n\nHelp also available at: #{url}"
70
+ end
71
+
72
+ command.long_description = long_desc
73
+ end
74
+ private :alter_command_description
75
+
76
+ # meant to be overriden
77
+ def website
78
+ ""
79
+ end
80
+
81
+ # https://github.com/erikhuda/thor/issues/244
82
+ # Deprecation warning: Thor exit with status 0 on errors. To keep this behavior, you must define `exit_on_failure?` in `Lono::CLI`
83
+ # You can silence deprecations warning by setting the environment variable THOR_SILENCE_DEPRECATION.
84
+ def exit_on_failure?
85
+ true
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,159 @@
1
+ =begin
2
+ Code Explanation:
3
+
4
+ There are 3 types of things to auto-complete:
5
+
6
+ 1. command: the command itself
7
+ 2. parameters: command parameters.
8
+ 3. options: command options
9
+
10
+ Here's an example:
11
+
12
+ mycli hello name --from me
13
+
14
+ * command: hello
15
+ * parameters: name
16
+ * option: --from
17
+
18
+ When command parameters are done processing, the remaining completion words will be options. We can tell that the command params are completed based on the method arity.
19
+
20
+ ## Arity
21
+
22
+ For example, say you had a method for a CLI command with the following form:
23
+
24
+ ufo scale service count --cluster development
25
+
26
+ It's equivalent ruby method:
27
+
28
+ scale(service, count) = has an arity of 2
29
+
30
+ So typing:
31
+
32
+ ufo scale service count [TAB] # there are 3 parameters including the "scale" command according to Thor's CLI processing.
33
+
34
+ So the completion should only show options, something like this:
35
+
36
+ --noop --verbose --cluster
37
+
38
+ ## Splat Arguments
39
+
40
+ When the ruby method has a splat argument, it's arity is negative. Here are some example methods and their arities.
41
+
42
+ ship(service) = 1
43
+ scale(service, count) = 2
44
+ ships(*services) = -1
45
+ foo(example, *rest) = -2
46
+
47
+ Fortunately, negative and positive arity values are processed the same way. So we take simply take the absolute value of the arity and process it the same.
48
+
49
+ Here are some test cases, hit TAB after typing the command:
50
+
51
+ ecs_solo completion
52
+ ecs_solo completion hello
53
+ ecs_solo completion hello name
54
+ ecs_solo completion hello name --
55
+ ecs_solo completion hello name --noop
56
+
57
+ ecs_solo completion
58
+ ecs_solo completion sub:goodbye
59
+ ecs_solo completion sub:goodbye name
60
+
61
+ ## Subcommands and Thor::Group Registered Commands
62
+
63
+ Sometimes the commands are not simple thor commands but are subcommands or Thor::Group commands. A good specific example is the ufo tool.
64
+
65
+ * regular command: ufo ship
66
+ * subcommand: ufo docker
67
+ * Thor::Group command: ufo init
68
+
69
+ Auto-completion accounts for each of these type of commands.
70
+ =end
71
+ module EcsSolo
72
+ class Completer
73
+ def initialize(command_class, *params)
74
+ @params = params
75
+ @current_command = @params[0]
76
+ @command_class = command_class # CLI initiall
77
+ end
78
+
79
+ def run
80
+ if subcommand?(@current_command)
81
+ subcommand_class = @command_class.subcommand_classes[@current_command]
82
+ @params.shift # destructive
83
+ Completer.new(subcommand_class, *@params).run # recursively use subcommand
84
+ return
85
+ end
86
+
87
+ # full command has been found!
88
+ unless found?(@current_command)
89
+ puts all_commands
90
+ return
91
+ end
92
+
93
+ # will only get to here if command aws found (above)
94
+ arity = @command_class.instance_method(@current_command).arity.abs
95
+ if @params.size > arity or thor_group_command?
96
+ puts options_completion
97
+ else
98
+ puts params_completion
99
+ end
100
+ end
101
+
102
+ def subcommand?(command)
103
+ @command_class.subcommands.include?(command)
104
+ end
105
+
106
+ # hacky way to detect that command is a registered Thor::Group command
107
+ def thor_group_command?
108
+ command_params(raw=true) == [[:rest, :args]]
109
+ end
110
+
111
+ def found?(command)
112
+ public_methods = @command_class.public_instance_methods(false)
113
+ command && public_methods.include?(command.to_sym)
114
+ end
115
+
116
+ # all top-level commands
117
+ def all_commands
118
+ commands = @command_class.all_commands.reject do |k,v|
119
+ v.is_a?(Thor::HiddenCommand)
120
+ end
121
+ commands.keys
122
+ end
123
+
124
+ def command_params(raw=false)
125
+ params = @command_class.instance_method(@current_command).parameters
126
+ # Example:
127
+ # >> Sub.instance_method(:goodbye).parameters
128
+ # => [[:req, :name]]
129
+ # >>
130
+ raw ? params : params.map!(&:last)
131
+ end
132
+
133
+ def params_completion
134
+ offset = @params.size - 1
135
+ offset_params = command_params[offset..-1]
136
+ command_params[offset..-1].first
137
+ end
138
+
139
+ def options_completion
140
+ used = ARGV.select { |a| a.include?('--') } # so we can remove used options
141
+
142
+ method_options = @command_class.all_commands[@current_command].options.keys
143
+ class_options = @command_class.class_options.keys
144
+
145
+ all_options = method_options + class_options + ['help']
146
+
147
+ all_options.map! { |o| "--#{o.to_s.gsub('_','-')}" }
148
+ filtered_options = all_options - used
149
+ filtered_options.uniq
150
+ end
151
+
152
+ # Useful for debugging. Using puts messes up completion.
153
+ def log(msg)
154
+ File.open("/tmp/complete.log", "a") do |file|
155
+ file.puts(msg)
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,6 @@
1
+ class EcsSolo::Completer::Script
2
+ def self.generate
3
+ bash_script = File.expand_path("script.sh", File.dirname(__FILE__))
4
+ puts "source #{bash_script}"
5
+ end
6
+ end
@@ -0,0 +1,10 @@
1
+ _ecs_solo() {
2
+ COMPREPLY=()
3
+ local word="${COMP_WORDS[COMP_CWORD]}"
4
+ local words=("${COMP_WORDS[@]}")
5
+ unset words[0]
6
+ local completion=$(ecs_solo completion ${words[@]})
7
+ COMPREPLY=( $(compgen -W "$completion" -- "$word") )
8
+ }
9
+
10
+ complete -F _ecs_solo ecs_solo
@@ -0,0 +1,73 @@
1
+ module EcsSolo
2
+ class Deploy < AbstractBase
3
+ def initialize(options={})
4
+ super
5
+ @identifier = options[:identifier] # ECS Service or CloudFormation Stack
6
+ @cluster = options[:cluster]
7
+ @command = options[:command]
8
+ end
9
+
10
+ def run
11
+ task_definition = find_task_definition
12
+ unless task_definition
13
+ puts "Unable to task definition associated with service #{@service.color(:green)} in the cluster #{@cluster.color(:green)}"
14
+ exit 1
15
+ end
16
+
17
+ @docker = Docker.new(@options.merge(task_definition: task_definition))
18
+ @docker.execute
19
+ end
20
+
21
+ def find_task_definition
22
+ puts "Finding Docker image associated with service #{@service}"
23
+ service = find_service
24
+ return unless service
25
+
26
+ task_definition = service.task_definition
27
+ resp = ecs.describe_task_definition(task_definition: task_definition)
28
+ puts "Found task definition with Docker image"
29
+ resp.task_definition
30
+ end
31
+
32
+ def find_service
33
+ ecs_service_arn = cloudformation_ecs_service_arn(@identifier)
34
+ if ecs_service_arn
35
+ # IE: arn:aws:ecs:us-west-2:112233445566:service/development/demo-web-development-Ecs-179L598PRC44
36
+ @cluster = ecs_service_arn.split('/')[1] # override @cluster
37
+ ecs_service(ecs_service_arn)
38
+ else
39
+ ecs_service(@identifier)
40
+ end
41
+ end
42
+
43
+ def cloudformation_ecs_service_arn(stack_name)
44
+ resp = cloudformation.describe_stack_resources(stack_name: stack_name)
45
+ resource = resp.stack_resources.find { |r| r.resource_type == "AWS::ECS::Service" }
46
+ resource.physical_resource_id # IE: arn:aws:ecs:us-west-2:112233445566:service/development/demo-web-development-Ecs-179L598PRC44
47
+ rescue Aws::CloudFormation::Errors::ValidationError => e
48
+ if e.message.include?("does not exist")
49
+ return
50
+ else
51
+ raise(e)
52
+ end
53
+ end
54
+
55
+ def ecs_service(service)
56
+ begin
57
+ resp = ecs.describe_services(services: [service], cluster: @cluster)
58
+ rescue Aws::ECS::Errors::ClusterNotFoundException => e
59
+ puts "#{e.class}: #{e.message}"
60
+ puts "WARN: #{@cluster.color(:green)} not found."
61
+ return
62
+ end
63
+
64
+ resp.services.first
65
+ end
66
+
67
+ private
68
+ def docker
69
+ Docker.new
70
+ end
71
+ memoize :docker
72
+ end
73
+ end
@@ -0,0 +1,74 @@
1
+ module EcsSolo
2
+ class Docker
3
+ def initialize(options={})
4
+ @options = options
5
+ @task_definition = options[:task_definition] # describe_task_definition resp.task_definition
6
+ @command = options[:command] # will be array
7
+ end
8
+
9
+ def execute
10
+ unless in_use?
11
+ run
12
+ return
13
+ end
14
+
15
+ if @options[:force_new]
16
+ puts "INFO: Forcing new container"
17
+ stop
18
+ rm
19
+ run
20
+ else
21
+ puts "WARN: container name is already in use".color(:yellow)
22
+ puts "If you want to force a new container, use the --force-new option."
23
+ end
24
+ end
25
+
26
+ def run
27
+ sh "docker run --name #{name} -d #{image} #{@command}"
28
+ end
29
+
30
+ def in_use?
31
+ sh "docker ps -a -f name=#{name} --format '{{.Names}}' | grep #{name}"
32
+ end
33
+
34
+ def stop
35
+ sh "docker stop #{name}"
36
+ end
37
+
38
+ def rm
39
+ sh "docker rm #{name}"
40
+ end
41
+
42
+ def pull
43
+ sh "docker pull #{image}"
44
+ end
45
+
46
+ # Almost resembles the name ecs-agent generates.
47
+ # Unsure how about the random looking id at the end though. Example:
48
+ #
49
+ # ecs-demo-web-217-web-86a0bcbac2b7d1b92d00
50
+ #
51
+ # So not including that.
52
+ #
53
+ # Also not including revision to make this script simpler. So final result:
54
+ #
55
+ # ecs-demo-web-web
56
+ #
57
+ def name
58
+ "ecs-#{@task_definition.family}-#{container_definition.name}"
59
+ end
60
+
61
+ def image
62
+ container_definition.image
63
+ end
64
+
65
+ def container_definition
66
+ @task_definition.container_definitions.first
67
+ end
68
+
69
+ def sh(command)
70
+ puts "=> #{command}"
71
+ system(command)
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,9 @@
1
+ module EcsSolo::Help
2
+ class << self
3
+ def text(namespaced_command)
4
+ path = namespaced_command.to_s.gsub(':','/')
5
+ path = File.expand_path("../help/#{path}.md", __FILE__)
6
+ IO.read(path) if File.exist?(path)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,20 @@
1
+ ## Examples
2
+
3
+ ecs_solo completion
4
+
5
+ Prints words for TAB auto-completion.
6
+
7
+ ecs_solo completion
8
+ ecs_solo completion hello
9
+ ecs_solo completion hello name
10
+
11
+ To enable, TAB auto-completion add the following to your profile:
12
+
13
+ eval $(ecs_solo completion_script)
14
+
15
+ Auto-completion example usage:
16
+
17
+ ecs_solo [TAB]
18
+ ecs_solo hello [TAB]
19
+ ecs_solo hello name [TAB]
20
+ ecs_solo hello name --[TAB]
@@ -0,0 +1,3 @@
1
+ To use, add the following to your `~/.bashrc` or `~/.profile`
2
+
3
+ eval $(ecs_solo completion_script)
@@ -0,0 +1,12 @@
1
+ ## Examples
2
+
3
+ ecs-solo deploy IDENTIFIER
4
+ ecs-solo deploy IDENTIFIER --cluster development
5
+
6
+ The `IDENTIFIER` can be either a:
7
+
8
+ 1. CloudFormation stack name that has an `AWS::ECS::Service` resource. The ECS Service and its Cluster is inferred from the stack.
9
+ 2. ECS Service, the `--cluster` optional will probably be needed since the ECS service may not be running in the default development cluster.
10
+
11
+ ecs-solo deploy CFN_STACK
12
+ ecs-solo deploy ECS_SERVICE --cluster development
@@ -0,0 +1,3 @@
1
+ module EcsSolo
2
+ VERSION = "0.1.0"
3
+ end
Binary file
Binary file
@@ -0,0 +1,26 @@
1
+ describe EcsSolo::CLI do
2
+ before(:all) do
3
+ @args = "--from Tung"
4
+ end
5
+
6
+ describe "ecs_solo" do
7
+ it "hello" do
8
+ out = execute("exe/ecs_solo hello world #{@args}")
9
+ expect(out).to include("from: Tung\nHello world")
10
+ end
11
+
12
+ commands = {
13
+ "hell" => "hello",
14
+ "hello" => "name",
15
+ "hello -" => "--from",
16
+ "hello name" => "--from",
17
+ "hello name --" => "--from",
18
+ }
19
+ commands.each do |command, expected_word|
20
+ it "completion #{command}" do
21
+ out = execute("exe/ecs_solo completion #{command}")
22
+ expect(out).to include(expected_word) # only checking for one word for simplicity
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,29 @@
1
+ ENV["BACKEND_ADMIN_TEST"] = "1"
2
+
3
+ # CodeClimate test coverage: https://docs.codeclimate.com/docs/configuring-test-coverage
4
+ # require 'simplecov'
5
+ # SimpleCov.start
6
+
7
+ require "pp"
8
+ require "byebug"
9
+ root = File.expand_path("../", File.dirname(__FILE__))
10
+ require "#{root}/lib/ecs_solo"
11
+
12
+ module Helper
13
+ def execute(cmd)
14
+ puts "Running: #{cmd}" if show_command?
15
+ out = `#{cmd}`
16
+ puts out if show_command?
17
+ out
18
+ end
19
+
20
+ # Added SHOW_COMMAND because DEBUG is also used by other libraries like
21
+ # bundler and it shows its internal debugging logging also.
22
+ def show_command?
23
+ ENV['DEBUG'] || ENV['SHOW_COMMAND']
24
+ end
25
+ end
26
+
27
+ RSpec.configure do |c|
28
+ c.include Helper
29
+ end
metadata ADDED
@@ -0,0 +1,246 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ecs-solo
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tung Nguyen
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2020-02-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: aws-sdk-cloudformation
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: aws-sdk-ecs
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: memoist
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rainbow
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: thor
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: zeitwerk
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: bundler
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: byebug
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: cli_markdown
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: rake
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: rspec
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ description:
182
+ email:
183
+ - tongueroo@gmail.com
184
+ executables:
185
+ - ecs-solo
186
+ extensions: []
187
+ extra_rdoc_files: []
188
+ files:
189
+ - CHANGELOG.md
190
+ - Gemfile
191
+ - Gemfile.lock
192
+ - Guardfile
193
+ - README.md
194
+ - Rakefile
195
+ - ecs-solo.gemspec
196
+ - exe/ecs-solo
197
+ - lib/ecs-solo.rb
198
+ - lib/ecs_solo.rb
199
+ - lib/ecs_solo/abstract_base.rb
200
+ - lib/ecs_solo/autoloader.rb
201
+ - lib/ecs_solo/aws_services.rb
202
+ - lib/ecs_solo/cli.rb
203
+ - lib/ecs_solo/command.rb
204
+ - lib/ecs_solo/completer.rb
205
+ - lib/ecs_solo/completer/script.rb
206
+ - lib/ecs_solo/completer/script.sh
207
+ - lib/ecs_solo/deploy.rb
208
+ - lib/ecs_solo/docker.rb
209
+ - lib/ecs_solo/help.rb
210
+ - lib/ecs_solo/help/completion.md
211
+ - lib/ecs_solo/help/completion_script.md
212
+ - lib/ecs_solo/help/deploy.md
213
+ - lib/ecs_solo/version.rb
214
+ - pkg/ecs-solo-0.1.0.gem
215
+ - pkg/solo-0.1.0.gem
216
+ - spec/cli_spec.rb
217
+ - spec/spec_helper.rb
218
+ homepage: https://github.com/tongueroo/ecs-solo
219
+ licenses:
220
+ - MIT
221
+ metadata:
222
+ homepage_uri: https://github.com/tongueroo/ecs-solo
223
+ source_code_uri: https://github.com/tongueroo/ecs-deploy
224
+ changelog_uri: https://github.com/tongueroo/ecs-deploy/blob/master/CHANGELOG.md
225
+ post_install_message:
226
+ rdoc_options: []
227
+ require_paths:
228
+ - lib
229
+ required_ruby_version: !ruby/object:Gem::Requirement
230
+ requirements:
231
+ - - ">="
232
+ - !ruby/object:Gem::Version
233
+ version: '0'
234
+ required_rubygems_version: !ruby/object:Gem::Requirement
235
+ requirements:
236
+ - - ">="
237
+ - !ruby/object:Gem::Version
238
+ version: '0'
239
+ requirements: []
240
+ rubygems_version: 3.1.2
241
+ signing_key:
242
+ specification_version: 4
243
+ summary: Deploy Docker image from ECS service to current instance
244
+ test_files:
245
+ - spec/cli_spec.rb
246
+ - spec/spec_helper.rb