ops_team 1.0.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0e74fe375694383d5cf99385e04c314e804a2ab1aae6b03558b811e78874c65e
4
- data.tar.gz: 4c9f31d818c8bc456c4c9c37d40d42491dde28ef6e038eee77ff0e7c42e7a6da
3
+ metadata.gz: 284cb7b523fa92a233a477d63adf73ca4257a460968fa135e722af70d7782e33
4
+ data.tar.gz: e9ec66dfe05a1411b5a3180cbffe5545ec23dfc193d06150c80a2cb96147bcd9
5
5
  SHA512:
6
- metadata.gz: 87320ca1362f2691ad4808bca8b7733b2d1be4bc70b4ca06b650267bf5c50c7d69e64a554fc91d246c821e17454794ae0aadb9cca27ea747f7092f5f636a6492
7
- data.tar.gz: 6b6bee1687f1858a1097931e336ddcf28fe4f50a9b8802cded747a034f4fefddbcab0fd4331bd961a269811384eb574bfca4e4cc69c5cca576a709bcb2cb59e0
6
+ metadata.gz: 4f399f48ff25956007f5ee9f7ff043b01c06639d1c58ba6d2dbffbaac5705b33b6c0d94a8a323279b7781aa8a199b8076c3f98e645b9e1a27c1e418cffd4c876
7
+ data.tar.gz: 0abe327dc2b32a4a82124a1de5f83e71f2d2674b2d3d102cd96db47a4dbed316d094d9041c9f2a2d729176c0cd2cc4ee1c393726f659d4a8d1460e2a1ac03ec6
data/Gemfile CHANGED
@@ -6,6 +6,7 @@ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
6
 
7
7
  gem "bcrypt_pbkdf"
8
8
  gem "colorize"
9
+ gem 'concurrent-ruby', require: 'concurrent'
9
10
  gem "e2mmap"
10
11
  gem "ed25519"
11
12
  gem "ejson"
data/bin/ops CHANGED
@@ -1,8 +1,31 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require_relative "../loader"
4
+ require 'optparse'
5
+
6
+
7
+ def usage
8
+ puts "Usage: ops [-f|--file <ops_yml>] action [<action args>"
9
+ puts " ops_yml: the config file to load instead of './ops.yml'"
10
+ puts " action_args: arguments to the action loaded from the config file; depends on the action"
11
+
12
+ exit(1)
13
+ end
5
14
 
15
+ options = {}
16
+ while ARGV[0]&.match(/^-/)
17
+ opt = ARGV.shift
18
+ case opt
19
+ when '-f', '--file'
20
+ usage unless ARGV.length >= 1
21
+
22
+ options[:file] = ARGV.shift
23
+ else
24
+ usage
25
+ end
26
+ end
27
+
28
+ require_relative "../loader"
6
29
  require 'ops'
7
30
 
8
- Ops.new(ARGV).run
31
+ Ops.new(ARGV, config_file: options[:file]).run
data/lib/action.rb CHANGED
@@ -17,7 +17,11 @@ class Action
17
17
  raise NotAllowedInEnvError, "Action not allowed in #{Environment.environment} environment."
18
18
  end
19
19
 
20
- Kernel.exec(to_s)
20
+ if perform_shell_expansion?
21
+ Kernel.exec(to_s)
22
+ else
23
+ Kernel.exec(*to_a)
24
+ end
21
25
  end
22
26
 
23
27
  def to_s
@@ -60,6 +64,10 @@ class Action
60
64
 
61
65
  private
62
66
 
67
+ def to_a
68
+ command.split(" ").reject(&:nil?) | @args
69
+ end
70
+
63
71
  def not_in_envs
64
72
  @config["not_in_envs"] || []
65
73
  end
@@ -75,4 +83,8 @@ class Action
75
83
 
76
84
  true
77
85
  end
86
+
87
+ def perform_shell_expansion?
88
+ @config["shell_expansion"].nil? ? true : @config["shell_expansion"]
89
+ end
78
90
  end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'concurrent'
