drone-hunter 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: 656d8a52817c26e8a3dc5d956cd458b4c3c6ae8354523968bfdfba0acf3d6358
4
+ data.tar.gz: df60380cea37bcd2817000df50fd60050c1a3e0e8ba6e6675050a74f325007f9
5
+ SHA512:
6
+ metadata.gz: fd3f1e3932c15708c5afcff7405bfd534f7779c7e36d66f305566cfab96ce0820299a4edacb2069df8b8fd95ba6f0c63574c21ad63c8dbd8faa9d707c99ac2b8
7
+ data.tar.gz: 46525d26161abbbbd93401936ef6a3a38254d693761f2c26884b97baafcef57c5cc778377af8e184d1015ace32eeff8ab99d709a436e60baeac331048cf2809b
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+
2
+ The MIT License (MIT)
3
+ Copyright © 2022 Chris Olstrom <chris@olstrom.com>
4
+ Copyright © 2022 SUSE LLC
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the “Software”), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in
14
+ all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # drone-hunter - finds your dronefiles when you have too many repositories.
2
+
3
+ ## Overview
4
+
5
+ `drone-hunter` is a small utility that trawls whatever GitHub accounts you give it, looking for Drone CI configs.
6
+
7
+ ## Why does this exist?
8
+
9
+ Because I needed to search across hundreds of repositories across multiple GitHub organizations, searching for drone configs so I could review and compare them. I considered Terraform for this, but opted for Ruby instead, because caching makes iterative development much faster, and I didn't know how to make Terraform do that. Also the GitHub Provider for Terraform didn't handle full repository names for several data sources.
10
+
11
+ ## How do I obtain this majestic tool?
12
+
13
+ ```shell
14
+ gem install drone-hunter
15
+ ```
16
+
17
+ ## How do I use it?
18
+
19
+ ```shell
20
+ drone-hunter <owner ...>
21
+ ```
22
+
23
+ Where `owner` is either a user or an organization on GitHub.
24
+
25
+ `drone-hunter` will use the GitHub API to find all the repositories that are visible to you in the accounts you indicated. This means that if you're using private repositories, you'll need a `GITHUB_TOKEN` in the environment that has access to whatever repositories you want it to read from.
26
+
27
+ With this list of repositories, `drone-hunter` will find the default branches for everything, fetch an index of the tree for the commit at the tip of that branch, dig around for stuff that looks like it might be a drone config, and download all of them.
28
+
29
+ All of this will be cached.
30
+
31
+ The current version will output something like this (this may change in future releases):
32
+
33
+ ```json
34
+ [
35
+ {
36
+ "repository": "rancherlabs/drone-plugin-golangci-lint",
37
+ "path": ".drone.yml",
38
+ "sha": "d95f9416c3521bdcb4acc0146d9af9fbe42ff165",
39
+ "content": "---\nkind: pipeline\nname: golangci-lint\n\nsteps:\n- name: golangci-lint-run\n image: rancher/drone-golangci-lint:latest\n failure: ignore\n\n---\nkind: pipeline\nname: docker\n\nsteps:\n- name: publish\n image: plugins/docker\n settings:\n username:\n from_secret: docker_username\n password:\n from_secret: docker_password\n repo: rancher/drone-golangci-lint\n tags: latest\n when:\n instance:\n - drone-publish.rancher.io\n ref:\n include:\n - \"refs/heads/*\"\n - \"refs/tags/*\"\n - \"refs/pull/*\"\n event:\n - push\n - tag\n\n"
40
+ }
41
+ ]
42
+ ```
43
+
44
+ By default, it produces JSON output. You can change this with the `--output-format` option. Maybe you want YAML, since that's what drone configs are...
45
+
46
+ ```yaml
47
+ - repository: rancherlabs/drone-plugin-golangci-lint
48
+ path: ".drone.yml"
49
+ sha: d95f9416c3521bdcb4acc0146d9af9fbe42ff165
50
+ content: |+
51
+ ---kind: pipelinename: golangci-lintsteps:- name: golangci-lint-run image: rancher/drone-golangci-lint:latest failure: ignore---kind: pipelinename: dockersteps:- name: publish image: plugins/docker settings: username: from_secret: docker_username password: from_secret: docker_password repo: rancher/drone-golangci-lint tags: latest when: instance: - drone-publish.rancher.io ref: include: - "refs/heads/*" - "refs/tags/*" - "refs/pull/*" event: - push - tag
52
+ ```
53
+
54
+ Or maybe you want to dump a copy of every dronefile onto the filesystem. Set `--output-format=files` and you'll get something like this:
55
+
56
+ ```shell
57
+ find drone-hunter.output -type f
58
+ ```
59
+
60
+ ```text
61
+ drone-hunter.output/rancherlabs/drone-plugin-golangci-lint/.drone.yml
62
+ drone-hunter.output/rancherlabs/ssh-pub-keys/.drone.yml
63
+ drone-hunter.output/rancherlabs/drone-runner-laboratory/.drone_lab.yml
64
+ drone-hunter.output/rancherlabs/rio-website/.drone.yml
65
+ drone-hunter.output/rancherlabs/rancher-catalog-stats/.drone.yml
66
+ drone-hunter.output/rancherlabs/drone-plugin-fossa/.drone.yml
67
+ drone-hunter.output/rancherlabs/swiss-army-knife/.drone.ci.yml
68
+ drone-hunter.output/rancherlabs/swiss-army-knife/.drone.yml
69
+ drone-hunter.output/rancherlabs/huawei-ui/.drone.yml
70
+ drone-hunter.output/rancherlabs/drone-runner-docker/.drone.yml
71
+ drone-hunter.output/rancherlabs/k3s-website/.drone.yml
72
+ drone-hunter.output/rancherlabs/k3os-website/.drone.yml
73
+ drone-hunter.output/rancherlabs/website-theme/.drone.yml
74
+ drone-hunter.output/rancherlabs/support-tools/.drone.yml
75
+ ```
76
+
77
+ The only limits are your imagination (and the GitHub API Rate Limit).
78
+
79
+ ## License
80
+
81
+ `drone-hunter` is available under the [MIT License](https://tldrlegal.com/license/mit-license). See `LICENSE.txt` for the full text.
data/bin/drone-hunter ADDED
@@ -0,0 +1,151 @@
1
+ #! /usr/bin/env ruby
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ #########################
5
+ # Ruby Standard Library #
6
+ #########################
7
+
8
+ require "logger"
9
+ require "optparse"
10
+
11
+ #########################
12
+ # Internal Dependencies #
13
+ #########################
14
+
15
+ require_relative "../lib/drone_hunter"
16
+
17
+ def github_access_token_from_environment
18
+ # prefer a token designated for this program
19
+ ENV.fetch("DRONE_HUNTER_GITHUB_ACCESS_TOKEN") do
20
+ # but accept one that isn't designated for any particular use
21
+ ENV.fetch("GITHUB_ACCESS_TOKEN") do
22
+ # or accept one designated for the underlying library
23
+ ENV.fetch("OCTOKIT_ACCESS_TOKEN") do
24
+ # or play nice with Terraform's GitHub Provider
25
+ ENV.fetch("GITHUB_TOKEN") do
26
+ # well... a token isn't strictly REQUIRED...
27
+ nil
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ def output_format_from(input)
35
+ case input
36
+ when /j(son)?/i then :JSON
37
+ when /y(a?ml)?/i then :YAML
38
+ when /f(ile)?(s(ystem)?)?/i then :FileSystem
39
+ else raise NotImplementedError
40
+ end
41
+ end
42
+
43
+ def log_level_from(input)
44
+ case input
45
+ when /warn/i then Logger::WARN
46
+ when /info/i then Logger::INFO
47
+ when /debug/i then Logger::DEBUG
48
+ else raise NotImplementedError
49
+ end
50
+ end
51
+
52
+ #########################
53
+ # Default Configuration #
54
+ #########################
55
+
56
+ config = {
57
+ hacking: (ENV["HACKING"] ? true : false),
58
+ cache: {
59
+ dir: File.expand_path(ENV.fetch("DRONE_HUNTER_CACHE_DIR", './drone-hunter.cache'))
60
+ },
61
+ github: {
62
+ auto_paginate: true,
63
+ access_token: github_access_token_from_environment
64
+ },
65
+ log: {
66
+ level: log_level_from(ENV.fetch("DRONE_HUNTER_LOG_LEVEL", "info"))
67
+ },
68
+ output: {
69
+ format: output_format_from(ENV.fetch("DRONE_HUNTER_OUTPUT_FORMAT", "json")),
70
+ path: File.expand_path(ENV.fetch("DRONE_HUNTER_OUTPUT_PATH", "./drone-hunter.output")),
71
+ }
72
+ }
73
+
74
+ ################################
75
+ # Options from the Commandline #
76
+ ################################
77
+
78
+ OptionParser.new do |options|
79
+ options.on("-C", "--cache-dir=DIR", "env: DRONE_HUNTER_CACHE_DIR") { |argument| config[:cache][:dir] = File.expand_path(argument) }
80
+ options.on( "--github-access-token=TOKEN", "env: DRONE_HUNTER_GITHUB_ACCESS_TOKEN") { |argument| config[:github][:access_token] = argument }
81
+ options.on("-L", "--log-level=LEVEL", "env: DRONE_HUNTER_LOG_LEVEL") { |argument| config[:log][:level] = log_level_from(argument) }
82
+ options.on("-o", "--output-format=FORMAT", "env: DRONE_HUNTER_OUTPUT_FORMAT") { |argument| config[:output][:format] = output_format_from(argument) }
83
+ options.on("-p", "--output-path=PATH", "env: DRONE_HUNTER_OUTPUT_PATH") { |argument| config[:output][:path] = File.expand_path(argument) }
84
+ end.parse!
85
+
86
+ #################
87
+ # Sanity Checks #
88
+ #################
89
+
90
+ if ARGV.empty?
91
+ STDERR.puts "#{File.basename($PROGRAM_NAME)} <owner ...>"
92
+ exit Errno::EINVAL::Errno
93
+ end
94
+
95
+ #################
96
+ # Logging Setup #
97
+ #################
98
+
99
+ log = Logger::new(STDERR)
100
+ log.level = config[:log][:level]
101
+
102
+ ################
103
+ # GitHub Setup #
104
+ ################
105
+
106
+ # the GitHub API has a pretty low rate limit for unauthenticated calls,
107
+ # and we can exhaust that very quickly when doing what we need to do.
108
+ # Warn the user about this missing configuration as a courtesy.
109
+ unless config.dig(:github, :access_token)
110
+ log.warn("No GitHub Access Token provided. Expect rate limiting.")
111
+ end
112
+
113
+ github = Octokit::Client.new(**config[:github])
114
+
115
+ #################
116
+ # Caching Setup #
117
+ #################
118
+
119
+ cache = Moneta.new(:File, dir: config[:cache][:dir])
120
+
121
+ ################
122
+ # Main Program #
123
+ ################
124
+
125
+ hunt = DroneHunter.new(owners: ARGV, log: log, github: github, cache: cache)
126
+
127
+ if config[:hacking]
128
+ require "pry"
129
+ binding.pry
130
+ else
131
+ hunt.dronefiles.then do |dronefiles|
132
+ case config[:output][:format]
133
+ when :JSON
134
+ require "json"
135
+ STDOUT.puts JSON.pretty_generate dronefiles
136
+ when :YAML
137
+ require "yaml"
138
+ STDOUT.puts YAML.dump dronefiles
139
+ when :FileSystem
140
+ require "fileutils"
141
+ Dir.chdir(File.expand_path(*FileUtils.mkdir_p(config[:output][:path]))) do |output|
142
+ dronefiles.each do |file|
143
+ Dir.chdir(File.expand_path(*FileUtils.mkdir_p(File.join(output, file[:repository])))) do |repo|
144
+ File.write(File.join(".", file[:path]), file[:content])
145
+ end
146
+ end
147
+ end
148
+ else raise NotImplementedError
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,24 @@
1
+ Gem::Specification.new do |gem|
2
+ tag = `git describe --tags --abbrev=0`.chomp
3
+
4
+ gem.name = 'drone-hunter'
5
+ gem.homepage = 'https://github.com/colstrom/drone-hunter'
6
+ gem.summary = 'Hunts for Drone CI files across many repositories'
7
+
8
+ gem.version = "#{tag}"
9
+ gem.licenses = ['MIT']
10
+ gem.authors = ['Chris Olstrom']
11
+ gem.email = 'chris@olstrom.com'
12
+
13
+ # gem.cert_chain = ['trust/certificates/colstrom.pem']
14
+ # gem.signing_key = File.expand_path ENV.fetch 'GEM_SIGNING_KEY'
15
+
16
+ gem.files = `git ls-files -z`.split("\x0")
17
+ gem.test_files = `git ls-files -z -- {test,spec,features}/*`.split("\x0")
18
+ gem.executables = `git ls-files -z -- bin/*`.split("\x0").map { |f| File.basename(f) }
19
+
20
+ gem.require_paths = ['lib']
21
+
22
+ gem.add_runtime_dependency 'octokit', '~> 4.25', '>= 4.25.1'
23
+ gem.add_runtime_dependency 'moneta', '~> 1.5', '>= 1.5.1'
24
+ end
@@ -0,0 +1,107 @@
1
+ # SPDX-License-Identifier: MIT
2
+
3
+ #########################
4
+ # Ruby Standard Library #
5
+ #########################
6
+
7
+ require "base64"
8
+ require "logger"
9
+ require "set"
10
+
11
+ #########################
12
+ # External Dependencies #
13
+ #########################
14
+
15
+ require "moneta"
16
+ require "octokit"
17
+
18
+ class DroneHunter
19
+ def initialize(**options)
20
+ @log ||= options.fetch(:log) { Logger.new(STDERR) }
21
+ @github ||= options.fetch(:client) { Octokit::Client.new(auto_paginate: true) }
22
+ @cache ||= options.fetch(:cache) { Moneta.new(:File, dir: 'drone-hunter.cache') }
23
+ @owners ||= Set.new(options.fetch(:owners, []))
24
+ end
25
+
26
+ attr_reader :log
27
+ attr_reader :github
28
+ attr_reader :cache
29
+ attr_reader :owners
30
+
31
+ def cached(key, *rest, &block)
32
+ if cache.key?(key)
33
+ log.debug("(cache) #{key}")
34
+ cache.fetch(key)
35
+ else
36
+ log.info("(fetch) #{key}")
37
+ cache.store(key, block.call(*rest))
38
+ end
39
+ end
40
+
41
+ def repositories(owner = nil)
42
+ case owner
43
+ when String then cached("repositories/#{owner}") { github.repositories(owner) }
44
+ when nil then owners.flat_map { |owner| repositories(owner) }
45
+ else raise TypeError
46
+ end
47
+ end
48
+
49
+ def branches(repo = nil)
50
+ case repo
51
+ when String then cached("branches/#{repo}") { github.branches(repo) }
52
+ when Sawyer::Resource then branches(repo.full_name)
53
+ when nil then repositories.flat_map { |repo| branches(repo) }
54
+ else raise TypeError
55
+ end
56
+ end
57
+
58
+ def trees
59
+ repositories.map do |repo|
60
+ tree_sha = branches(repo.full_name).find do |branch|
61
+ branch.name == repo.default_branch
62
+ end&.commit&.sha
63
+
64
+ next unless tree_sha
65
+ repo = repo.full_name
66
+
67
+ {
68
+ repo => cached("tree/#{repo}/#{tree_sha}") { github.tree(repo, tree_sha) }.tree
69
+ }
70
+ end.compact.reduce(&:merge)
71
+ end
72
+
73
+ def blobs
74
+ trees.map do |repo, tree|
75
+ blobs = tree
76
+ .select { |entry| entry.path.match?(/ya?ml$/) }
77
+ .select { |entry| entry.path.match?(/drone/) }
78
+ .map do |entry|
79
+ {
80
+ entry.path => cached("blob/#{repo}/#{entry.sha}") { github.blob(repo, entry.sha) }
81
+ }
82
+ end
83
+ next if blobs.empty?
84
+
85
+ {
86
+ repo => blobs.reduce(&:merge)
87
+ }
88
+ end.compact.reduce(&:merge)
89
+ end
90
+
91
+ def dronefiles
92
+ blobs.flat_map do |repo, blobs|
93
+ blobs.map do |path, blob|
94
+ case blob[:encoding]
95
+ when "base64"
96
+ {
97
+ "repository" => repo,
98
+ "path" => path,
99
+ "sha" => blob[:sha],
100
+ "content" => Base64::decode64(blob[:content])
101
+ }
102
+ else raise EncodingError
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: drone-hunter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Chris Olstrom
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-08-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: octokit
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '4.25'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 4.25.1
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '4.25'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 4.25.1
33
+ - !ruby/object:Gem::Dependency
34
+ name: moneta
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.5'
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 1.5.1
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '1.5'
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 1.5.1
53
+ description:
54
+ email: chris@olstrom.com
55
+ executables:
56
+ - drone-hunter
57
+ extensions: []
58
+ extra_rdoc_files: []
59
+ files:
60
+ - Gemfile
61
+ - LICENSE.txt
62
+ - README.md
63
+ - bin/drone-hunter
64
+ - drone-hunter.gemspec
65
+ - lib/drone_hunter.rb
66
+ homepage: https://github.com/colstrom/drone-hunter
67
+ licenses:
68
+ - MIT
69
+ metadata: {}
70
+ post_install_message:
71
+ rdoc_options: []
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ requirements: []
85
+ rubygems_version: 3.0.3.1
86
+ signing_key:
87
+ specification_version: 4
88
+ summary: Hunts for Drone CI files across many repositories
89
+ test_files: []