ops_team 1.0.0 → 1.3.0

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 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