release_manager 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.bash_profile +27 -0
  3. data/.dockerignore +1 -0
  4. data/.env +3 -0
  5. data/.gitignore +13 -0
  6. data/.rspec +2 -0
  7. data/CHANGELOG.md +55 -0
  8. data/Dockerfile +14 -0
  9. data/Gemfile +14 -0
  10. data/Gemfile.lock +72 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +311 -0
  13. data/Rakefile +6 -0
  14. data/app_startup_script.sh +4 -0
  15. data/bin/console +14 -0
  16. data/bin/setup +8 -0
  17. data/docker-compose.yml +37 -0
  18. data/exe/bump-changelog +5 -0
  19. data/exe/deploy-mod +5 -0
  20. data/exe/release-mod +5 -0
  21. data/exe/sandbox-create +13 -0
  22. data/lib/release_manager.rb +33 -0
  23. data/lib/release_manager/changelog.rb +130 -0
  24. data/lib/release_manager/cli/deploy_mod_cli.rb +44 -0
  25. data/lib/release_manager/cli/release_mod_cli.rb +43 -0
  26. data/lib/release_manager/cli/sandbox_create_cli.rb +138 -0
  27. data/lib/release_manager/control_mod.rb +83 -0
  28. data/lib/release_manager/control_repo.rb +35 -0
  29. data/lib/release_manager/errors.rb +13 -0
  30. data/lib/release_manager/git/credentials.rb +98 -0
  31. data/lib/release_manager/git/utilites.rb +263 -0
  32. data/lib/release_manager/logger.rb +52 -0
  33. data/lib/release_manager/module_deployer.rb +77 -0
  34. data/lib/release_manager/puppet_module.rb +211 -0
  35. data/lib/release_manager/puppetfile.rb +148 -0
  36. data/lib/release_manager/release.rb +174 -0
  37. data/lib/release_manager/sandbox.rb +272 -0
  38. data/lib/release_manager/vcs_manager.rb +22 -0
  39. data/lib/release_manager/vcs_manager/gitlab_adapter.rb +112 -0
  40. data/lib/release_manager/vcs_manager/vcs_adapter.rb +22 -0
  41. data/lib/release_manager/version.rb +3 -0
  42. data/lib/release_manager/workflow_action.rb +5 -0
  43. data/release_manager.gemspec +38 -0
  44. data/setup_repos.rb +95 -0
  45. metadata +175 -0