4
+
5
+ require 'builtin'
6
+ require 'output'
7
+
8
+ module Builtins
9
+ class Countdown < Builtin
10
+ USAGE_STRING = "Usage: ops countdown <seconds>"
11
+
12
+ class << self
13
+ def description
14
+ "Like `sleep`, but displays time remaining in terminal."
15
+ end
16
+ end
17
+
18
+ def run
19
+ check_args
20
+
21
+ timer_task.execute
22
+
23
+ while timer_task.running?
24
+ sleep(1)
25
+ timer_task.shutdown if task_complete?
26
+ end
27
+ Output.out("\rCountdown complete after #{sleep_seconds}s.")
28
+ end
29
+
30
+ private
31
+
32
+ def check_args
33
+ check_arg_count
34
+ check_arg_is_positive_int
35
+ end
36
+
37
+ def check_arg_count
38
+ raise Builtin::ArgumentError, USAGE_STRING unless args.length == 1
39
+ end
40
+
41
+ def check_arg_is_positive_int
42
+ raise Builtin::ArgumentError, USAGE_STRING unless sleep_seconds.positive?
43
+ # raised when the arg is not an int
44
+ rescue ::ArgumentError
45
+ raise Builtin::ArgumentError, USAGE_STRING
46
+ end
47
+
48
+ def timer_task
49
+ @timer_task ||= Concurrent::TimerTask.new(run_now: true, execution_interval: 1) do
50
+ Output.print("\r#{seconds_left}")
51
+ end
52
+ end
53
+
54
+ def sleep_seconds
55
+ Integer(args.first)
56
+ end
57
+
58
+ def task_start_time
59
+ @task_start_time ||= Time.now
60
+ end
61
+
62
+ def task_end_time
63
+ @task_end_time ||= task_start_time + sleep_seconds
64
+ end
65
+
66
+ def task_complete?
67
+ Time.now > task_end_time
68
+ end
69
+
70
+ def seconds_left
71
+ Integer(task_end_time - Time.now + 1)
72
+ end
73
+ end
74
+ end
@@ -5,11 +5,11 @@ require 'dependencies/brew'
5
5
  module Dependencies
6
6
  class Cask < Brew
7
7
  def met?
8
- execute("brew cask list #{name}")
8
+ execute("brew list --cask #{name}")
9
9
  end
10
10
 
11
11
  def meet
12
- execute("brew cask install #{name}")
12
+ execute("brew install --cask #{name}")
13
13
  end
14
14
  end
15
15
  end
data/lib/environment.rb CHANGED
@@ -13,8 +13,9 @@ class Environment
13
13
  end
14
14
  end
15
15
 
16
- def initialize(env_hash)
16
+ def initialize(env_hash, config_path)
17
17
  @env_hash = env_hash
18
+ @config_path = config_path
18
19
  end
19
20
 
20
21
  def set_variables
@@ -26,7 +27,7 @@ class Environment
26
27
  private
27
28
 
28
29
  def set_ops_variables
29
- ENV["OPS_YML_DIR"] = Dir.pwd
30
+ ENV["OPS_YML_DIR"] = File.dirname(@config_path)
30
31
  ENV["OPS_VERSION"] = Version.version.to_s
31
32
  ENV["OPS_SECRETS_FILE"] = Secrets.config_path_for(Environment.environment)
32
33
  ENV["OPS_CONFIG_FILE"] = AppConfig.config_path_for(Environment.environment)
data/lib/ops.rb CHANGED
@@ -5,25 +5,15 @@ require 'yaml'
5
5
  require 'require_all'
6
6
  require "rubygems"
7
7
 
8
- require 'hook_handler'
9
- require 'action'
10
8
  require 'output'
11
9
  require 'options'
12
- require 'environment'
13
10
  require 'version'
14
- require 'action_list'
15
- require 'action_suggester'
16
- require 'forwards'
11
+ require 'runner'
17
12
 
18
13
  require_rel "builtins"
19
14
 
20
15
  # executes commands based on local `ops.yml`
21
16
  class Ops
22
- class UnknownActionError < StandardError; end
23
- class ActionConfigError < StandardError; end
24
-
25
- CONFIG_FILE = "ops.yml"
26
-
27
17
  INVALID_SYNTAX_EXIT_CODE = 64
28
18
  UNKNOWN_ACTION_EXIT_CODE = 65
29
19
  ERROR_LOADING_APP_CONFIG_EXIT_CODE = 66
@@ -40,50 +30,55 @@ class Ops
40
30
  end
