release_manager 0.3.0

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