podjumper 0.1.0 → 0.1.1
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 +4 -4
- data/.ruby-version +1 -0
- data/README.md +16 -4
- data/bin/podj +31 -7
- data/lib/podjumper/command.rb +39 -0
- data/lib/podjumper/jump_info.rb +83 -0
- data/lib/podjumper/pod.rb +83 -0
- data/lib/podjumper/refinements.rb +35 -0
- data/lib/podjumper/version.rb +1 -1
- data/lib/podjumper.rb +5 -57
- data/podjumper.gemspec +5 -6
- metadata +36 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 20feea954a12a81227f2ef8bff7f62697a159525
|
4
|
+
data.tar.gz: 8cc529ef8186915511690c17c0b205ef95131bf3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4934b0b9d82315c32bfd67594f6c84caf1eda555c21f097dff57d1432576aac241fa8795d7df6b6868cf4e7c6b779bbeda6724dfc939b1bba2fc8e76a359e13d
|
7
|
+
data.tar.gz: ae7355ddff207b2c07a2192ea29ac3c0d72d90aadfc2be9682906f6ba8dca9031644bbc70f8fdf6606e4f894e21bfeeed3c262efe113a7f9b581c0d38fee4778
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-2.3.0
|
data/README.md
CHANGED
@@ -1,8 +1,12 @@
|
|
1
1
|
# Podjumper
|
2
2
|
|
3
|
-
|
3
|
+
Podjumper is a tiny command line tool for handling qa servers on a kubernetes cluster.
|
4
|
+
It makes a lof of assumptions of your kubernetes setup. For example, for shorthand commands which
|
5
|
+
are documented below it assumes that all pods have a label 'subdomain' and that there is only
|
6
|
+
one pod for each unique subdomain label.
|
4
7
|
|
5
|
-
|
8
|
+
This is how podjumper is currently intended to work. More tooling is in development.
|
9
|
+
Some basic examples are below.
|
6
10
|
|
7
11
|
## Installation
|
8
12
|
|
@@ -22,7 +26,15 @@ Or install it yourself as:
|
|
22
26
|
|
23
27
|
## Usage
|
24
28
|
|
25
|
-
|
29
|
+
Tailing logs of a QA server
|
30
|
+
```ruby
|
31
|
+
$ podj logs qa4
|
32
|
+
```
|
33
|
+
|
34
|
+
List all rake commands remotely
|
35
|
+
```ruby
|
36
|
+
$ podj exec qa4 'bundle exec rake tasks -T'
|
37
|
+
```
|
26
38
|
|
27
39
|
## Development
|
28
40
|
|
@@ -32,7 +44,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
32
44
|
|
33
45
|
## Contributing
|
34
46
|
|
35
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
47
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/johndavidmartinez/podjumper. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
36
48
|
|
37
49
|
## License
|
38
50
|
|
data/bin/podj
CHANGED
@@ -1,13 +1,27 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
3
|
require 'gli'
|
4
|
+
require 'tty-command'
|
4
5
|
require 'podjumper'
|
5
6
|
|
6
7
|
include Podjumper
|
7
8
|
include GLI::App
|
8
9
|
|
10
|
+
using Refinements
|
11
|
+
|
9
12
|
version Podjumper::VERSION
|
10
13
|
|
14
|
+
desc 'Navigate to a container by picking through namespaces and subdomains.'
|
15
|
+
command %i[navigate nav] do |cmd|
|
16
|
+
cmd.action do |global_options, options, args|
|
17
|
+
info = JumpInfo.load(args)
|
18
|
+
puts "Namespace: #{info.namespace}",
|
19
|
+
"Subdomain: #{info.subdomain}",
|
20
|
+
"Pod Name: #{info.pod_name}",
|
21
|
+
"Container: #{info.container_name}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
11
25
|
desc 'List things'
|
12
26
|
command [:list, :ls] do |l|
|
13
27
|
l.desc 'List all avaliable namespaces'
|
@@ -28,19 +42,29 @@ command [:list, :ls] do |l|
|
|
28
42
|
l.desc 'List all containers in a namespace\'s pod'
|
29
43
|
l.command [:containers, :c] do |c|
|
30
44
|
c.action do |global_options, options, args|
|
31
|
-
|
32
|
-
subdomain = args[1]
|
45
|
+
info = JumpInfo.load_upto_pod(args)
|
33
46
|
filter = /#{args[2]}/
|
34
|
-
puts
|
47
|
+
puts info.pod.container_names.select_regex(filter)
|
35
48
|
end
|
36
49
|
end
|
37
50
|
end
|
38
51
|
|
39
|
-
desc '
|
40
|
-
command [:
|
41
|
-
j.action do |
|
52
|
+
desc 'Shorthand command runs given command on nearest pod with subdomain label'
|
53
|
+
command [:exec] do |j|
|
54
|
+
j.action do |_, _, args|
|
55
|
+
subdomain = args[0]
|
56
|
+
command = args[1...args.length].join(' ')
|
57
|
+
info = JumpInfo.load_rails_from_subdomain(subdomain)
|
58
|
+
Command.new(info).exec(command)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
desc 'Shorthand command tails logs on nearest pod with subdomain label'
|
63
|
+
command [:logs] do |l|
|
64
|
+
l.action do |_, _, args|
|
42
65
|
subdomain = args[0]
|
43
|
-
|
66
|
+
info = JumpInfo.load_rails_from_subdomain(subdomain)
|
67
|
+
Command.new(info).logs
|
44
68
|
end
|
45
69
|
end
|
46
70
|
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Podjumper
|
2
|
+
class Command
|
3
|
+
def initialize(info)
|
4
|
+
@namespace = info.namespace
|
5
|
+
@pod = info.pod_name
|
6
|
+
@container = info.container_name
|
7
|
+
end
|
8
|
+
|
9
|
+
def exec(command)
|
10
|
+
tty = TTY::Command.new
|
11
|
+
tty.run(exec_command(command))
|
12
|
+
end
|
13
|
+
|
14
|
+
def logs
|
15
|
+
tty = TTY::Command.new(printer: :null)
|
16
|
+
tty.run(logs_command) do |out, _|
|
17
|
+
puts out if !ping?(out)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def exec_command(command)
|
24
|
+
"kubectl exec -ti #{@pod} -c #{@container} --namespace #{@namespace} -- #{command}"
|
25
|
+
end
|
26
|
+
|
27
|
+
def logs_command
|
28
|
+
"kubectl logs #{@pod} #{@container} --namespace #{@namespace} -f --since=1s"
|
29
|
+
end
|
30
|
+
|
31
|
+
def ping?(log_line)
|
32
|
+
return ping_matchers.any? { |regex| log_line =~ regex }
|
33
|
+
end
|
34
|
+
|
35
|
+
def ping_matchers
|
36
|
+
[/api\/ping/, /Api::SystemController#ping/, /Completed 200 OK/]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
using Refinements
|
2
|
+
|
3
|
+
module Podjumper
|
4
|
+
JumpInfo = Struct.new(:namespace, :pod, :container_name) do
|
5
|
+
def subdomain
|
6
|
+
pod&.subdomain
|
7
|
+
end
|
8
|
+
|
9
|
+
def pod_name
|
10
|
+
pod&.name
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class << JumpInfo
|
15
|
+
def load(args)
|
16
|
+
args = args.dup
|
17
|
+
namespace = disambiguate_namespace(args.shift)
|
18
|
+
pod = disambiguate_pod(namespace, args.shift)
|
19
|
+
container = disambiguate_container(pod, args.shift)
|
20
|
+
|
21
|
+
new(namespace, pod, container)
|
22
|
+
end
|
23
|
+
|
24
|
+
def load_upto_pod(args)
|
25
|
+
args = args.dup
|
26
|
+
namespace = disambiguate_namespace(args.shift)
|
27
|
+
pod = disambiguate_pod(namespace, args.shift)
|
28
|
+
|
29
|
+
new(namespace, pod, nil)
|
30
|
+
end
|
31
|
+
|
32
|
+
def load_upto_namespace(args)
|
33
|
+
args = args.dup
|
34
|
+
namespace = disambiguate_namespace(args.shift)
|
35
|
+
|
36
|
+
new(namespace, nil, nil)
|
37
|
+
end
|
38
|
+
|
39
|
+
def load_rails_from_subdomain(subdomain)
|
40
|
+
pod = Pod.find_by_subdomain(subdomain)
|
41
|
+
new(pod.namespace, pod, pod.container('rails'))
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def disambiguate_namespace(query)
|
47
|
+
disambiguate(namespaces, query: query, title: 'Namespaces')
|
48
|
+
end
|
49
|
+
|
50
|
+
def disambiguate_pod(namespace, query)
|
51
|
+
all_pods = Pod.where(namespace: namespace)
|
52
|
+
subdomains = all_pods.map(&:subdomain).uniq
|
53
|
+
subdomain = disambiguate(subdomains, query: query, title: "Subdomains in namespace #{namespace.inspect}")
|
54
|
+
pods = all_pods.select { |pod| pod.subdomain == subdomain }
|
55
|
+
|
56
|
+
disambiguate(pods, title: "Pods in #{namespace} with subdomain #{subdomain.inspect}", &:name)
|
57
|
+
end
|
58
|
+
|
59
|
+
def disambiguate_container(pod, query)
|
60
|
+
disambiguate(pod.container_names, query: query, title: "Containers in pod #{pod.name.inspect}")
|
61
|
+
end
|
62
|
+
|
63
|
+
def disambiguate(choices, query: nil, title: nil, &choice_name)
|
64
|
+
choice_name ||= :itself
|
65
|
+
matches = matching_choices(choices, query, &choice_name)
|
66
|
+
if matches.count == 1
|
67
|
+
matches.first
|
68
|
+
else
|
69
|
+
puts title if title
|
70
|
+
cli.ask_for_choice(matches, &choice_name)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def matching_choices(choices, query)
|
75
|
+
return choices if query.nil?
|
76
|
+
choices.select { |c| yield(c) =~ /#{query}/ }
|
77
|
+
end
|
78
|
+
|
79
|
+
def cli
|
80
|
+
@cli ||= HighLine.new
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module Podjumper
|
2
|
+
class Pod
|
3
|
+
def initialize(description)
|
4
|
+
@description = description
|
5
|
+
validate_resource!
|
6
|
+
validate_version!
|
7
|
+
end
|
8
|
+
|
9
|
+
def namespace
|
10
|
+
metadata['namespace']
|
11
|
+
end
|
12
|
+
|
13
|
+
def subdomain
|
14
|
+
labels['subdomain']
|
15
|
+
end
|
16
|
+
|
17
|
+
def name
|
18
|
+
metadata['name']
|
19
|
+
end
|
20
|
+
|
21
|
+
def labels
|
22
|
+
metadata['labels'] || {}
|
23
|
+
end
|
24
|
+
|
25
|
+
def container(name)
|
26
|
+
container_names.find { |container_name| container_name =~ /#{name}/ }
|
27
|
+
end
|
28
|
+
|
29
|
+
def container_names
|
30
|
+
containers.map { |container| container['name'] }
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_h
|
34
|
+
JSON.parse(to_json)
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_json
|
38
|
+
description.to_json
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
attr_reader :description
|
44
|
+
|
45
|
+
def kind
|
46
|
+
description['kind']
|
47
|
+
end
|
48
|
+
|
49
|
+
def api_version
|
50
|
+
description['apiVersion']
|
51
|
+
end
|
52
|
+
|
53
|
+
def metadata
|
54
|
+
description['metadata']
|
55
|
+
end
|
56
|
+
|
57
|
+
def containers
|
58
|
+
description['spec']['containers']
|
59
|
+
end
|
60
|
+
|
61
|
+
def validate_resource!
|
62
|
+
raise "Expected a pod, got #{kind.inspect}" if kind != 'Pod'
|
63
|
+
end
|
64
|
+
|
65
|
+
def validate_version!
|
66
|
+
raise "Invalid API version #{api_version.inspect}" if api_version != 'v1'
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class << Pod
|
71
|
+
def where(namespace: nil, subdomain: nil)
|
72
|
+
namespace_flag = namespace ? "-n '#{namespace}'" : '--all-namespaces'
|
73
|
+
subdomain_flag = subdomain ? "-l 'subdomain=#{subdomain}'" : ''
|
74
|
+
json = JSON.parse(`kubectl get pods --output=json #{namespace_flag} #{subdomain_flag}`)
|
75
|
+
|
76
|
+
json['items'].map(&Pod.method(:new))
|
77
|
+
end
|
78
|
+
|
79
|
+
def find_by_subdomain(subdomain)
|
80
|
+
new(JSON.parse(`kubectl get pods --output=json --all-namespaces --selector='subdomain=#{subdomain}'`)['items'].first)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -14,4 +14,39 @@ module Refinements
|
|
14
14
|
end
|
15
15
|
end
|
16
16
|
end
|
17
|
+
|
18
|
+
refine HighLine do
|
19
|
+
def ask_for_choice(choices)
|
20
|
+
padding_max = choices.count.to_s.length
|
21
|
+
puts choices.each_with_index.map { |choice, index|
|
22
|
+
index += 1
|
23
|
+
choice = yield choice if block_given?
|
24
|
+
padding = ' ' * (padding_max - index.to_s.length)
|
25
|
+
"#{index}. #{padding}#{choice}"
|
26
|
+
}
|
27
|
+
|
28
|
+
index = ask_for_index(choices.count)
|
29
|
+
return if index.nil?
|
30
|
+
|
31
|
+
choices[index]
|
32
|
+
end
|
33
|
+
|
34
|
+
def ask_for_index(count)
|
35
|
+
answer = ask("Choose from 1..#{count}, or (q)uit...") do |q|
|
36
|
+
q.case = :downcase
|
37
|
+
q.validate = /\A\d+|q\Z/
|
38
|
+
end
|
39
|
+
exit 0 if answer == 'q' # TODO: Safe exit
|
40
|
+
index = Integer(answer)
|
41
|
+
if index < 1
|
42
|
+
puts 'The first choice is 1'
|
43
|
+
ask_for_index(count)
|
44
|
+
elsif index > count
|
45
|
+
puts "The last choice is #{count}"
|
46
|
+
ask_for_index(count)
|
47
|
+
else
|
48
|
+
index - 1
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
17
52
|
end
|
data/lib/podjumper/version.rb
CHANGED
data/lib/podjumper.rb
CHANGED
@@ -1,63 +1,11 @@
|
|
1
|
+
require 'highline'
|
2
|
+
require 'json'
|
1
3
|
require 'podjumper/version'
|
2
4
|
require 'podjumper/refinements'
|
3
|
-
require '
|
5
|
+
require 'podjumper/pod'
|
6
|
+
require 'podjumper/jump_info'
|
7
|
+
require 'podjumper/command'
|
4
8
|
|
5
9
|
module Podjumper
|
6
10
|
using Refinements
|
7
|
-
|
8
|
-
def namespaces
|
9
|
-
`kubectl get namespaces --output=name`
|
10
|
-
.split("\n")
|
11
|
-
.map { |str| str.drop_prefix('namespaces/') }
|
12
|
-
end
|
13
|
-
|
14
|
-
def subdomains(namespace = nil)
|
15
|
-
description = describe_pods(namespace: namespace)
|
16
|
-
description.map do |pod|
|
17
|
-
subdomain = get_subdomain(pod)
|
18
|
-
next unless subdomain
|
19
|
-
namespace = get_namespace(pod)
|
20
|
-
[namespace, subdomain].join(' ')
|
21
|
-
end.compact
|
22
|
-
end
|
23
|
-
|
24
|
-
def containers(namespace, subdomain)
|
25
|
-
description = describe_pods(namespace: namespace, subdomain: subdomain)
|
26
|
-
description.flat_map do |pod|
|
27
|
-
subdomain = get_subdomain(pod)
|
28
|
-
next unless subdomain
|
29
|
-
namespace = get_namespace(pod)
|
30
|
-
container_names(pod).map do |container_name|
|
31
|
-
[namespace, subdomain, container_name].join(' ')
|
32
|
-
end
|
33
|
-
end.compact
|
34
|
-
end
|
35
|
-
|
36
|
-
def describe_pods(namespace: nil, subdomain: nil)
|
37
|
-
namespace_flag = namespace ? "--namespace='#{namespace}'" : '--all-namespaces'
|
38
|
-
subdomain_flag = subdomain ? "--selector='subdomain=#{subdomain}'" : ''
|
39
|
-
JSON.parse(`kubectl get pods --output=json #{namespace_flag} #{subdomain_flag}`)['items']
|
40
|
-
end
|
41
|
-
|
42
|
-
def get_subdomain(pod)
|
43
|
-
return unless pod['metadata'].key?('labels')
|
44
|
-
pod['metadata']['labels']['subdomain']
|
45
|
-
end
|
46
|
-
|
47
|
-
def get_namespace(pod)
|
48
|
-
pod['metadata']['namespace']
|
49
|
-
end
|
50
|
-
|
51
|
-
def container_names(pod)
|
52
|
-
pod['spec']['containers'].map { |container| container['name'] }
|
53
|
-
end
|
54
|
-
|
55
|
-
def tty_command(subdomain)
|
56
|
-
pods = describe_pods
|
57
|
-
pod = pods.select! { |pod| pod&.[]('metadata')&.[]('labels')&.[]('subdomain') == subdomain }.first
|
58
|
-
pod_name = pod['metadata']['name']
|
59
|
-
container = container_names(pod).select_regex(/rails/).first
|
60
|
-
namespace = get_namespace(pod)
|
61
|
-
"kubectl exec -ti #{pod_name} -c #{container} --namespace #{namespace} -- bash"
|
62
|
-
end
|
63
11
|
end
|
data/podjumper.gemspec
CHANGED
@@ -26,8 +26,8 @@ Gem::Specification.new do |spec|
|
|
26
26
|
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
27
27
|
f.match(%r{^(test|spec|features)/})
|
28
28
|
end
|
29
|
-
spec.bindir = "
|
30
|
-
spec.executables =
|
29
|
+
spec.bindir = "bin"
|
30
|
+
spec.executables = ["podj"]
|
31
31
|
spec.require_paths = ["lib"]
|
32
32
|
|
33
33
|
spec.add_development_dependency "bundler", "~> 1.15"
|
@@ -35,8 +35,7 @@ Gem::Specification.new do |spec|
|
|
35
35
|
spec.add_development_dependency "rspec", "~> 3.0"
|
36
36
|
spec.add_development_dependency "byebug"
|
37
37
|
|
38
|
-
spec.add_dependency "gli", "~> 2.17
|
39
|
-
|
40
|
-
spec.
|
41
|
-
spec.executables << 'podj'
|
38
|
+
spec.add_dependency "gli", "~> 2.17"
|
39
|
+
spec.add_dependency "highline", ">= 1.7"
|
40
|
+
spec.add_dependency "tty-command"
|
42
41
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: podjumper
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- johndavidmartinez
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-01-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -72,14 +72,42 @@ dependencies:
|
|
72
72
|
requirements:
|
73
73
|
- - "~>"
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version: 2.17
|
75
|
+
version: '2.17'
|
76
76
|
type: :runtime
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version: 2.17
|
82
|
+
version: '2.17'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: highline
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '1.7'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '1.7'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: tty-command
|
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'
|
83
111
|
description:
|
84
112
|
email:
|
85
113
|
- johndavidmartinez1@gmail.com
|
@@ -90,6 +118,7 @@ extra_rdoc_files: []
|
|
90
118
|
files:
|
91
119
|
- ".gitignore"
|
92
120
|
- ".rspec"
|
121
|
+
- ".ruby-version"
|
93
122
|
- ".travis.yml"
|
94
123
|
- CODE_OF_CONDUCT.md
|
95
124
|
- Gemfile
|
@@ -102,6 +131,9 @@ files:
|
|
102
131
|
- bin/setup
|
103
132
|
- cmd
|
104
133
|
- lib/podjumper.rb
|
134
|
+
- lib/podjumper/command.rb
|
135
|
+
- lib/podjumper/jump_info.rb
|
136
|
+
- lib/podjumper/pod.rb
|
105
137
|
- lib/podjumper/refinements.rb
|
106
138
|
- lib/podjumper/version.rb
|
107
139
|
- podjumper.gemspec
|