41
31
  end
42
32
 
43
- def initialize(argv)
33
+ def initialize(argv, config_file: nil)
44
34
  @action_name = argv[0]
45
35
  @args = argv[1..-1]
36
+ @config_file = config_file || "ops.yml"
46
37
 
47
38
  Options.set(config["options"] || {})
48
39
  end
49
40
 
41
+ # rubocop:disable Metrics/MethodLength
42
+ # better to have all the rescues in one place
50
43
  def run
51
44
  # "return" is here to allow specs to stub "exit" without executing everything after it
52
45
  return exit(INVALID_SYNTAX_EXIT_CODE) unless syntax_valid?
53
46
  return exit(MIN_VERSION_NOT_MET_EXIT_CODE) unless min_version_met?
54
47
 
55
- run_action
56
- rescue UnknownActionError => e
48
+ runner.run
49
+ rescue Runner::UnknownActionError => e
57
50
  Output.error(e.to_s)
58
51
  Output.out(RECOMMEND_HELP_TEXT) unless print_did_you_mean
59
52
  exit(UNKNOWN_ACTION_EXIT_CODE)
60
- rescue ActionConfigError => e
53
+ rescue Runner::ActionConfigError => e
61
54
  Output.error("Error(s) running action '#{@action_name}': #{e}")
62
55
  exit(ACTION_CONFIG_ERROR_EXIT_CODE)
56
+ rescue Builtin::ArgumentError => e
57
+ Output.error("Error running builtin '#{@action_name}': #{e}")
58
+ exit(BUILTIN_SYNTAX_ERROR_EXIT_CODE)
59
+ rescue AppConfig::ParsingError => e
60
+ Output.error("Error parsing app config: #{e}")
61
+ exit(ERROR_LOADING_APP_CONFIG_EXIT_CODE)
62
+ rescue Action::NotAllowedInEnvError => e
63
+ Output.error("Error running action #{@action_name}: #{e}")
64
+ exit(ACTION_NOT_ALLOWED_IN_ENV_EXIT_CODE)
63
65
  end
66
+ # rubocop:enable Metrics/MethodLength
64
67
 
65
68
  private
66
69
 
67
70
  def syntax_valid?
68
- if @action_name.nil?
69
- Output.error("Usage: ops <action>")
70
- Output.out(RECOMMEND_HELP_TEXT)
71
- false
72
- else
73
- true
74
- end
71
+ return true unless @action_name.nil?
72
+
73
+ Output.error("Usage: ops <action>")
74
+ Output.out(RECOMMEND_HELP_TEXT)
75
+ false
75
76
  end
76
77
 
77
78
  def print_did_you_mean
78
- suggestions = did_you_mean.check(@action_name)
79
+ Output.out("Did you mean '#{runner.suggestions.join(", ")}'?") if runner.suggestions.any?
79
80
 
80
- Output.out("Did you mean '#{suggestions.join(", ")}'?") if suggestions.any?
81
-
82
- suggestions.any?
83
- end
84
-
85
- def did_you_mean
86
- ActionSuggester.new(action_list.names + action_list.aliases + builtin_names)
81
+ runner.suggestions.any?
87
82
  end
88
83
 
89
84
  def min_version_met?
@@ -101,96 +96,34 @@ class Ops
101
96
  config["min_version"]
102
97
  end
103
98
 
