gitlab-swat 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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