heroku-config 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
+ SHA256:
3
+ metadata.gz: 8d09f67aa9118f360968ebac3deda4367589a820fff3f58c1cbb55dc8f12da0f
4
+ data.tar.gz: 7fbda965589b2a43c44d928e893a579f340db81b7fbde46791fb27c65377ce0c
5
+ SHA512:
6
+ metadata.gz: eed2ef3cffa39a324c910771da3ccd45ab8d98856a36d3d45b380a93572fbf20cf0212eba620b7058caf9745e59f53059b4eea0b15585e35da9c44174722d1b3
7
+ data.tar.gz: 54399e1010245b85cdc57164d634b529ff6aaa79b93d326127db45588f699bbf86b5074c273e01261c47ef635ac082b303842ad767228f5aee7da771b7ef12fa
data/.gitignore ADDED
@@ -0,0 +1,16 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ _yardoc
7
+ coverage
8
+ doc/
9
+ InstalledFiles
10
+ lib/bundler/man
11
+ pkg
12
+ rdoc
13
+ spec/reports
14
+ test/tmp
15
+ test/version_tmp
16
+ tmp
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --require spec_helper
2
+ --color
3
+ --format documentation
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ This project *tries* to adhere to [Semantic Versioning](http://semver.org/), even before v1.0.
5
+
6
+ ## [0.1.0]
7
+ - Initial release.
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem dependencies in heroku-config.gemspec
4
+ gemspec
5
+
6
+ gem "codeclimate-test-reporter", group: :test, require: nil
data/Gemfile.lock ADDED
@@ -0,0 +1,86 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ heroku-config (0.1.0)
5
+ activesupport
6
+ aws-sdk-core
7
+ aws-sdk-iam
8
+ memoist
9
+ rainbow
10
+ thor
11
+ zeitwerk
12
+
13
+ GEM
14
+ remote: https://rubygems.org/
15
+ specs:
16
+ activesupport (6.0.1)
17
+ concurrent-ruby (~> 1.0, >= 1.0.2)
18
+ i18n (>= 0.7, < 2)
19
+ minitest (~> 5.1)
20
+ tzinfo (~> 1.1)
21
+ zeitwerk (~> 2.2)
22
+ aws-eventstream (1.0.3)
23
+ aws-partitions (1.240.0)
24
+ aws-sdk-core (3.78.0)
25
+ aws-eventstream (~> 1.0, >= 1.0.2)
26
+ aws-partitions (~> 1, >= 1.239.0)
27
+ aws-sigv4 (~> 1.1)
28
+ jmespath (~> 1.0)
29
+ aws-sdk-iam (1.31.0)
30
+ aws-sdk-core (~> 3, >= 3.71.0)
31
+ aws-sigv4 (~> 1.1)
32
+ aws-sigv4 (1.1.0)
33
+ aws-eventstream (~> 1.0, >= 1.0.2)
34
+ byebug (11.0.1)
35
+ cli_markdown (0.1.0)
36
+ codeclimate-test-reporter (1.0.9)
37
+ simplecov (<= 0.13)
38
+ concurrent-ruby (1.1.5)
39
+ diff-lcs (1.3)
40
+ docile (1.1.5)
41
+ i18n (1.7.0)
42
+ concurrent-ruby (~> 1.0)
43
+ jmespath (1.4.0)
44
+ json (2.2.0)
45
+ memoist (0.16.1)
46
+ minitest (5.13.0)
47
+ rainbow (3.0.0)
48
+ rake (13.0.1)
49
+ rspec (3.9.0)
50
+ rspec-core (~> 3.9.0)
51
+ rspec-expectations (~> 3.9.0)
52
+ rspec-mocks (~> 3.9.0)
53
+ rspec-core (3.9.0)
54
+ rspec-support (~> 3.9.0)
55
+ rspec-expectations (3.9.0)
56
+ diff-lcs (>= 1.2.0, < 2.0)
57
+ rspec-support (~> 3.9.0)
58
+ rspec-mocks (3.9.0)
59
+ diff-lcs (>= 1.2.0, < 2.0)
60
+ rspec-support (~> 3.9.0)
61
+ rspec-support (3.9.0)
62
+ simplecov (0.13.0)
63
+ docile (~> 1.1.0)
64
+ json (>= 1.8, < 3)
65
+ simplecov-html (~> 0.10.0)
66
+ simplecov-html (0.10.2)
67
+ thor (0.20.3)
68
+ thread_safe (0.3.6)
69
+ tzinfo (1.2.5)
70
+ thread_safe (~> 0.1)
71
+ zeitwerk (2.2.1)
72
+
73
+ PLATFORMS
74
+ ruby
75
+
76
+ DEPENDENCIES
77
+ bundler
78
+ byebug
79
+ cli_markdown
80
+ codeclimate-test-reporter
81
+ heroku-config!
82
+ rake
83
+ rspec
84
+
85
+ BUNDLED WITH
86
+ 2.0.2
data/Guardfile ADDED
@@ -0,0 +1,19 @@
1
+ guard "bundler", cmd: "bundle" do
2
+ watch("Gemfile")
3
+ watch(/^.+\.gemspec/)
4
+ end
5
+
6
+ guard :rspec, cmd: "bundle exec rspec" do
7
+ require "guard/rspec/dsl"
8
+ dsl = Guard::RSpec::Dsl.new(self)
9
+
10
+ # RSpec files
11
+ rspec = dsl.rspec
12
+ watch(rspec.spec_helper) { rspec.spec_dir }
13
+ watch(rspec.spec_support) { rspec.spec_dir }
14
+ watch(rspec.spec_files)
15
+
16
+ # Ruby files
17
+ ruby = dsl.ruby
18
+ dsl.watch_spec_files_for(ruby.lib_files)
19
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2019 Tung Nguyen
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # HerokuConfig
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/heroku-config.png)](http://badge.fury.io/rb/heroku-config)
4
+
5
+ Easily rotate AWS keys and heroku configs.
6
+
7
+ ## Usage
8
+
9
+ heroku-config aws-rotate APP
10
+
11
+ ## Example with Output
12
+
13
+ $ heroku-config aws-rotate protected-oasis-24054
14
+ => heroku config:get AWS_ACCESS_KEY_ID -a protected-oasis-24054
15
+ Updating access key for user: bob
16
+ Created new access key: AKIAXZ6ODJLQQEXAMPLE
17
+ => heroku config:set AWS_ACCESS_KEY_ID=AKIAXZ6ODJLQQEXAMPLE AWS_SECRET_ACCESS_KEY=sp4gmsuif0XgYG2cPiZbkvl93kTGaeDDhEXAMPLE -a protected-oasis-24054
18
+ Setting heroku config variables
19
+ Setting AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and restarting protected-oasis-24054... done, v21
20
+
21
+ AWS_ACCESS_KEY_ID: AKIAXZ6ODJLQQEXAMPLE
22
+ AWS_SECRET_ACCESS_KEY: sp4gmsuif0XgYG2cPiZbkvl93kTGaeDDhEXAMPLE
23
+ Old access key deleted: AKIAXZ6ODJLQSGGE27KK
24
+ $
25
+
26
+ ## Installation
27
+
28
+ Add this line to your application's Gemfile:
29
+
30
+ gem "heroku-config"
31
+
32
+ And then execute:
33
+
34
+ bundle
35
+
36
+ Or install it yourself as:
37
+
38
+ gem install heroku-config
39
+
40
+ ## Contributing
41
+
42
+ 1. Fork it
43
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
44
+ 3. Commit your changes (`git commit -am "Add some feature"`)
45
+ 4. Push to the branch (`git push origin my-new-feature`)
46
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ task default: :spec
5
+
6
+ RSpec::Core::RakeTask.new
7
+
8
+ require_relative "lib/heroku-config"
9
+ require "cli_markdown"
10
+ desc "Generates cli reference docs as markdown"
11
+ task :docs do
12
+ mkdir_p "docs/_includes"
13
+ CliMarkdown::Creator.create_all(cli_class: HerokuConfig::CLI, cli_name: "heroku-config")
14
+ end
data/exe/heroku-config ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Trap ^C
4
+ Signal.trap("INT") {
5
+ puts "\nCtrl-C detected. Exiting..."
6
+ sleep 0.1
7
+ exit
8
+ }
9
+
10
+ $:.unshift(File.expand_path("../../lib", __FILE__))
11
+ require "heroku-config"
12
+ require "heroku_config/cli"
13
+
14
+ HerokuConfig::CLI.start(ARGV)
@@ -0,0 +1,34 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "heroku_config/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "heroku-config"
8
+ spec.version = HerokuConfig::VERSION
9
+ spec.authors = ["Tung Nguyen"]
10
+ spec.email = ["tongueroo@gmail.com"]
11
+ spec.summary = "Heroku Config AWS Access Key Rotator"
12
+ spec.homepage = "https://github.com/tongueroo/heroku-config"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files`.split($/)
16
+ spec.bindir = "exe"
17
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "activesupport"
22
+ spec.add_dependency "aws-sdk-core" # for aws-sdk-sts
23
+ spec.add_dependency "aws-sdk-iam"
24
+ spec.add_dependency "memoist"
25
+ spec.add_dependency "rainbow"
26
+ spec.add_dependency "thor"
27
+ spec.add_dependency "zeitwerk"
28
+
29
+ spec.add_development_dependency "bundler"
30
+ spec.add_development_dependency "byebug"
31
+ spec.add_development_dependency "cli_markdown"
32
+ spec.add_development_dependency "rake"
33
+ spec.add_development_dependency "rspec"
34
+ end
@@ -0,0 +1 @@
1
+ require_relative "heroku_config"
@@ -0,0 +1,22 @@
1
+ require "zeitwerk"
2
+
3
+ module HerokuConfig
4
+ class Autoloader
5
+ class Inflector < Zeitwerk::Inflector
6
+ def camelize(basename, _abspath)
7
+ map = { cli: "CLI", version: "VERSION" }
8
+ map[basename.to_sym] || super
9
+ end
10
+ end
11
+
12
+ class << self
13
+ def setup
14
+ loader = Zeitwerk::Loader.new
15
+ loader.inflector = Inflector.new
16
+ loader.push_dir(File.dirname(__dir__)) # lib
17
+ loader.ignore("#{File.dirname(__dir__)}/heroku-config.rb")
18
+ loader.setup
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,106 @@
1
+ module HerokuConfig
2
+ class AwsKey < Base
3
+ include AwsServices
4
+ class MaxKeysError < StandardError; end
5
+
6
+ def initialize(options, access_key_id)
7
+ @options, @access_key_id = options, access_key_id
8
+ @app = options[:app]
9
+ end
10
+
11
+ def rotate
12
+ user_name = get_user_name
13
+
14
+ message = "Updating access key for user: #{user_name}"
15
+ message = "NOOP: #{message}" if ENV['HEROKU_CONFIG_TEST']
16
+ puts message.color(:green)
17
+ return false if @options[:noop]
18
+
19
+ check_max_keys_limit!(user_name)
20
+ new_key, new_secret = create_access_key(user_name)
21
+ wait_until_usable(new_key, new_secret)
22
+
23
+ update_heroku_config(new_key, new_secret)
24
+ delete_old_access_key(user_name)
25
+
26
+ true
27
+ end
28
+
29
+ def get_user_name
30
+ return "fakeuser" if @options[:noop]
31
+
32
+ resp = iam.get_access_key_last_used(
33
+ access_key_id: @access_key_id,
34
+ )
35
+ resp.user_name
36
+ end
37
+
38
+ def wait_until_usable(key, secret)
39
+ delay, retries = 5, 0
40
+ begin
41
+ sts.get_caller_identity
42
+ true
43
+ rescue Aws::STS::Errors::InvalidClientTokenId => e
44
+ puts "#{e.class}: #{e.message}"
45
+ retries += 1
46
+ if retries <= 20
47
+ puts "New IAM key not usable yet. Delaying for #{delay} seconds and retrying..."
48
+ sleep delay
49
+ retry
50
+ end
51
+ end
52
+ end
53
+
54
+ def delete_old_access_key(user_name)
55
+ resp = iam.list_access_keys(user_name: user_name)
56
+ access_keys = resp.access_key_metadata
57
+ # Important: Only delete if there are keys 2.
58
+ return if access_keys.size <= 1
59
+
60
+ old_key = access_keys.sort_by(&:create_date).first
61
+ iam.delete_access_key(user_name: user_name, access_key_id: old_key.access_key_id)
62
+ puts "Old access key deleted: #{old_key.access_key_id}"
63
+ end
64
+
65
+ def update_heroku_config(new_key, new_secret)
66
+ out = config.set(
67
+ "AWS_ACCESS_KEY_ID" => new_key,
68
+ "AWS_SECRET_ACCESS_KEY" => new_secret,
69
+ )
70
+ puts "Setting heroku config variables"
71
+ puts out
72
+ end
73
+
74
+ # Returns:
75
+ #
76
+ # #<struct Aws::IAM::Types::AccessKey
77
+ # user_name="tung",
78
+ # access_key_id="AKIAXZ6ODJLQUU6O3FD2",
79
+ # status="Active",
80
+ # secret_access_key="8eEnLLdR7gQE9fkFiDVuemi3qPf3mBMXxEXAMPLE",
81
+ # create_date=2019-08-13 21:14:35 UTC>>
82
+ #
83
+ def create_access_key(user_name)
84
+ resp = iam.create_access_key(
85
+ user_name: user_name,
86
+ )
87
+ access_key = resp.access_key
88
+ key, secret = access_key.access_key_id, access_key.secret_access_key
89
+ puts "Created new access key: #{key}"
90
+ [key, secret]
91
+ end
92
+
93
+ private
94
+ # Check if there are 2 keys, cannot rotate if there are 2 keys already.
95
+ # Raise error if there are 2 keys.
96
+ # Returns false if not at max limit
97
+ MAX_KEYS = 2
98
+ def check_max_keys_limit!(user_name)
99
+ resp = iam.list_access_keys(user_name: user_name)
100
+ return false if resp.access_key_metadata.size < MAX_KEYS # not at max limit
101
+
102
+ puts "ERROR: There are already 2 access keys for user: #{user_name.color(:green)}".color(:red)
103
+ raise MaxKeysError
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,19 @@
1
+ module HerokuConfig
2
+ class AwsRotate < Base
3
+ def initialize(options={})
4
+ @options = options
5
+ @app = options[:app]
6
+ end
7
+
8
+ def run
9
+ key_id = config.get("AWS_ACCESS_KEY_ID")
10
+ unless key_id
11
+ puts "WARN: No AWS_ACCESS_KEY_ID found for #{@app.color(:green)} app. Exiting."
12
+ exit 0
13
+ end
14
+
15
+ aws_key = AwsKey.new(@options, key_id)
16
+ aws_key.rotate
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,18 @@
1
+ require "aws-sdk-iam"
2
+ require "aws-sdk-sts"
3
+
4
+ module HerokuConfig
5
+ module AwsServices
6
+ extend Memoist
7
+
8
+ def iam
9
+ Aws::IAM::Client.new
10
+ end
11
+ memoize :iam
12
+
13
+ def sts
14
+ Aws::STS::Client.new
15
+ end
16
+ memoize :sts
17
+ end
18
+ end
@@ -0,0 +1,10 @@
1
+ module HerokuConfig
2
+ class Base
3
+ extend Memoist
4
+
5
+ def config
6
+ Config.new(@app)
7
+ end
8
+ memoize :config
9
+ end
10
+ end
@@ -0,0 +1,29 @@
1
+ module HerokuConfig
2
+ class CLI < Command
3
+ class_option :verbose, type: :boolean
4
+ class_option :noop, type: :boolean
5
+
6
+ desc "aws-rotate APP", "Say aws_rotate to APP"
7
+ long_desc Help.text(:aws_rotate)
8
+ def aws_rotate(app)
9
+ AwsRotate.new(options.merge(app: app)).run
10
+ end
11
+
12
+ desc "completion *PARAMS", "Prints words for auto-completion."
13
+ long_desc Help.text("completion")
14
+ def completion(*params)
15
+ Completer.new(CLI, *params).run
16
+ end
17
+
18
+ desc "completion_script", "Generates a script that can be eval to setup auto-completion."
19
+ long_desc Help.text("completion_script")
20
+ def completion_script
21
+ Completer::Script.generate
22
+ end
23
+
24
+ desc "version", "prints version"
25
+ def version
26
+ puts VERSION
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,82 @@
1
+ require "thor"
2
+
3
+ # Override thor's long_desc identation behavior
4
+ # https://github.com/erikhuda/thor/issues/398
5
+ class Thor
6
+ module Shell
7
+ class Basic
8
+ def print_wrapped(message, options = {})
9
+ message = "\n#{message}" unless message[0] == "\n"
10
+ stdout.puts message
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ module HerokuConfig
17
+ class Command < Thor
18
+ class << self
19
+ def dispatch(m, args, options, config)
20
+ # Allow calling for help via:
21
+ # heroku-config command help
22
+ # heroku-config command -h
23
+ # heroku-config command --help
24
+ # heroku-config command -D
25
+ #
26
+ # as well thor's normal way:
27
+ #
28
+ # heroku-config help command
29
+ help_flags = Thor::HELP_MAPPINGS + ["help"]
30
+ if args.length > 1 && !(args & help_flags).empty?
31
+ args -= help_flags
32
+ args.insert(-2, "help")
33
+ end
34
+
35
+ # heroku-config version
36
+ # heroku-config --version
37
+ # heroku-config -v
38
+ version_flags = ["--version", "-v"]
39
+ if args.length == 1 && !(args & version_flags).empty?
40
+ args = ["version"]
41
+ end
42
+
43
+ super
44
+ end
45
+
46
+ # Override command_help to include the description at the top of the
47
+ # long_description.
48
+ def command_help(shell, command_name)
49
+ meth = normalize_command_name(command_name)
50
+ command = all_commands[meth]
51
+ alter_command_description(command)
52
+ super
53
+ end
54
+
55
+ def alter_command_description(command)
56
+ return unless command
57
+
58
+ # Add description to beginning of long_description
59
+ long_desc = if command.long_description
60
+ "#{command.description}\n\n#{command.long_description}"
61
+ else
62
+ command.description
63
+ end
64
+
65
+ # add reference url to end of the long_description
66
+ unless website.empty?
67
+ full_command = [command.ancestor_name, command.name].compact.join('-')
68
+ url = "#{website}/reference/heroku-config-#{full_command}"
69
+ long_desc += "\n\nHelp also available at: #{url}"
70
+ end
71
+
72
+ command.long_description = long_desc
73
+ end
74
+ private :alter_command_description
75
+
76
+ # meant to be overriden
77
+ def website
78
+ ""
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,6 @@
1
+ class HerokuConfig::Completer::Script
2
+ def self.generate
3
+ bash_script = File.expand_path("script.sh", File.dirname(__FILE__))
4
+ puts "source #{bash_script}"
5
+ end
6
+ end
@@ -0,0 +1,10 @@
1
+ _heroku-config() {
2
+ COMPREPLY=()
3
+ local word="${COMP_WORDS[COMP_CWORD]}"
4
+ local words=("${COMP_WORDS[@]}")
5
+ unset words[0]
6
+ local completion=$(heroku-config completion ${words[@]})
7
+ COMPREPLY=( $(compgen -W "$completion" -- "$word") )
8
+ }
9
+
10
+ complete -F _heroku-config heroku-config
@@ -0,0 +1,159 @@
1
+ =begin
2
+ Code Explanation:
3
+
4
+ There are 3 types of things to auto-complete:
5
+
6
+ 1. command: the command itself
7
+ 2. parameters: command parameters.
8
+ 3. options: command options
9
+
10
+ Here's an example:
11
+
12
+ mycli hello name --from me
13
+
14
+ * command: hello
15
+ * parameters: name
16
+ * option: --from
17
+
18
+ When command parameters are done processing, the remaining completion words will be options. We can tell that the command params are completed based on the method arity.
19
+
20
+ ## Arity
21
+
22
+ For example, say you had a method for a CLI command with the following form:
23
+
24
+ ufo scale service count --cluster development
25
+
26
+ It's equivalent ruby method:
27
+
28
+ scale(service, count) = has an arity of 2
29
+
30
+ So typing:
31
+
32
+ ufo scale service count [TAB] # there are 3 parameters including the "scale" command according to Thor's CLI processing.
33
+
34
+ So the completion should only show options, something like this:
35
+
36
+ --noop --verbose --cluster
37
+
38
+ ## Splat Arguments
39
+
40
+ When the ruby method has a splat argument, it's arity is negative. Here are some example methods and their arities.
41
+
42
+ ship(service) = 1
43
+ scale(service, count) = 2
44
+ ships(*services) = -1
45
+ foo(example, *rest) = -2
46
+
47
+ Fortunately, negative and positive arity values are processed the same way. So we take simply take the absolute value of the arity and process it the same.
48
+
49
+ Here are some test cases, hit TAB after typing the command:
50
+
51
+ heroku-config completion
52
+ heroku-config completion hello
53
+ heroku-config completion hello name
54
+ heroku-config completion hello name --
55
+ heroku-config completion hello name --noop
56
+
57
+ heroku-config completion
58
+ heroku-config completion sub:goodbye
59
+ heroku-config completion sub:goodbye name
60
+
61
+ ## Subcommands and Thor::Group Registered Commands
62
+
63
+ Sometimes the commands are not simple thor commands but are subcommands or Thor::Group commands. A good specific example is the ufo tool.
64
+
65
+ * regular command: ufo ship
66
+ * subcommand: ufo docker
67
+ * Thor::Group command: ufo init
68
+
69
+ Auto-completion accounts for each of these type of commands.
70
+ =end
71
+ module HerokuConfig
72
+ class Completer
73
+ def initialize(command_class, *params)
74
+ @params = params
75
+ @current_command = @params[0]
76
+ @command_class = command_class # CLI initiall
77
+ end
78
+
79
+ def run
80
+ if subcommand?(@current_command)
81
+ subcommand_class = @command_class.subcommand_classes[@current_command]
82
+ @params.shift # destructive
83
+ Completer.new(subcommand_class, *@params).run # recursively use subcommand
84
+ return
85
+ end
86
+
87
+ # full command has been found!
88
+ unless found?(@current_command)
89
+ puts all_commands
90
+ return
91
+ end
92
+
93
+ # will only get to here if command aws found (above)
94
+ arity = @command_class.instance_method(@current_command).arity.abs
95
+ if @params.size > arity or thor_group_command?
96
+ puts options_completion
97
+ else
98
+ puts params_completion
99
+ end
100
+ end
101
+
102
+ def subcommand?(command)
103
+ @command_class.subcommands.include?(command)
104
+ end
105
+
106
+ # hacky way to detect that command is a registered Thor::Group command
107
+ def thor_group_command?
108
+ command_params(raw=true) == [[:rest, :args]]
109
+ end
110
+
111
+ def found?(command)
112
+ public_methods = @command_class.public_instance_methods(false)
113
+ command && public_methods.include?(command.to_sym)
114
+ end
115
+
116
+ # all top-level commands
117
+ def all_commands
118
+ commands = @command_class.all_commands.reject do |k,v|
119
+ v.is_a?(Thor::HiddenCommand)
120
+ end
121
+ commands.keys
122
+ end
123
+
124
+ def command_params(raw=false)
125
+ params = @command_class.instance_method(@current_command).parameters
126
+ # Example:
127
+ # >> Sub.instance_method(:goodbye).parameters
128
+ # => [[:req, :name]]
129
+ # >>
130
+ raw ? params : params.map!(&:last)
131
+ end
132
+
133
+ def params_completion
134
+ offset = @params.size - 1
135
+ offset_params = command_params[offset..-1]
136
+ command_params[offset..-1].first
137
+ end
138
+
139
+ def options_completion
140
+ used = ARGV.select { |a| a.include?('--') } # so we can remove used options
141
+
142
+ method_options = @command_class.all_commands[@current_command].options.keys
143
+ class_options = @command_class.class_options.keys
144
+
145
+ all_options = method_options + class_options + ['help']
146
+
147
+ all_options.map! { |o| "--#{o.to_s.gsub('_','-')}" }
148
+ filtered_options = all_options - used
149
+ filtered_options.uniq
150
+ end
151
+
152
+ # Useful for debugging. Using puts messes up completion.
153
+ def log(msg)
154
+ File.open("/tmp/complete.log", "a") do |file|
155
+ file.puts(msg)
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,72 @@
1
+ require 'open3'
2
+
3
+ module HerokuConfig
4
+ class Config
5
+ def initialize(app)
6
+ @app = app
7
+ check_heroku_cli_installed!
8
+ end
9
+
10
+ def get(name)
11
+ return "fakevalue" if ENV['HEROKU_CONFIG_TEST']
12
+ sh "heroku config:get #{name} -a #{@app}"
13
+ end
14
+
15
+ def set(*params)
16
+ case params.size
17
+ when 1 # Hash
18
+ set_many(params.first)
19
+ when 2 # 2 Strings
20
+ set_one(name, value)
21
+ else
22
+ raise "ERROR: #{params.class} is a class that is not supported"
23
+ end
24
+ end
25
+
26
+ def set_one(name, value)
27
+ sh "heroku config:set #{name} #{value} -a #{@app}"
28
+ end
29
+
30
+ # Example:
31
+ #
32
+ # set(a: 1, b: 2)
33
+ # =>
34
+ # heroku config:set a=1 b=2 -a APP
35
+ #
36
+ def set_many(hash)
37
+ args = hash.map { |k,v| "#{k}=#{v}" }.join(' ')
38
+ sh "heroku config:set #{args} -a #{@app}", include_stderr: true
39
+ end
40
+
41
+ private
42
+ def sh(command, include_stderr: false)
43
+ puts "=> #{command}"
44
+ stdout, stderr, status = Open3.capture3(command)
45
+
46
+ out = stdout.strip
47
+ unless status.success?
48
+ puts "ERROR: #{stderr}".color(:red)
49
+ if stderr.empty?
50
+ puts "STDOUT: #{stdout}"
51
+ end
52
+ exit 1
53
+ end
54
+ if include_stderr
55
+ stderr + "\n" + out
56
+ else
57
+ out
58
+ end
59
+ end
60
+
61
+ def check_heroku_cli_installed!
62
+ return if heroku_cli_installed?
63
+
64
+ puts "The heroku cli is not installed. Please install the heroku cli: https://devcenter.heroku.com/articles/heroku-cli"
65
+ exit 1
66
+ end
67
+
68
+ def heroku_cli_installed?
69
+ system("type heroku > /dev/null 2>&1")
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,18 @@
1
+ ## Examples
2
+
3
+ heroku-config aws-rotate APP
4
+
5
+ ## Example with Output
6
+
7
+ $ heroku-config aws-rotate protected-oasis-24054
8
+ => heroku config:get AWS_ACCESS_KEY_ID -a protected-oasis-24054
9
+ Updating access key for user: bob
10
+ Created new access key: AKIAXZ6ODJLQQEXAMPLE
11
+ => heroku config:set AWS_ACCESS_KEY_ID=AKIAXZ6ODJLQQEXAMPLE AWS_SECRET_ACCESS_KEY=sp4gmsuif0XgYG2cPiZbkvl93kTGaeDDhEXAMPLE -a protected-oasis-24054
12
+ Setting heroku config variables
13
+ Setting AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and restarting protected-oasis-24054... done, v21
14
+
15
+ AWS_ACCESS_KEY_ID: AKIAXZ6ODJLQQEXAMPLE
16
+ AWS_SECRET_ACCESS_KEY: sp4gmsuif0XgYG2cPiZbkvl93kTGaeDDhEXAMPLE
17
+ Old access key deleted: AKIAXZ6ODJLQSGGE27KK
18
+ $
@@ -0,0 +1,20 @@
1
+ ## Examples
2
+
3
+ heroku-config completion
4
+
5
+ Prints words for TAB auto-completion.
6
+
7
+ heroku-config completion
8
+ heroku-config completion hello
9
+ heroku-config completion hello name
10
+
11
+ To enable, TAB auto-completion add the following to your profile:
12
+
13
+ eval $(heroku-config completion_script)
14
+
15
+ Auto-completion example usage:
16
+
17
+ heroku-config [TAB]
18
+ heroku-config hello [TAB]
19
+ heroku-config hello name [TAB]
20
+ heroku-config hello name --[TAB]
@@ -0,0 +1,3 @@
1
+ To use, add the following to your `~/.bashrc` or `~/.profile`
2
+
3
+ eval $(heroku-config completion_script)
@@ -0,0 +1,9 @@
1
+ module HerokuConfig::Help
2
+ class << self
3
+ def text(namespaced_command)
4
+ path = namespaced_command.to_s.gsub(':','/')
5
+ path = File.expand_path("../help/#{path}.md", __FILE__)
6
+ IO.read(path) if File.exist?(path)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ module HerokuConfig
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,11 @@
1
+ $:.unshift(File.expand_path("../", __FILE__))
2
+ require "heroku_config/version"
3
+ require "memoist"
4
+ require "rainbow/ext/string"
5
+
6
+ require "heroku_config/autoloader"
7
+ HerokuConfig::Autoloader.setup
8
+
9
+ module HerokuConfig
10
+ class Error < StandardError; end
11
+ end
@@ -0,0 +1,13 @@
1
+ describe HerokuConfig::CLI do
2
+ before(:all) do
3
+ @args = "APP --noop"
4
+ end
5
+
6
+ describe "heroku-config" do
7
+ it "aws-rotate" do
8
+ out = execute("exe/heroku-config aws-rotate #{@args}")
9
+ puts out
10
+ expect(out).to include("NOOP: Updating access key for user: fakeuser")
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,29 @@
1
+ ENV["HEROKU_CONFIG_TEST"] = "1"
2
+
3
+ # CodeClimate test coverage: https://docs.codeclimate.com/docs/configuring-test-coverage
4
+ # require 'simplecov'
5
+ # SimpleCov.start
6
+
7
+ require "pp"
8
+ require "byebug"
9
+ root = File.expand_path("../", File.dirname(__FILE__))
10
+ require "#{root}/lib/heroku-config"
11
+
12
+ module Helper
13
+ def execute(cmd)
14
+ puts "Running: #{cmd}" if show_command?
15
+ out = `#{cmd}`
16
+ puts out if show_command?
17
+ out
18
+ end
19
+
20
+ # Added SHOW_COMMAND because DEBUG is also used by other libraries like
21
+ # bundler and it shows its internal debugging logging also.
22
+ def show_command?
23
+ ENV['DEBUG'] || ENV['SHOW_COMMAND']
24
+ end
25
+ end
26
+
27
+ RSpec.configure do |c|
28
+ c.include Helper
29
+ end
metadata ADDED
@@ -0,0 +1,245 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: heroku-config
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tung Nguyen
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2019-11-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: aws-sdk-core
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: aws-sdk-iam
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: memoist
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rainbow
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: thor
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: zeitwerk
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: bundler
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: byebug
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: cli_markdown
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: rake
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: rspec
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ description:
182
+ email:
183
+ - tongueroo@gmail.com
184
+ executables:
185
+ - heroku-config
186
+ extensions: []
187
+ extra_rdoc_files: []
188
+ files:
189
+ - ".gitignore"
190
+ - ".rspec"
191
+ - CHANGELOG.md
192
+ - Gemfile
193
+ - Gemfile.lock
194
+ - Guardfile
195
+ - LICENSE.txt
196
+ - README.md
197
+ - Rakefile
198
+ - exe/heroku-config
199
+ - heroku-config.gemspec
200
+ - lib/heroku-config.rb
201
+ - lib/heroku_config.rb
202
+ - lib/heroku_config/autoloader.rb
203
+ - lib/heroku_config/aws_key.rb
204
+ - lib/heroku_config/aws_rotate.rb
205
+ - lib/heroku_config/aws_services.rb
206
+ - lib/heroku_config/base.rb
207
+ - lib/heroku_config/cli.rb
208
+ - lib/heroku_config/command.rb
209
+ - lib/heroku_config/completer.rb
210
+ - lib/heroku_config/completer/script.rb
211
+ - lib/heroku_config/completer/script.sh
212
+ - lib/heroku_config/config.rb
213
+ - lib/heroku_config/help.rb
214
+ - lib/heroku_config/help/aws_rotate.md
215
+ - lib/heroku_config/help/completion.md
216
+ - lib/heroku_config/help/completion_script.md
217
+ - lib/heroku_config/version.rb
218
+ - spec/lib/cli_spec.rb
219
+ - spec/spec_helper.rb
220
+ homepage: https://github.com/tongueroo/heroku-config
221
+ licenses:
222
+ - MIT
223
+ metadata: {}
224
+ post_install_message:
225
+ rdoc_options: []
226
+ require_paths:
227
+ - lib
228
+ required_ruby_version: !ruby/object:Gem::Requirement
229
+ requirements:
230
+ - - ">="
231
+ - !ruby/object:Gem::Version
232
+ version: '0'
233
+ required_rubygems_version: !ruby/object:Gem::Requirement
234
+ requirements:
235
+ - - ">="
236
+ - !ruby/object:Gem::Version
237
+ version: '0'
238
+ requirements: []
239
+ rubygems_version: 3.0.6
240
+ signing_key:
241
+ specification_version: 4
242
+ summary: Heroku Config AWS Access Key Rotator
243
+ test_files:
244
+ - spec/lib/cli_spec.rb
245
+ - spec/spec_helper.rb