104
- def run_action
105
- return forward.run if forward
106
-
107
- do_before_all
108
-
109
- return builtin.run if builtin
110
-
111
- raise UnknownActionError, "Unknown action: #{@action_name}" unless action
112
- raise ActionConfigError, action.config_errors.join("; ") unless action.config_valid?
113
-
114
- do_before_action
115
- Output.notice("Running '#{action}' from #{CONFIG_FILE} in environment '#{ENV['environment']}'...")
116
- action.run
117
- rescue Builtin::ArgumentError => e
118
- Output.error("Error running builtin '#{@action_name}': #{e}")
119
- exit(BUILTIN_SYNTAX_ERROR_EXIT_CODE)
120
- rescue AppConfig::ParsingError => e
121
- Output.error("Error parsing app config: #{e}")
122
- exit(ERROR_LOADING_APP_CONFIG_EXIT_CODE)
123
- rescue Action::NotAllowedInEnvError => e
124
- Output.error("Error running action #{@action_name}: #{e}")
125
- exit(ACTION_NOT_ALLOWED_IN_ENV_EXIT_CODE)
126
- end
127
-
128
- def do_before_all
129
- AppConfig.load
130
- Secrets.load if action && action.load_secrets?
131
- environment.set_variables
132
- end
133
-
134
- def do_before_action
135
- return if ENV["OPS_RUNNING"] || action.skip_hooks?("before")
136
-
137
- # this prevents before hooks from running in ops executed by ops
138
- ENV["OPS_RUNNING"] = "1"
139
- hook_handler.do_hooks("before")
140
- end
141
-
142
- def hook_handler
143
- @hook_handler ||= HookHandler.new(config)
144
- end
145
-
146
- def builtin
147
- @builtin ||= Builtin.class_for(name: @action_name).new(@args, config)
148
- rescue NameError
149
- # this means there isn't a builtin with that name in that module
150
- nil
151
- end
152
-
153
- def builtin_names
154
- Builtins.constants.select { |c| Builtins.const_get(c).is_a? Class }.map(&:downcase)
155
- end
156
-
157
- def forward
158
- @forward ||= Forwards.new(@config, @args).get(@action_name)
159
- end
160
-
161
- def action
162
- return action_list.get(@action_name) if action_list.get(@action_name)
163
- return action_list.get_by_alias(@action_name) if action_list.get_by_alias(@action_name)
164
- end
165
-
166
- def action_list
167
- @action_list ||= begin
168
- Output.warn("'ops.yml' has no 'actions' defined.") if config.any? && config["actions"].nil?
169
-
170
- ActionList.new(config["actions"], @args)
171
- end
99
+ def runner
100
+ @runner ||= Runner.new(@action_name, @args, config, config_file_absolute_path)
172
101
  end
173
102
 
174
103
  def config
175
104
  @config ||= begin
176
- if File.exist?(CONFIG_FILE)
177
- YAML.load_file(CONFIG_FILE)
105
+ if config_file_exists?
106
+ parsed_config_contents
178
107
  else
179
- Output.warn("File '#{CONFIG_FILE}' does not exist.") unless @action_name == "init"
108
+ Output.warn("File '#{@config_file}' does not exist.") unless @action_name == "init"
180
109
  {}
181
110
  end
182
- rescue StandardError => e
183
- Output.warn("Error parsing '#{CONFIG_FILE}': #{e}")
184
- {}
185
111
  end
186
112
  end
187
113
 
188
- def env_vars
189
- config.dig("options", "environment") || {}
114
+ def parsed_config_contents
115
+ YAML.load_file(@config_file)
116
+ rescue StandardError => e
117
+ Output.warn("Error parsing '#{@config_file}': #{e}")
118
+ {}
119
+ end
120
+
121
+ def config_file_exists?
122
+ File.exist?(@config_file)
190
123
  end
191
124
 
192
- def environment
193
- @environment ||= Environment.new(env_vars)
125
+ def config_file_absolute_path
126
+ File.expand_path(@config_file)
194
127
  end
195
128
  end
196
129
 
