gitlab-swat 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d7764e07cd7efc9efec68895fe07f7c8a2c3db9d
4
+ data.tar.gz: 679cc6ac1ae287113cb9fc3f70c6282c2eaaedb6
5
+ SHA512:
6
+ metadata.gz: f323d1a8b248bad4c21b11a734a97697bb96961df59dee5b724dbe64bf4ecda27773a8120b40438349097c6f5511a30c60996412671fb40a49f8fe5e6b00deef
7
+ data.tar.gz: d5165ea4b0ebfb42e9ddc10d63fae6b8e2a5b1ea80e30f324ed0514b2ec99fc812b674684aebe3cb21f2e16bef0c0f40698f9bbe88dde91f0a57bc031d508a0a
data/.gitlab-ci.yml ADDED
@@ -0,0 +1,20 @@
1
+ image: ruby:2.3
2
+ before_script:
3
+ - ruby -v
4
+ - which ruby
5
+ - gem install bundler --no-ri --no-rdoc
6
+ - bundle install --jobs $(nproc) "${FLAGS[@]}" --path vendor
7
+ - git config --global user.email "you@example.com"
8
+ - git config --global user.name "Your Name"
9
+
10
+ cache:
11
+ paths:
12
+ - vendor
13
+
14
+ rspec:
15
+ script:
16
+ - bundle exec rspec -f d -c -b
17
+
18
+ rubocop:
19
+ script:
20
+ - bundle exec rubocop
data/.rubocop.yml ADDED
@@ -0,0 +1,30 @@
1
+ # Commonly used screens these days easily fit more than 80 characters.
2
+ Metrics/LineLength:
3
+ Max: 120
4
+ # Just use double quotes please
5
+ #
6
+ Style/StringLiterals:
7
+ EnforcedStyle: double_quotes
8
+
9
+ Style/FrozenStringLiteralComment:
10
+ Enabled: false
11
+
12
+ # Jim Weirich block style
13
+ Style/BlockDelimiters:
14
+ EnforcedStyle: semantic
15
+
16
+ Style/SignalException:
17
+ EnforcedStyle: semantic
18
+
19
+ Style/RaiseArgs:
20
+ EnforcedStyle: compact
21
+
22
+ Metrics/MethodLength:
23
+ Max: 15
24
+
25
+ Metrics/AbcSize:
26
+ Enabled: false
27
+
28
+ Metrics/BlockLength:
29
+ Exclude:
30
+ - 'spec/**/*.rb'
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "cog-rb", "~>0.4"
4
+
5
+ group "development", "test" do
6
+ gem "rspec", "~>3.5"
7
+ gem "rubocop", "~>0.42"
8
+ gem "simplecov", "~>0.13", require: false
9
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,52 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ ast (2.3.0)
5
+ cog-rb (0.4.4)
6
+ rake (~> 11.2)
7
+ diff-lcs (1.3)
8
+ docile (1.1.5)
9
+ json (2.0.3)
10
+ parser (2.4.0.0)
11
+ ast (~> 2.2)
12
+ powerpack (0.1.1)
13
+ rainbow (2.2.1)
14
+ rake (11.3.0)
15
+ rspec (3.5.0)
16
+ rspec-core (~> 3.5.0)
17
+ rspec-expectations (~> 3.5.0)
18
+ rspec-mocks (~> 3.5.0)
19
+ rspec-core (3.5.4)
20
+ rspec-support (~> 3.5.0)
21
+ rspec-expectations (3.5.0)
22
+ diff-lcs (>= 1.2.0, < 2.0)
23
+ rspec-support (~> 3.5.0)
24
+ rspec-mocks (3.5.0)
25
+ diff-lcs (>= 1.2.0, < 2.0)
26
+ rspec-support (~> 3.5.0)
27
+ rspec-support (3.5.0)
28
+ rubocop (0.48.0)
29
+ parser (>= 2.3.3.1, < 3.0)
30
+ powerpack (~> 0.1)
31
+ rainbow (>= 1.99.1, < 3.0)
32
+ ruby-progressbar (~> 1.7)
33
+ unicode-display_width (~> 1.0, >= 1.0.1)
34
+ ruby-progressbar (1.8.1)
35
+ simplecov (0.14.1)
36
+ docile (~> 1.1.0)
37
+ json (>= 1.8, < 3)
38
+ simplecov-html (~> 0.10.0)
39
+ simplecov-html (0.10.0)
40
+ unicode-display_width (1.1.3)
41
+
42
+ PLATFORMS
43
+ ruby
44
+
45
+ DEPENDENCIES
46
+ cog-rb (~> 0.4)
47
+ rspec (~> 3.5)
48
+ rubocop (~> 0.42)
49
+ simplecov (~> 0.13)
50
+
51
+ BUNDLED WITH
52
+ 1.13.7
data/README.md ADDED
@@ -0,0 +1,101 @@
1
+ [![coverage report](https://gitlab.com/gitlab-cog/swat/badges/master/coverage.svg)](https://gitlab.com/gitlab-cog/swat/commits/master)
2
+
3
+ # GitLab SWAT
4
+
5
+ A Successful Deployment Ends Peacefully With No Bullets Fired.
6
+ If That’s Simply Not Possible, SWAT Uses Special Weapons and Tactics to Keep the Public Safe
7
+
8
+ ## Defining ~~Weapons~~ Scripts
9
+
10
+ A script should only contain one class with the following structure
11
+
12
+ ```ruby
13
+ module Swat
14
+ #
15
+ # Command to use when calling the script file, it has to respect the module and class name
16
+ # because it will be imported from rails and called by name
17
+ #
18
+ class Command < BaseCommand
19
+ def prepare(context)
20
+ fail "I need at least 1 argument" if @args.empty?
21
+ context[:some_key] = "something"
22
+ "text to add to the prepare stage result"
23
+ end
24
+
25
+ def pre_check(context)
26
+ fail "something is not right" unless context[:some_key] == "something"
27
+ "text to add to the pre_check stage result"
28
+ end
29
+
30
+ def execute(context)
31
+ fail "execution failed!" if context.empty?
32
+ "Context so far is #{context}"
33
+ end
34
+ end
35
+ end
36
+ ```
37
+
38
+ ### Execution Stages
39
+
40
+ * prepare: initial stage, used to parse arguments, or whatever is necessary before getting into the pre_check stage.
41
+ * pre_check: stage used to validate that the command should continue to the execute stage if it is running in execute mode. Dryrun would only reach this stage.
42
+ * execute: the actual operation.
43
+
44
+ Any stage that raises an exception will stop the execution, and will force and early return with a failure state and the different messages from the executed phases.
45
+
46
+ ### Available tools
47
+
48
+ * `@args` the arguments tha are sent from the execution and reach the command, simply a string array.
49
+ * `context` a hashmap that is created before the prepare stage and is sent to all methods, use this to accumulate state across stages.
50
+
51
+ # Configuring in cog
52
+
53
+ ## Environment variables
54
+
55
+ * **SCRIPTS_REMOTE_URL** url pointing to the remote repository
56
+ * **SCRIPTS_LOCAL_PATH** folder where the remote repository will be downloaded to
57
+ * **RAILS_RUNNER_COMMAND** command used to run rails, for example: bundle exec rails runner ./scripts/lib/swat_run.rb
58
+ * **RAILS_WORKING_DIR** working dir in which to execute the rails runner command
59
+
60
+ ## Cog Commands
61
+
62
+ * `dryrun <script> [args]` executes the given script with arguments in dryrun mode
63
+ * `strike <script> [args]` executes the given script with arguments in execute mode
64
+ * `reload [-f]` clones or pulls the scripts repo, use _-f_ to wipe the repo and clone it from scratch
65
+
66
+ # Development
67
+
68
+ ## How to run integration tests
69
+
70
+ ### Dry Run Mode
71
+
72
+ ```sh
73
+ $ SCRIPTS_LOCAL_PATH=/home/user/src/gitlab.com/gitlab-cog/swat/scripts RAILS_RUNNER_COMMAND="rails r /home/user/src/gitlab.com/gitlab-cog/swat/lib/swat_run.rb" RAILS_WORKING_DIR=/home/user/src/gitlab.com/gitlab-cog/rails-project COG_COMMAND="dryrun" COG_ARGV_0="test" COG_ARGV_1="success" COG_ARGV_2="success" COG_ARGC=3 ./cog-command
74
+ COG_TEMPLATE: execution_result
75
+ {"execution_mode":"dryrun","prepare":{"successful":true,"output":"preparation is fine so far"},"pre_check":{"successful":true,"output":"all is gut"}}
76
+ ```
77
+
78
+ ### Strike Mode
79
+
80
+ ```sh
81
+ $ SCRIPTS_LOCAL_PATH=/home/user/src/gitlab.com/gitlab-cog/swat/scripts RAILS_RUNNER_COMMAND="rails r /home/user/src/gitlab.com/gitlab-cog/swat/lib/swat_run.rb" RAILS_WORKING_DIR=/home/user/src/gitlab.com/gitlab-cog/rails-project COG_COMMAND="strike" COG_ARGV_0="test" COG_ARGV_1="success" COG_ARGV_2="success" COG_ARGC=3 ./cog-command
82
+ COG_TEMPLATE: execution_result
83
+ {"execution_mode":"execute","prepare":{"successful":true,"output":"preparation is fine so far"},"pre_check":{"successful":true,"output":"all is gut"},"execute":{"successful":true,"output":"Context so far is {:prepared=\u003e\"done\", :checks=\u003e\"done\"}"}}
84
+ ```
85
+
86
+ ### Reload command
87
+
88
+ ```sh
89
+ $ SCRIPTS_LOCAL_PATH=/tmp/testing-cog/second SCRIPTS_REMOTE_URL=$(pwd) COG_COMMAND="reload" ./cog-command
90
+ {"source":"/home/user/src/gitlab.com/gitlab-cog/swat","target":"/tmp/testing-cog/scripts","action":"clone","wiped":false, "head":"1234 current commit"}
91
+ ```
92
+
93
+ ```sh
94
+ $ SCRIPTS_LOCAL_PATH=/tmp/testing-cog/second SCRIPTS_REMOTE_URL=$(pwd) COG_COMMAND="reload" ./cog-command
95
+ {"source":"/home/user/src/gitlab.com/gitlab-cog/swat","target":"/tmp/testing-cog/scripts","action":"pull","wiped":false, "head":"1234 current commit"}
96
+ ```
97
+
98
+ ```sh
99
+ $ SCRIPTS_LOCAL_PATH=/tmp/testing-cog/second SCRIPTS_REMOTE_URL=$(pwd) COG_COMMAND="reload" COG_OPTS=wipe COG_OPT_WIPE=true ./cog-command
100
+ {"source":"/home/user/src/gitlab.com/gitlab-cog/swat","target":"/tmp/testing-cog/scripts","action":"clone","wiped":true, "head":"1234 current commit"}
101
+ ```
data/cog-command ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Add the lib directory to the load path
4
+ libdir = File.join(File.dirname(__FILE__), "lib")
5
+ $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
6
+
7
+ require "bundler/setup"
8
+ require "cog"
9
+
10
+ Cog.bundle("swat")
data/config.yaml ADDED
@@ -0,0 +1,50 @@
1
+ ---
2
+ cog_bundle_version: 4
3
+ name: swat
4
+ version: 0.0.1
5
+ docker:
6
+ image: gitlab/swat
7
+ tag: 0.0.1
8
+ description: >
9
+ A Successful Deployment Ends Peacefully With No Bullets Fired.
10
+ If That’s Simply Not Possible, SWAT Uses Special Weapons and Tactics to Keep the Public Safe
11
+ config:
12
+ env:
13
+ - var: SCRIPT_REMOTE_URL
14
+ description: Required Url for a git repo where the scripts are stored
15
+ - var: SCRIPT_LOCAL_PATH
16
+ description: Required path to a local semi-persistent folder where to clone and update the scripts repo
17
+ - var: RAILS_EXECUTABLE
18
+ description: Required path in which to find the rails executable script with which load the script
19
+ homepage: https://gitlab.com/gitlab-cog/swat
20
+ author: Pablo Carranza <pablo@gitlab.com>
21
+ permissions:
22
+ - swat:reload
23
+ - swat:dryrun
24
+ - swat:strike
25
+ commands:
26
+ reload:
27
+ description: Clones or pulls the scripts repo
28
+ executable: /home/bundle/cog-command
29
+ options:
30
+ wipe:
31
+ type: bool
32
+ required: false
33
+ rules:
34
+ - must have swat:reload
35
+ dryrun:
36
+ description: Runs a script up to the precheck phase, used for training, showing or simply checking
37
+ executable: /home/bundle/cog-command
38
+ arguments: "<script> [args...]"
39
+ rules:
40
+ - must have swat:dryrun
41
+ strike:
42
+ description: Runs a script to the end, performing changes to the system.
43
+ executable: /home/bundle/cog-command
44
+ arguments: "<script> [args...]"
45
+ rules:
46
+ - must have swat:execute
47
+ templates:
48
+ execution_result:
49
+ body: |
50
+ ~json var=$results~
@@ -0,0 +1,27 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "gitlab-swat"
3
+ s.version = "0.1.0"
4
+ s.date = "2017-04-01"
5
+ s.summary = "ChatOps Cog Bundle that enables admins to remotely run predefined scripts in a rails console"
6
+ s.description = <<~eos
7
+ A Successful Deployment Ends Peacefully With No Bullets Fired.
8
+ If That’s Simply Not Possible, SWAT Uses Special Weapons and Tactics to Keep the Public Safe
9
+
10
+ GitLab-Swat allows admins to quickly deploy scripts that can be remotely executed through a rails console
11
+
12
+ Allowing fast action by using an external git repository as the scripts source, but keeping safety high by
13
+ enforcing a prepare-pre check-execute model that allows execution break at any stage if things are not going
14
+ as expected
15
+ eos
16
+ s.authors = ["Pablo Carranza"]
17
+ s.email = "pablo@gitlab.com"
18
+ s.files = `git ls-files -z`.split("\x0")
19
+ s.test_files = s.files.grep(%r{^(spec)/})
20
+ s.license = "MIT"
21
+
22
+ s.add_dependency "cog-rb", "~> 0.4"
23
+
24
+ s.add_development_dependency "rspec", "~> 3.5"
25
+ s.add_development_dependency "rubocop", "~> 0.42"
26
+ s.add_development_dependency "simplecov", "~> 0.13"
27
+ end
@@ -0,0 +1,18 @@
1
+ require "cog"
2
+ require "swat"
3
+ require "rails_loader"
4
+
5
+ module CogCmd
6
+ module Swat
7
+ #
8
+ # Cog Command that loads and dryruns the given script
9
+ #
10
+ class Dryrun < Cog::Command
11
+ def run_command
12
+ rails = ::Swat::RailsLoader.new
13
+ response.template = "execution_result"
14
+ response.content = rails.run("dryrun #{request.args.join(' ')}")
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,25 @@
1
+ require "cog"
2
+ require "json"
3
+ require "swat_git"
4
+
5
+ module CogCmd
6
+ module Swat
7
+ #
8
+ # Cog Command that [re]loads a local git repo for scripts
9
+ #
10
+ class Reload < Cog::Command
11
+ def run_command
12
+ git = ::Swat::Git.new
13
+ git.wipe if wipe?
14
+ result = { source: git.source,
15
+ target: git.target,
16
+ wiped: wipe? }.merge(git.update)
17
+ response.content = result.to_json
18
+ end
19
+
20
+ def wipe?
21
+ request.options["wipe"] == true || request.options["wipe"] == "true"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,18 @@
1
+ require "cog"
2
+ require "swat"
3
+ require "rails_loader"
4
+
5
+ module CogCmd
6
+ module Swat
7
+ #
8
+ # Cog Command that loads and executes the given script
9
+ #
10
+ class Strike < Cog::Command
11
+ def run_command
12
+ rails = ::Swat::RailsLoader.new
13
+ response.template = "execution_result"
14
+ response.content = rails.run("execute #{request.args.join(' ')}").to_s
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,44 @@
1
+ require "open3"
2
+
3
+ module Swat
4
+ #
5
+ # Loads Rails command runner
6
+ #
7
+ # Loads RAILS_RUNNER_COMMAND from the environment to define
8
+ #
9
+ # Sample script:
10
+ #
11
+ # SCRIPTS_LOCAL_PATH=path_to/scripts bundle exec rails runner
12
+ # path_to/scripts/lib/swat_run.rb execute test success success 2&>/dev/null
13
+ #
14
+ # Can read the runner command and the working dir from environment variables
15
+ # such as:
16
+ # RAILS_RUNNER_COMMAND
17
+ # RAILS_WORKING_DIR
18
+ #
19
+ class RailsLoader
20
+ def initialize(command = ENV["RAILS_RUNNER_COMMAND"] || "",
21
+ working_dir = ENV["RAILS_WORKING_DIR"] || Dir.getwd)
22
+ fail "Invalid RAILS_RUNNER_COMMAND, please provide a rails command" if command.nil? || command.empty?
23
+ fail "Invalid RAILS_WORKING_DIR, '#{working_dir}' does not exists" unless Dir.exist?(working_dir)
24
+ @rails_command = command
25
+ @rails_working_dir = working_dir
26
+ end
27
+
28
+ def run(args)
29
+ command = "#{@rails_command} #{args}"
30
+ env = {
31
+ "PATH" => ENV["PATH"],
32
+ "SCRIPTS_REMOTE_URL" => ENV["SCRIPTS_REMOTE_URL"],
33
+ "SCRIPTS_LOCAL_PATH" => ENV["SCRIPTS_LOCAL_PATH"]
34
+ }
35
+ Open3.popen3(env, command, unsetenv_others: true, chdir: @rails_working_dir) do |_, stdout, stderr, wait_thr|
36
+ if wait_thr.value != 0
37
+ fail "Command #{args} failed with err: '#{stderr.read.strip}' " \
38
+ "out: '#{stdout.read.strip}'"
39
+ end
40
+ stdout.read.strip
41
+ end
42
+ end
43
+ end
44
+ end
data/lib/swat.rb ADDED
@@ -0,0 +1,131 @@
1
+ require "pathname"
2
+ #
3
+ # Swat module, where all the stuff lives
4
+ #
5
+ module Swat
6
+ #
7
+ # Defines the basic behavior of a command
8
+ #
9
+ # Inherith to reuse all this logic, only pre-check and execute are mandatory methods
10
+ # the rest can be as is
11
+ #
12
+ # Use the context object to send state from one stage to the next
13
+ class BaseCommand
14
+ def initialize(args)
15
+ @args = args
16
+ end
17
+
18
+ def prepare(_context)
19
+ # validate arguments here
20
+ end
21
+
22
+ def pre_check(_context)
23
+ fail "PreChecks are not defined"
24
+ end
25
+
26
+ def execute(_context)
27
+ fail "Execution is not defined"
28
+ end
29
+ end
30
+
31
+ #
32
+ # Represents the whole execution pipeline of a command
33
+ #
34
+ # Returns a hashmap that contains the following keys
35
+ # execution_mode: self descriptive
36
+ # prepare, precheck, execute: the different stages of execution
37
+ # each stage will include a hash with:
38
+ # succesful: boolean, indicating if it was successful or not
39
+ # output: the stdout in case the stage was successfull, stderr otherwise
40
+ class CommandExecution
41
+ def initialize(command, execution_mode = "dryrun")
42
+ @command = command
43
+ @stages = stages(execution_mode)
44
+ @result = { execution_mode: execution_mode }
45
+ @context = {}
46
+ end
47
+
48
+ def run
49
+ @stages.each do |stage|
50
+ execute_stage(stage)
51
+ break unless @result[stage][:successful]
52
+ end
53
+ @result
54
+ end
55
+
56
+ def execute_stage(stage)
57
+ @result[stage] = { successful: true, output: @command.public_send(stage, @context) }
58
+ rescue => e
59
+ @result[stage] = { successful: false, output: e.to_s }
60
+ end
61
+
62
+ def stages(execution_mode)
63
+ case execution_mode
64
+ when "dryrun"
65
+ %i(prepare pre_check)
66
+ when "execute"
67
+ %i(prepare pre_check execute)
68
+ else
69
+ fail "Invalid execution mode '#{execution_mode}'"
70
+ end
71
+ end
72
+ end
73
+
74
+ #
75
+ # Parameters parsing helper class
76
+ #
77
+ # Assumes the first parameter to be the execution mode (dryrun or execute)
78
+ # Assumes the second parameter to be the script name
79
+ # Captures the rest as arguments that are piped in to the script
80
+ class Parameters
81
+ attr_accessor :execution_mode, :command, :args
82
+
83
+ def initialize(args = ARGV.clone)
84
+ @args = args
85
+ @execution_mode = @args.shift
86
+ @command = @args.shift
87
+ fail "Execution mode is mandatory" if @execution_mode.nil?
88
+ fail "No command was specified" if @command.nil?
89
+ end
90
+
91
+ def to_s
92
+ "#{@execution_mode} #{@command} #{@args.inspect}"
93
+ end
94
+ end
95
+
96
+ #
97
+ # An executable script
98
+ #
99
+ # Loads the script from file system using ruby's `require` command, then creates a
100
+ # ::Swat::Command and then calls `run` on it
101
+ #
102
+ # If a Swat::Command object cannot be found (NameError) the command is considered bogus.
103
+ class Script
104
+ def initialize(parameters, scripts_path = ENV["SCRIPTS_LOCAL_PATH"])
105
+ @parameters = parameters
106
+ @scripts_path = Pathname.new(scripts_path || "scripts")
107
+ end
108
+
109
+ def run
110
+ CommandExecution.new(create_command, @parameters.execution_mode).run
111
+ end
112
+
113
+ private
114
+
115
+ def command_file
116
+ @command_file ||= @scripts_path.join("#{@parameters.command}.rb")
117
+ end
118
+
119
+ def create_command
120
+ fail "Could not find command #{@parameters.command} in #{command_file.expand_path}" unless command_file.file?
121
+ require command_file.expand_path
122
+ ::Swat::Command.new(@parameters.args)
123
+ rescue NameError
124
+ raise "#{@parameters.command} does not define a Swat::Command object"
125
+ end
126
+ end
127
+
128
+ def self.run
129
+ Script.new(Parameters.new).run
130
+ end
131
+ end
data/lib/swat_git.rb ADDED
@@ -0,0 +1,62 @@
1
+ require "fileutils"
2
+
3
+ module Swat
4
+ #
5
+ # Git handler
6
+ #
7
+ class Git
8
+ attr_accessor :source, :target
9
+
10
+ def initialize(source = ENV["SCRIPTS_REMOTE_URL"], target = ENV["SCRIPTS_LOCAL_PATH"])
11
+ @source = source
12
+ @target = target
13
+ end
14
+
15
+ def wipe
16
+ FileUtils.rm_rf(@target) if File.writable?(@target)
17
+ end
18
+
19
+ def update
20
+ if Dir.exist?(@target)
21
+ pull
22
+ else
23
+ clone
24
+ end
25
+ end
26
+
27
+ def valid?
28
+ return false unless File.exist?(@target)
29
+ Dir.chdir(@target) {
30
+ `git config --get remote.origin.url`.strip == @source
31
+ }
32
+ end
33
+
34
+ private
35
+
36
+ def clone
37
+ repo_path = Pathname.new(@target)
38
+ parent_folder = repo_path.parent
39
+ repo_name = repo_path.basename
40
+ fail "Invalid target path #{parent_folder}" unless File.writable?(parent_folder)
41
+ Dir.chdir(parent_folder) do
42
+ `git clone #{@source} #{repo_name} 2> /dev/null`
43
+ end
44
+ { action: "clone", head: current_commit }
45
+ end
46
+
47
+ def pull
48
+ fail "Invalid target repo #{@target}" unless valid?
49
+ Dir.chdir(@target) do
50
+ `git pull origin 2> /dev/null`
51
+ end
52
+ { action: "pull", head: current_commit }
53
+ end
54
+
55
+ def current_commit
56
+ fail "Invalid target repo #{@target}" unless valid?
57
+ Dir.chdir(@target) do
58
+ `git log --oneline -1 HEAD`.strip
59
+ end
60
+ end
61
+ end
62
+ end
data/lib/swat_run.rb ADDED
@@ -0,0 +1,20 @@
1
+ # Usage: through rails execute the following command:
2
+ # bundle exec rails runner <path to>/marvin_run.rb <command> [arguments]
3
+ #
4
+ # Such as:
5
+ # - <path to>/marvin.rb points to the place where this file is located
6
+ # - command is the name of a file existing inside the scripts folder of this repo
7
+ # - arguments are the required arguments for the given command
8
+ #
9
+ # Implementing new commands:
10
+ # - Create a file in the scripts folder such as scripts/<command>.rb
11
+ # - In this file create a Swat::Command class that inheriths Swat::BaseCommand
12
+ # - It is mandatory to override pre_check and execute, these methods should either execute or fail with an exception
13
+ # - Optionally the method `validate` can be implemented to validate the received arguments before execution
14
+
15
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
16
+
17
+ require "json"
18
+ require_relative "swat"
19
+
20
+ puts Swat.run.to_json
File without changes
data/scripts/test.rb ADDED
@@ -0,0 +1,23 @@
1
+ module Swat
2
+ #
3
+ # Test command
4
+ #
5
+ class Command < BaseCommand
6
+ def prepare(context)
7
+ fail "I need at least 1 argument" if @args.empty?
8
+ context[:prepared] = "done"
9
+ "preparation is fine so far"
10
+ end
11
+
12
+ def pre_check(context)
13
+ fail "something is not right" unless @args[0].to_sym == :success
14
+ context[:checks] = "done"
15
+ "all is gut"
16
+ end
17
+
18
+ def execute(context)
19
+ fail "execution failed!" unless @args[1].to_sym == :success
20
+ "Context so far is #{context}"
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,6 @@
1
+ #!/bin/sh
2
+
3
+ >&2 echo "This is all wrong, and in stderr"
4
+ echo "This is in stdout"
5
+
6
+ exit 1
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "pathname"
4
+
5
+ $LOAD_PATH << Pathname.new(Dir.getwd).join("lib").expand_path.to_path
6
+ file = Pathname.new(Dir.getwd).join(ARGV.shift).expand_path.to_path
7
+
8
+ require_relative file
@@ -0,0 +1,21 @@
1
+ require_relative "spec_helper"
2
+ require "rails_loader"
3
+
4
+ describe Swat::RailsLoader do
5
+ it "can load and execute a command" do
6
+ expect(::Swat::RailsLoader
7
+ .new("./spec/helpers/rails_stub.rb lib/swat_run.rb")
8
+ .run("dryrun test success"))
9
+ .to eq("{\"execution_mode\":\"dryrun\",\"prepare\":{\"successful\":true," \
10
+ "\"output\":\"preparation is fine so far\"}," \
11
+ "\"pre_check\":{\"successful\":true,\"output\":\"all is gut\"}}")
12
+ end
13
+
14
+ it "can err out when the return code is not 0" do
15
+ expect {
16
+ ::Swat::RailsLoader
17
+ .new("./spec/helpers/fail_stub.sh")
18
+ .run("dryrun")
19
+ }.to raise_error(/Command dryrun failed with err/)
20
+ end
21
+ end
@@ -0,0 +1,43 @@
1
+ require "simplecov"
2
+
3
+ SimpleCov.start do
4
+ add_filter "vendor"
5
+ end
6
+
7
+ require "rspec"
8
+ require "swat"
9
+ require "tmpdir"
10
+ require "fileutils"
11
+
12
+ class GitSpecHelper
13
+ attr_accessor :source_repo, :target_dir
14
+
15
+ def initialize
16
+ @source_repo = Dir.mktmpdir
17
+ @target_dir = Dir.mktmpdir
18
+ Dir.chdir(@source_repo) do
19
+ `git init`
20
+ `echo "hi" > readme.md`
21
+ `git add readme.md`
22
+ `git commit -m "initial commit"`
23
+ end
24
+ clean_target
25
+ end
26
+
27
+ def destroy
28
+ FileUtils.rm_rf(@source_repo)
29
+ FileUtils.rm_rf(@target_dir) if File.exist?(@target_dir)
30
+ end
31
+
32
+ def clean_target
33
+ FileUtils.rm_rf(@target_dir)
34
+ end
35
+ end
36
+
37
+ def with_environment(args: [])
38
+ ENV["COG_ARGC"] = args.length.times { |n| ENV["COG_ARGV_#{n}"] = args[n] }.to_s
39
+ yield
40
+ ensure
41
+ ENV.delete("COG_ARGC")
42
+ args.length.times { |n| ENV.delete("COG_ARGV_#{n}") }
43
+ end
@@ -0,0 +1,64 @@
1
+ require_relative "spec_helper"
2
+ require_relative "../scripts/test"
3
+
4
+ describe Swat::CommandExecution do
5
+ it "fails with an invalid execution mode" do
6
+ expect { Swat::CommandExecution.new(Swat::Command.new([]), "invalid") }
7
+ .to raise_error(/Invalid execution mode 'invalid'/)
8
+ end
9
+
10
+ it "fails prepare stage without arguments" do
11
+ executor = Swat::CommandExecution.new(Swat::Command.new([]))
12
+ expect(executor.run).to eq(
13
+ execution_mode: "dryrun",
14
+ prepare: { successful: false, output: "I need at least 1 argument" }
15
+ )
16
+ end
17
+
18
+ it "stops execution on pre_checks when constraints are not met" do
19
+ executor = Swat::CommandExecution.new(Swat::Command.new([:failure]))
20
+ expect(executor.run).to eq(
21
+ execution_mode: "dryrun",
22
+ prepare: { successful: true, output: "preparation is fine so far" },
23
+ pre_check: { successful: false, output: "something is not right" }
24
+ )
25
+ end
26
+
27
+ it "it only goes as far as pre checks in dry run mode" do
28
+ executor = Swat::CommandExecution.new(Swat::Command.new([:success]))
29
+ expect(executor.run).to eq(
30
+ execution_mode: "dryrun",
31
+ prepare: { successful: true, output: "preparation is fine so far" },
32
+ pre_check: { successful: true, output: "all is gut" }
33
+ )
34
+ end
35
+
36
+ it "stops execution on pre_checks when prechecks fail" do
37
+ executor = Swat::CommandExecution.new(Swat::Command.new([:failure]), "execute")
38
+ expect(executor.run).to eq(
39
+ execution_mode: "execute",
40
+ prepare: { successful: true, output: "preparation is fine so far" },
41
+ pre_check: { successful: false, output: "something is not right" }
42
+ )
43
+ end
44
+
45
+ it "execution can fail and report it" do
46
+ executor = Swat::CommandExecution.new(Swat::Command.new(%i(success failure)), "execute")
47
+ expect(executor.run).to eq(
48
+ execution_mode: "execute",
49
+ prepare: { successful: true, output: "preparation is fine so far" },
50
+ pre_check: { successful: true, output: "all is gut" },
51
+ execute: { successful: false, output: "execution failed!" }
52
+ )
53
+ end
54
+
55
+ it "execution can succeed" do
56
+ executor = Swat::CommandExecution.new(Swat::Command.new(%i(success success)), "execute")
57
+ expect(executor.run).to eq(
58
+ execution_mode: "execute",
59
+ prepare: { successful: true, output: "preparation is fine so far" },
60
+ pre_check: { successful: true, output: "all is gut" },
61
+ execute: { successful: true, output: "Context so far is {:prepared=>\"done\", :checks=>\"done\"}" }
62
+ )
63
+ end
64
+ end
@@ -0,0 +1,13 @@
1
+ require_relative "spec_helper"
2
+
3
+ describe Swat::BaseCommand do
4
+ it "errs when calling prechecks" do
5
+ cmd = Swat::BaseCommand.new(:sentinel)
6
+ expect { cmd.pre_check({}) }.to raise_error(/PreChecks are not defined/)
7
+ end
8
+
9
+ it "errs when calling execute" do
10
+ cmd = Swat::BaseCommand.new(:sentinel)
11
+ expect { cmd.execute({}) }.to raise_error(/Execution is not defined/)
12
+ end
13
+ end
@@ -0,0 +1,8 @@
1
+ require_relative "spec_helper"
2
+ require "yaml"
3
+
4
+ describe "Configuration file" do
5
+ it "can be parsed as correct yaml" do
6
+ expect(YAML.parse("config.yaml")).not_to be_nil
7
+ end
8
+ end
@@ -0,0 +1,21 @@
1
+ require "spec_helper"
2
+ require "cog_cmd/swat/dryrun"
3
+
4
+ describe CogCmd::Swat::Dryrun do
5
+ before do allow(STDIN).to receive(:tty?) { true } end
6
+ before do
7
+ ENV["RAILS_RUNNER_COMMAND"] = "./spec/helpers/rails_stub.rb lib/swat_run.rb"
8
+ end
9
+ after do ENV.delete("RAILS_RUNNER_COMMAND") end
10
+
11
+ it "can be called" do
12
+ command = CogCmd::Swat::Dryrun.new
13
+ with_environment(args: ["test success"]) do
14
+ command.run_command
15
+ end
16
+ expect(command.response.content)
17
+ .to eq("{\"execution_mode\":\"dryrun\",\"prepare\":{\"successful\":true," \
18
+ "\"output\":\"preparation is fine so far\"}," \
19
+ "\"pre_check\":{\"successful\":true,\"output\":\"all is gut\"}}")
20
+ end
21
+ end
@@ -0,0 +1,66 @@
1
+ require "spec_helper"
2
+ require "swat_git"
3
+
4
+ describe Swat::Git do
5
+ context "Given a valid URL" do
6
+ before(:all) do
7
+ @git = GitSpecHelper.new
8
+ end
9
+
10
+ def add_commit
11
+ Dir.chdir(@git.source_repo) {
12
+ `echo "how you doing?" >> readme.md`
13
+ `git commit -a -m "Another line in"`
14
+ }
15
+ end
16
+
17
+ before(:each) do @git.clean_target end
18
+
19
+ it "can clone a repo" do
20
+ repo = Swat::Git.new(@git.source_repo, @git.target_dir)
21
+ repo.update
22
+ expect(repo.valid?).to be_truthy
23
+ end
24
+
25
+ it "can determine that a repo is invalid when it doesn't exists" do
26
+ expect(Swat::Git.new(@git.source_repo, @git.target_dir).valid?).to be_falsey
27
+ end
28
+
29
+ it "can clone and then update a repo" do
30
+ repo = Swat::Git.new(@git.source_repo, @git.target_dir)
31
+ repo.update
32
+ add_commit
33
+ repo.update
34
+ expect(repo.valid?).to be_truthy
35
+ end
36
+
37
+ it "can wipe a non existing repo" do
38
+ repo = Swat::Git.new(@git.source_repo, @git.target_dir)
39
+ repo.wipe
40
+ expect(File.exist?(@git.target_dir)).to be_falsey
41
+ end
42
+
43
+ it "can clone and then wipe a repo" do
44
+ repo = Swat::Git.new(@git.source_repo, @git.target_dir)
45
+ repo.update
46
+ repo.wipe
47
+ expect(File.exist?(@git.target_dir)).to be_falsey
48
+ end
49
+
50
+ it "can clone and then update a repo multiple times" do
51
+ repo = Swat::Git.new(@git.source_repo, @git.target_dir)
52
+ repo.update
53
+ 3.times do
54
+ add_commit
55
+ repo.update
56
+ end
57
+ expect(Dir.chdir(@git.target_dir) {
58
+ `git log --oneline | wc -l`.strip
59
+ }).to eq("5")
60
+ end
61
+
62
+ after(:all) do
63
+ @git.destroy
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,17 @@
1
+ require_relative "spec_helper"
2
+
3
+ describe Swat::Parameters do
4
+ it "loads the command with arguments" do
5
+ allow(ARGV).to receive(:clone) { %w(dryrun command arg) }
6
+ params = Swat::Parameters.new
7
+ expect(params.execution_mode).to eq("dryrun")
8
+ expect(params.command).to eq("command")
9
+ expect(params.args).to eq(["arg"])
10
+ expect(params.to_s).to eq("dryrun command [\"arg\"]")
11
+ end
12
+
13
+ it "fails to load if there is no command" do
14
+ allow(ARGV).to receive(:clone) { [] }
15
+ expect { Swat::Parameters.new }.to raise_error(/Execution mode is mandatory/)
16
+ end
17
+ end
@@ -0,0 +1,61 @@
1
+ require "spec_helper"
2
+ require "cog_cmd/swat/reload"
3
+
4
+ describe CogCmd::Swat::Reload do
5
+ before do allow(STDIN).to receive(:tty?) { true } end
6
+
7
+ let(:git) { GitSpecHelper.new }
8
+
9
+ before do
10
+ ENV["SCRIPTS_REMOTE_URL"] = git.source_repo
11
+ ENV["SCRIPTS_LOCAL_PATH"] = git.target_dir
12
+ end
13
+
14
+ it "reloads the repo" do
15
+ command = CogCmd::Swat::Reload.new
16
+ command.run_command
17
+ expect(JSON.parse(command.response.content)).to include(
18
+ "source" => git.source_repo,
19
+ "target" => git.target_dir,
20
+ "action" => "clone",
21
+ "wiped" => false
22
+ )
23
+ end
24
+
25
+ it "reloads the repo" do
26
+ begin
27
+ command = CogCmd::Swat::Reload.new
28
+ ENV["COG_OPTS"] = "wipe"
29
+ ENV["COG_OPT_WIPE"] = "true"
30
+ command.run_command
31
+ expect(JSON.parse(command.response.content)).to include(
32
+ "source" => git.source_repo,
33
+ "target" => git.target_dir,
34
+ "action" => "clone",
35
+ "wiped" => true
36
+ )
37
+ ensure
38
+ ENV.delete("COG_OPTS")
39
+ ENV.delete("COG_OPT_WIPE")
40
+ end
41
+ end
42
+
43
+ it "reloads the repo 2 times" do
44
+ command = CogCmd::Swat::Reload.new
45
+ command.run_command
46
+ command.run_command
47
+ command.run_command
48
+ expect(JSON.parse(command.response.content)).to include(
49
+ "source" => git.source_repo,
50
+ "target" => git.target_dir,
51
+ "action" => "pull",
52
+ "wiped" => false
53
+ )
54
+ end
55
+
56
+ after do
57
+ ENV.delete("SCRIPTS_REMOTE_URL")
58
+ ENV.delete("SCRIPTS_LOCAL_PATH")
59
+ git.destroy
60
+ end
61
+ end
@@ -0,0 +1,24 @@
1
+ require_relative "spec_helper"
2
+
3
+ describe Swat::Script do
4
+ context "with valid parameters" do
5
+ let(:params) {
6
+ Swat::Parameters.new(%w(dryrun test arg1 arg2))
7
+ }
8
+
9
+ it "can be created" do
10
+ script = Swat::Script.new(params)
11
+ expect(script).not_to be_nil
12
+ end
13
+
14
+ it "finds the test script" do
15
+ script = Swat::Script.new(params)
16
+ script.run
17
+ end
18
+
19
+ it "fils to find a valid script with a descriptive message" do
20
+ script = Swat::Script.new(params, "scr")
21
+ expect { script.run }.to raise_error(/Could not find command test in /)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,22 @@
1
+ require "spec_helper"
2
+ require "cog_cmd/swat/strike"
3
+
4
+ describe CogCmd::Swat::Strike do
5
+ before do allow(STDIN).to receive(:tty?) { true } end
6
+ before do
7
+ ENV["RAILS_RUNNER_COMMAND"] = "./spec/helpers/rails_stub.rb lib/swat_run.rb"
8
+ end
9
+ after do ENV.delete("RAILS_RUNNER_COMMAND") end
10
+
11
+ it "can be called" do
12
+ command = CogCmd::Swat::Strike.new
13
+ with_environment(args: ["test success success"]) do
14
+ command.run_command
15
+ end
16
+ expect(command.response.content)
17
+ .to eq("{\"execution_mode\":\"execute\",\"prepare\":{\"successful\":true," \
18
+ "\"output\":\"preparation is fine so far\"},\"pre_check\":{\"successful\":true," \
19
+ "\"output\":\"all is gut\"},\"execute\":{\"successful\":true," \
20
+ "\"output\":\"Context so far is {:prepared=>\\\"done\\\", :checks=>\\\"done\\\"}\"}}")
21
+ end
22
+ end
metadata ADDED
@@ -0,0 +1,151 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gitlab-swat
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Pablo Carranza
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-04-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: cog-rb
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.4'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.4'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.5'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.5'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubocop
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.42'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.42'
55
+ - !ruby/object:Gem::Dependency
56
+ name: simplecov
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.13'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.13'
69
+ description: |
70
+ A Successful Deployment Ends Peacefully With No Bullets Fired.
71
+ If That’s Simply Not Possible, SWAT Uses Special Weapons and Tactics to Keep the Public Safe
72
+
73
+ GitLab-Swat allows admins to quickly deploy scripts that can be remotely executed through a rails console
74
+
75
+ Allowing fast action by using an external git repository as the scripts source, but keeping safety high by
76
+ enforcing a prepare-pre check-execute model that allows execution break at any stage if things are not going
77
+ as expected
78
+ email: pablo@gitlab.com
79
+ executables: []
80
+ extensions: []
81
+ extra_rdoc_files: []
82
+ files:
83
+ - ".gitlab-ci.yml"
84
+ - ".rubocop.yml"
85
+ - Gemfile
86
+ - Gemfile.lock
87
+ - README.md
88
+ - cog-command
89
+ - config.yaml
90
+ - gitlab-swat.gemspec
91
+ - lib/cog_cmd/swat/dryrun.rb
92
+ - lib/cog_cmd/swat/reload.rb
93
+ - lib/cog_cmd/swat/strike.rb
94
+ - lib/rails_loader.rb
95
+ - lib/swat.rb
96
+ - lib/swat_git.rb
97
+ - lib/swat_run.rb
98
+ - scripts/invalid.rb
99
+ - scripts/test.rb
100
+ - spec/helpers/fail_stub.sh
101
+ - spec/helpers/rails_stub.rb
102
+ - spec/rails_loader_spec.rb
103
+ - spec/spec_helper.rb
104
+ - spec/swat_command_execution_spec.rb
105
+ - spec/swat_command_spec.rb
106
+ - spec/swat_config_spec.rb
107
+ - spec/swat_dryrun_spec.rb
108
+ - spec/swat_git_spec.rb
109
+ - spec/swat_parameters_spec.rb
110
+ - spec/swat_reload_spec.rb
111
+ - spec/swat_script_spec.rb
112
+ - spec/swat_strike_spec.rb
113
+ homepage:
114
+ licenses:
115
+ - MIT
116
+ metadata: {}
117
+ post_install_message:
118
+ rdoc_options: []
119
+ require_paths:
120
+ - lib
121
+ required_ruby_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ required_rubygems_version: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ requirements: []
132
+ rubyforge_project:
133
+ rubygems_version: 2.5.2
134
+ signing_key:
135
+ specification_version: 4
136
+ summary: ChatOps Cog Bundle that enables admins to remotely run predefined scripts
137
+ in a rails console
138
+ test_files:
139
+ - spec/helpers/fail_stub.sh
140
+ - spec/helpers/rails_stub.rb
141
+ - spec/rails_loader_spec.rb
142
+ - spec/spec_helper.rb
143
+ - spec/swat_command_execution_spec.rb
144
+ - spec/swat_command_spec.rb
145
+ - spec/swat_config_spec.rb
146
+ - spec/swat_dryrun_spec.rb
147
+ - spec/swat_git_spec.rb
148
+ - spec/swat_parameters_spec.rb
149
+ - spec/swat_reload_spec.rb
150
+ - spec/swat_script_spec.rb
151
+ - spec/swat_strike_spec.rb