@@ -0,0 +1,83 @@
1
+ class ControlMod
2
+ attr_reader :name, :metadata, :repo
3
+ attr_accessor :version
4
+
5
+ def initialize(name, args)
6
+ @name = name
7
+ @metadata = args.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
8
+ end
9
+
10
+ def repo
11
+ git_url
12
+ end
13
+
14
+ def git_url
15
+ metadata[:git]
16
+ end
17
+
18
+ def branch
19
+ metadata[:branch]
20
+ end
21
+
22
+ def to_json(state = nil)
23
+ metadata.to_json(state)
24
+ end
25
+
26
+ def to_s
27
+ name_line = "mod '#{name}',"
28
+ data = metadata.map { |k, v| ":#{k} => '#{v}'" }.join(",\n\ ")
29
+ "#{name_line}\n #{data}"
30
+ end
31
+
32
+ def bump_patch_version
33
+ return unless metadata[:tag]
34
+ pieces = metadata[:tag].split('.')
35
+ raise "invalid semver structure #{metadata[:tag]}" if pieces.count != 3
36
+ pieces[2] = pieces[2].next
37
+ pin_version(pieces.join('.'))
38
+ end
39
+
40
+ def bump_minor_version
41
+ return unless metadata[:tag]
42
+ pieces = metadata[:tag].split('.')
43
+ raise "invalid semver structure #{metadata[:tag]}" if pieces.count != 3
44
+ pieces[2] = '0'
45
+ pieces[1] = pieces[1].next
46
+ pin_version(pieces.join('.'))
47
+ end
48
+
49
+ def bump_major_version
50
+ return unless metadata[:tag]
51
+ pieces = metadata[:tag].split('.')
52
+ raise "invalid semver structure #{metadata[:tag]}" if pieces.count != 3
53
+ pieces[2] = '0'
54
+ pieces[1] = '0'
55
+ pieces[0] = pieces[0].next
56
+ pin_version(pieces.join('.'))
57
+ end
58
+
59
+ def version
60
+ metadata[:tag]
61
+ end
62
+
63
+ def version=(v)
64
+ metadata[:tag] = v
65
+ end
66
+
67
+ def pin_version(v)
68
+ metadata.delete(:ref)
69
+ metadata.delete(:branch)
70
+ metadata[:tag] = v
71
+ end
72
+
73
+ def pin_branch(name)
74
+ metadata[:branch] = name
75
+ metadata.delete(:ref)
76
+ metadata.delete(:tag)
77
+ end
78
+
79
+ def pin_url(src)
80
+ metadata[:git] = src
81
+ end
82
+
83
+ end
@@ -0,0 +1,35 @@
1
+ require 'release_manager/puppetfile'
2
+ require 'rugged'
3
+ require 'release_manager/git/utilites'
4
+
5
+ class ControlRepo
6
+ attr_accessor :path, :repo, :url
7
+
8
+ include ReleaseManager::Git::Utilities
9
+ include ReleaseManager::Logger
10
+
11
+ def initialize(path, url = nil)
12
+ @path = path
13
+ @url = url
14
+ end
15
+
16
+ # @return [ControlRepo] - creates a new control repo object and clones the url unless already cloned
17
+ def self.create(path, url)
18
+ c = ControlRepo.new(path, url)
19
+ c.clone(url, path)
20
+ c
21
+ end
22
+
23
+ def repo
24
+ @repo ||= ::Rugged::Repository.new(path)
25
+ end
26
+
27
+ def puppetfile
28
+ unless @puppetfile
29
+ @puppetfile = Puppetfile.new(File.join(path, 'Puppetfile'))
30
+ @puppetfile.base_path = path
31
+ end
32
+ @puppetfile
33
+ end
34
+
35
+ end
@@ -0,0 +1,13 @@
1
+ class ModNotFoundException < Exception; end
2
+ class InvalidModuleNameException < Exception; end
3
+ class PuppetfileNotFoundException < Exception; end
4
+ class InvalidPuppetfileException < Exception; end
5
+ class InvalidMetadataSource < Exception; end
6
+ class NoUnreleasedLine < Exception; end
7
+ class NoChangeLogFile < Exception; end
8
+ class UpstreamSourceMatch < Exception; end
9
+ class GitError < Exception; end
10
+ class RepoNotFound < Exception; end
11
+ class InvalidModule < Exception; end
12
+ class InvalidToken < Exception; end
13
+ class InvalidSshkey < Exception; end
@@ -0,0 +1,98 @@
1
+ require 'rugged'
2
+ require 'release_manager/logger'
3
+ require 'io/console'
4
+ # Generate credentials for secured remote connections.
5
+ module ReleaseManager
6
+ module Git
7
+ class Credentials
8
+ include ReleaseManager::Logger
9
+
10
+ # @param repository [Rugged::BaseRepository]
11
+ def initialize(repository = nil)
12
+ @repository = repository
13
+ @called = 0
14
+ end
15
+
16
+ def needs_auth?(url)
17
+ url =~ /\Agit@/
18
+ end
19
+
20
+ def call(url, username_from_url = 'git', allowed_types = [:ssh_key])
21
+ @called += 1
22
+ # Break out of infinite HTTP auth retry loop introduced in libgit2/rugged 0.24.0, libssh
23
+ # auth seems to already abort after ~50 attempts.
24
+ if @called > 50
25
+ raise Exception.new("Authentication failed for Git remote %{url}.") % {url: url.inspect}
26
+ end
27
+ if allowed_types.include?(:ssh_key)
28
+ # should also check to see if process is still alive
29
+ begin
30
+ if ENV['SSH_AUTH_SOCK'] or (ENV['SSH_AGENT_PID'] and Process.getpgid( ENV['SSH_AGENT_PID'].to_i ))
31
+ ssh_agent_credentials
32
+ else
33
+ logger.warn("Could not find ssh-agent running, falling back to ssh key")
34
+ ssh_key_credentials
35
+ end
36
+ rescue Errno::ESRCH
37
+ ssh_key_credentials
38
+ end
39
+ else
40
+ default_credentials
41
+ end
42
+ end
43
+
44
+ def prompt_for_password
45
+ print "Enter password for #{global_private_key}: "
46
+ STDIN.noecho(&:gets).chomp
47
+ end
48
+
49
+ # this assumes the user has the private key in their home folder
50
+ # we should be smarter about getting this, maybe consulting ssh
51
+ # directory to find out which key is for the host if using an ssh config file
52
+ # additionally if the key is password protected how do we prompt for the password?
53
+ def global_private_key
54
+ unless @global_private_key
55
+ @global_private_key = ENV['SSH_PRIVATE_KEY'] || File.expand_path(File.join(ENV['HOME'], '.ssh', 'id_rsa'))
56
+ logger.info("Using ssh private key #{@global_private_key}")
57
+ end
58
+ @global_private_key
59
+ end
60
+
61
+ def global_public_key
62
+ unless @global_public_key
63
+ @global_public_key = ENV['SSH_PUBLIC_KEY'] || File.expand_path(File.join(ENV['HOME'], '.ssh', 'id_rsa.pub'))
64
+ logger.info("Using ssh public key #{@global_public_key}")
65
+ end
66
+ @global_public_key
67
+ end
68
+
69
+ # SSH_AGENT_SOCK must be set
70
+ def ssh_agent_credentials
71
+ Rugged::Credentials::SshKeyFromAgent.new(username: git_username)
72
+ end
73
+
74
+ # this method does currently now work
75
+ def ssh_key_credentials(url = nil)
76
+ logger.error("Must use ssh-agent, please run ssh-agent zsh, then ssh-add to load your ssh key")
77
+ exit 1
78
+ unless File.readable?(global_private_key)
79
+ raise Exception.new("Unable to use SSH key auth for %{url}: private key %{private_key} is missing or unreadable" % {url: url.inspect, private_key: global_private_key.inspect} )
80
+ end
81
+ Rugged::Credentials::SshKey.new(:username => git_username,
82
+ :privatekey => global_private_key,
83
+ :publickey => global_public_key,
84
+ :passphrase => prompt_for_password)
85
+ end
86
+
87
+ def default_credentials
88
+ Rugged::Credentials::Default.new
89
+ end
90
+
91
+ def git_username
92
+ 'git'
93
+ end
94
+
95
+ end
96
+ end
97
+ end
98
+
@@ -0,0 +1,263 @@
1
+ require 'release_manager/git/credentials'
2
+ require 'uri'
3
+
4
+ module ReleaseManager
5
+ module Git
6
+ module Utilities
7
+
8
+ def repo
9
+ @repo ||= Rugged::Repository.new(path)
10
+ end
11
+
12
+ # @param [String] remote_name - the name of the remote
13
+ def fetch(remote_name = 'upstream')
14
+ return unless remote_exists?(remote_name)
15
+ remote = repo.remotes[remote_name]
16
+ logger.info("Fetching remote #{remote_name} from #{remote.url}")
17
+ remote.fetch({
18
+ #progress: lambda { |output| puts output },
19
+ credentials: credentials.call(remote.url)
20
+ })
21
+ end
22
+
23
+ def transports
24
+ [:ssh, :https].each do |transport|
25
+ unless ::Rugged.features.include?(transport)
26
+ logger.warn("Rugged has been compiled without support for %{transport}; Git repositories will not be reachable via %{transport}. Try installing libssh-devel") % {transport: transport}
27
+ end
28
+ end
29
+ end
30
+
31
+ def credentials
32
+ @credentials ||= ReleaseManager::Git::Credentials.new(nil)
33
+ end
34
+
35
+ # @param [String] branch - the name of the branch you want checked out when cloning
36
+ # @param [String] url - the url to clone
37
+ # @return [Rugged::Repository] - the clond repository
38
+ # Clones the url
39
+ # if the clone path already exists, nothing is done
40
+ def clone(url, path)
41
+ if File.exists?(File.join(path, '.git'))
42
+ add_remote(url, 'upstream')
43
+ fetch('upstream')
44
+ repo
45
+ else
46
+ logger.info("Cloning repo with url: #{url} to #{path}")
47
+ r = Rugged::Repository.clone_at(url, path, {
48
+ #progress: lambda { |output| puts output },
49
+ credentials: credentials.call(url)
50
+ })
51
+ r
52
+ end
53
+ end
54
+
55
+ # @param [String] url - the url of the remote
56
+ # @param [String] remote_name - the name of the remote
57
+ # @param [Boolean] reset_url - set to true if you wish to reset the remote url
58
+ # @return [Rugged::Remote] a rugged remote object
59
+ def add_remote(url, remote_name = 'upstream', reset_url = false )
60
+ return unless git_url?(url)
61
+ if remote_exists?(remote_name)
62
+ # ensure the correct url is set
63
+ # this sets a non persistant fetch url
64
+ unless remote_url_matches?(remote_name, url)
65
+ if reset_url
66
+ logger.info("Resetting #{remote_name} remote to #{url} for #{path}")
67
+ repo.remotes.set_url(remote_name,url)
68
+ repo.remotes[remote_name]
69
+ end
70
+ end
71
+ else
72
+ logger.info("Adding #{remote_name} remote to #{url} for #{path}")
73
+ repo.remotes.create(remote_name, url)
74
+ end
75
+ end
76
+
77
+ # @param [String] name - the name of the remote
78
+ # @return [Boolean] - return true if the remote name and url are defined in the git repo
79
+ def remote_exists?(name)
80
+ repo.remotes[name]
81
+ end
82
+
83
+ # @param [String] name - the name of the remote
84
+ # @param [String] url - the url of the remote
85
+ # @return [Boolean] - true if the url matches a remote url already defined
86
+ def remote_url_matches?(name, url)
87
+ repo.remotes[name].url.eql?(url)
88
+ end
89
+
90
+ # @param [String] name - the name of the branch
91
+ # @return [Boolean] - true if the branch exist
92
+ def branch_exist?(name)
93
+ repo.branches.exist?(name)
94
+ end
95
+
96
+ # we should be creating the branch from upstream
97
+ # @return [Rugged::Branch]
98
+ def create_branch(name, target = 'upstream/master')
99
+ # fetch the remote if defined in the target
100
+ unless branch_exist?(name)
101
+ fetch(target.split('/').first) if target.include?('/')
102
+ logger.info("Creating branch: #{name} for #{path}")
103
+ repo.create_branch(name, target)
104
+ else
105
+ repo.branches[name]
106
+ end
107
+ end
108
+
109
+ # deletes the branch with the given name
110
+ # @param [String] name - the name of the branch to delete
111
+ def delete_branch(name)
112
+ repo.branches.delete(name)
113
+ end
114
+
115
+ # @param [String] remote_name - the remote name to push the branch to
116
+ def push_branch(remote_name, branch)
117
+ remote = find_or_create_remote(remote_name)
118
+ refs = [repo.branches[branch].canonical_name]
119
+ logger.info("Pushing branch #{branch} to remote #{remote.url}")
120
+ remote.push(refs, credentials: credentials)
121
+ end
122
+
123
+ # push all the tags to the remote
124
+ # @param [String] remote_name - the remote name to push tags to
125
+ def push_tags(remote_name)
126
+ remote = find_or_create_remote(remote_name)
127
+ refs = repo.tags.map(&:canonical_name)
128
+ logger.info("Pushing tags to remote #{remote.url}")
129
+ remote.push(refs, credentials: credentials)
130
+ end
131
+
132
+ # @return [String] the name of the current branch
133
+ def current_branch
134
+ repo.head.name.sub(/^refs\/heads\//, '')
135
+ end
136
+
137
+ def checkout_branch(name)
138
+ if current_branch != name
139
+ logger.info("Checking out branch: #{name} for #{path}")
140
+ repo.checkout(name)
141
+ else
142
+ # already checked out
143
+ logger.debug("Currently on branch #{name} for #{path}")
144
+ repo.branches[name]
145
+ end
146
+ end
147
+
148
+ # @param [String] remote_name - the remote name
149
+ # @return [Rugged::Remote] the remote object
150
+ # find the remote or create a new remote with the name as source
151
+ def find_or_create_remote(remote_name)
152
+ remote_from_name(remote_name) ||
153
+ remote_from_url(remote_name) ||
154
+ add_remote(remote_name, 'source', true)
155
+ end
156
+
157
+ # @param [String] name - the remote name to push the branch to
158
+ # @return [Rugged::Remote] the remote object if found
159
+ # Given the url find the remote with that url
160
+ def remote_from_name(name)
161
+ repo.remotes.find { |r| r.name.eql?(name) } unless git_url?(name)
162
+ end
163
+
164
+ # @param [String] url - the remote url to push the branch to
165
+ # @return [Rugged::Remote] the remote object if found
166
+ # Given the url find the remote with that url
167
+ def remote_from_url(url)
168
+ repo.remotes.find { |r| r.url.eql?(url) } if git_url?(url)
169
+ end
170
+
171
+ # @param [String] name - the remote name or url to check
172
+ # @return [MatchData] MatchData if the remote name is a url
173
+ # Is the name actually a url?
174
+ def git_url?(name)
175
+ /((git|ssh|http(s)?)|(git@[\w\.]+))(:(\/\/)?)([\w\.@\:\/\-~]+)(\.git)(\/)?/.match(name)
176
+ end
177
+
178
+ # @return [String] - the author name found in the config
179
+ def author_name
180
+ repo.config.get('user.name') || Rugged::Config.global.get('user.name')
181
+ end
182
+
183
+ # @return [String] - the author email found in the config
184
+ def author_email
185
+ repo.config.get('user.email') || Rugged::Config.global.get('user.email')
186
+ end
187
+
188
+ # @return [Hash] the author information used in a commit message
189
+ def author
190
+ {:email=>author_email, :time=>Time.now, :name=>author_name}
191
+ end
192
+
193
+ # # @param [String] file - the path to the file you want to add
194
+ # def add_file(file)
195
+ # return unless File.exists?(file)
196
+ # index = repo.index
197
+ # file.slice!(repo.workdir)
198
+ # index.add(:path => file, :oid => Rugged::Blob.from_workdir(repo, file), :mode => 0100644)
199
+ # index.write
200
+ # end
201
+
202
+ # @param [String] file - the path to the file you want to add
203
+ def add_file(file)
204
+ # TODO: change this to rugged implementation
205
+ `git --work-tree=#{path} --git-dir=#{repo.path} add #{file}`
206
+ end
207
+
208
+ # @param [String] file - the path to the file you want to remove
209
+ def remove_file(file)
210
+ index = repo.index
211
+ File.unlink(file)
212
+ index.remove(file)
213
+ index.write
214
+ end
215
+
216
+ # # @param [String] message - the message you want in the commit
217
+ # def create_commit(message)
218
+ # # get the index for this repository
219
+ # logger.info repo.status { |file, status_data| puts "#{file} has status: #{status_data.inspect}" }
220
+ #
221
+ # index = repo.index
222
+ # index.read_tree repo.head.target.tree unless repo.empty?
223
+ # require 'pry'; binding.pry
224
+ # #repo.lookup
225
+ # tree_new = index.write_tree repo
226
+ # oid = Rugged::Commit.create(repo,
227
+ # author: author,
228
+ # message: message,
229
+ # committer: author,
230
+ # parents: repo.empty? ? [] : [repo.head.target].compact,
231
+ # tree: tree_new,
232
+ # update_ref: 'HEAD')
233
+ # logger.info("Created commit #{oid} with #{message}")
234
+ # index.write
235
+ # #repo.status { |file, status_data| puts "#{file} has status: #{status_data.inspect}" }
236
+ # oid
237
+ # end
238
+
239
+ # @param [String] message - the message you want in the commit
240
+ # TODO: change this to rugged implementation
241
+ def create_commit(message)
242
+ output = `git --work-tree=#{path} --git-dir=#{repo.path} commit --message '#{message}' 2>&1`
243
+ if $?.success?
244
+ logger.info("Created commit #{message}")
245
+ else
246
+ logger.error output
247
+ end
248
+ end
249
+
250
+ # @return [String] the current branch name
251
+ def current_branch
252
+ repo.head.name.sub(/^refs\/heads\//, '')
253
+ end
254
+
255
+ def cherry_pick(commit)
256
+ return unless commit
257
+ repo.cherrypick(commit)
258
+ logger.info("Cherry picking commit with id: #{commit}")
259
+ end
260
+
261
+ end
262
+ end
263
+ end