data/lib/runner.rb ADDED
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hook_handler'
4
+ require 'action'
5
+ require 'action_list'
6
+ require 'action_suggester'
7
+ require 'forwards'
8
+ require 'environment'
9
+
10
+ class Runner
11
+ class UnknownActionError < StandardError; end
12
+ class ActionConfigError < StandardError; end
13
+
14
+ def initialize(action_name, args, config, config_path)
15
+ @action_name = action_name
16
+ @args = args
17
+ @config = config
18
+ @config_path = config_path
19
+ end
20
+
21
+ def run
22
+ return forward.run if forward
23
+
24
+ do_before_all
25
+
26
+ return builtin.run if builtin
27
+
28
+ raise UnknownActionError, "Unknown action: #{@action_name}" unless action
29
+ raise ActionConfigError, action.config_errors.join("; ") unless action.config_valid?
30
+
31
+ do_before_action
32
+ Output.notice("Running '#{action}' in environment '#{ENV['environment']}'...")
33
+ action.run
34
+ end
35
+
36
+ def suggestions
37
+ @suggestions ||= ActionSuggester.new(action_list.names + action_list.aliases + builtin_names).check(@action_name)
38
+ end
39
+
40
+ private
41
+
42
+ def do_before_all
43
+ AppConfig.load
44
+ Secrets.load if action&.load_secrets?
45
+ environment.set_variables
46
+ end
47
+
48
+ def do_before_action
49
+ return if ENV["OPS_RUNNING"] || action.skip_hooks?("before")
50
+
51
+ # this prevents before hooks from running in ops executed by ops
52
+ ENV["OPS_RUNNING"] = "1"
53
+ hook_handler.do_hooks("before")
54
+ end
55
+
56
+ def hook_handler
57
+ @hook_handler ||= HookHandler.new(@config)
58
+ end
59
+
60
+ def builtin
61
+ @builtin ||= Builtin.class_for(name: @action_name).new(@args, @config)
62
+ rescue NameError
63
+ # this means there isn't a builtin with that name in that module
64
+ nil
65
+ end
66
+
67
+ def builtin_names
68
+ Builtins.constants.select { |c| Builtins.const_get(c).is_a? Class }.map(&:downcase)
69
+ end
70
+
71
+ def forward
72
+ @forward ||= Forwards.new(@config, @args).get(@action_name)
73
+ end
74
+
75
+ def action
76
+ return action_list.get(@action_name) if action_list.get(@action_name)
77
+ return action_list.get_by_alias(@action_name) if action_list.get_by_alias(@action_name)
78
+ end
79
+
80
+ def action_list
81
+ @action_list ||= begin
82
+ Output.warn("'ops.yml' has no 'actions' defined.") if @config.any? && @config["actions"].nil?
83
+
84
+ ActionList.new(@config["actions"], @args)
85
+ end
86
+ end
87
+
88
+ def env_vars
89
+ @config.dig("options", "environment") || {}
90
+ end
91
+
92
+ def environment
93
+ @environment ||= Environment.new(env_vars, @config_path)
94
+ end
95
+ end
data/ops_team.gemspec CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'ops_team'
5
- s.version = '1.0.0'
5
+ s.version = '1.3.0'
6
6
  s.authors = [
7
7
  'nickthecook@gmail.com'
8
8
  ]
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ops_team
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - nickthecook@gmail.com
@@ -94,22 +94,22 @@ dependencies:
94
94
  name: net-ssh
95
95
  requirement: !ruby/object:Gem::Requirement
96
96
  requirements:
97
- - - ">="
98
- - !ruby/object:Gem::Version
99
- version: 6.1.0
100
97
  - - "~>"
101
98
  - !ruby/object:Gem::Version
102
99
  version: '6.1'
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: 6.1.0
103
103
  type: :runtime
104
104
  prerelease: false
105
105
  version_requirements: !ruby/object:Gem::Requirement
106
106
  requirements:
107
- - - ">="
108
- - !ruby/object:Gem::Version
109
- version: 6.1.0
110
107
  - - "~>"
111
108
  - !ruby/object:Gem::Version
112
109
  version: '6.1'
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: 6.1.0
113
113
  - !ruby/object:Gem::Dependency
114
114
  name: require_all
115
115
  requirement: !ruby/object:Gem::Requirement
@@ -152,6 +152,7 @@ files:
152
152
  - lib/builtin.rb
153
153
  - lib/builtins/background.rb
154
154
  - lib/builtins/background_log.rb
155
+ - lib/builtins/countdown.rb
155
156
  - lib/builtins/down.rb
156
157
  - lib/builtins/env.rb
157
158
  - lib/builtins/envdiff.rb
@@ -181,6 +182,7 @@ files:
181
182
  - lib/ops.rb
182
183
  - lib/options.rb
183
184
  - lib/output.rb
185
+ - lib/runner.rb
184
186
  - lib/secrets.rb
185
187
  - lib/version.rb
186
188
  - loader.rb
@@ -204,7 +206,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
204
206
  - !ruby/object:Gem::Version
205
207
  version: '0'
206
208
  requirements: []
207
- rubygems_version: 3.0.3
209
+ rubygems_version: 3.1.2
208
210
  signing_key:
209
211
  specification_version: 4
210
212
  summary: ops_team handles basic operations tasks for your project, driven by YAML