cyclid 0.2.1 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+ # Copyright 2016 Liqwyd Ltd.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ require_relative 'oauth'
17
+
18
+ # Top level module for the core Cyclid code.
19
+ module Cyclid
20
+ # Module for the Cyclid API
21
+ module API
22
+ # Module for Cyclid Plugins
23
+ module Plugins
24
+ # Container for the Sinatra related controllers modules
25
+ module ApiExtension
26
+ # Github plugin method callbacks
27
+ module GithubMethods
28
+ # Plugin configuration methods
29
+ module Config
30
+ # Load the config for the Github plugin and set defaults if they're not
31
+ # in the config
32
+ def load_github_config(config)
33
+ config.symbolize_keys!
34
+
35
+ server_config = config[:github] || {}
36
+ Cyclid.logger.debug "config=#{server_config}"
37
+
38
+ server_config.symbolize_keys
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+ # Copyright 2016 Liqwyd Ltd.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ # Top level module for the core Cyclid code.
17
+ module Cyclid
18
+ # Module for the Cyclid API
19
+ module API
20
+ # Module for Cyclid Plugins
21
+ module Plugins
22
+ # Container for the Sinatra related controllers modules
23
+ module ApiExtension
24
+ # Github plugin method callbacks
25
+ module GithubMethods
26
+ # Github event handler helper methods
27
+ module Helpers
28
+ def pull_request
29
+ @pr ||= @payload['pull_request']
30
+ end
31
+
32
+ def pr_clone_url
33
+ pull_request['base']['repo']['html_url']
34
+ end
35
+
36
+ def pr_head
37
+ @pr_head ||= pull_request['head']
38
+ end
39
+
40
+ def pr_sha
41
+ pr_head['sha']
42
+ end
43
+
44
+ def pr_ref
45
+ pr_head['ref']
46
+ end
47
+
48
+ def pr_repo
49
+ @pr_repo ||= pr_head['repo']
50
+ end
51
+
52
+ def pr_status_url
53
+ url = pr_repo['statuses_url']
54
+ @pr_status_url ||= url.gsub('{sha}', pr_sha)
55
+ end
56
+
57
+ def pr_trees_url
58
+ url = pr_repo['trees_url']
59
+ @pr_trees_url ||= url.gsub('{/sha}', "/#{pr_sha}")
60
+ end
61
+
62
+ def pr_repository
63
+ @repo ||= Octokit::Repository.from_url(pr_clone_url)
64
+ end
65
+
66
+ def push_head_commit
67
+ @head_commit ||= @payload['head_commit']
68
+ end
69
+
70
+ def push_ref
71
+ @payload['ref']
72
+ end
73
+
74
+ def push_sha
75
+ @push_sha ||= push_head_commit['id']
76
+ end
77
+
78
+ def push_clone_url
79
+ @push_clone_url ||= @payload['repository']['html_url']
80
+ end
81
+
82
+ def push_repository
83
+ @push_repo ||= Octokit::Repository.from_url(push_clone_url)
84
+ end
85
+
86
+ def find_oauth_token(config, clone_url)
87
+ # Get an OAuth token, if one is set for this repo
88
+ Cyclid.logger.debug "attempting to find auth token for #{clone_url}"
89
+ auth_token = nil
90
+ config['repository_tokens'].each do |entry|
91
+ entry_url = URI(entry['url'])
92
+ auth_token = entry['token'] if entry_url.host == clone_url.host && \
93
+ entry_url.path == clone_url.path
94
+ end
95
+ # If we didn't find a token specifically for this repository, use
96
+ # the organization OAuth token
97
+ auth_token = config['oauth_token'] if auth_token.nil?
98
+
99
+ return auth_token
100
+ end
101
+
102
+ def find_job_file(tree)
103
+ # See if a .cyclid.yml or .cyclid.json file exists in the project
104
+ # root
105
+ sha = nil
106
+ type = nil
107
+ tree['tree'].each do |file|
108
+ match = file['path'].match(/\A\.cyclid\.(json|yml)\z/)
109
+ next unless match
110
+
111
+ sha = file['sha']
112
+ type = match[1]
113
+ break
114
+ end
115
+ [sha, type]
116
+ end
117
+
118
+ def load_job_file(repo, sha, type)
119
+ blob = @client.blob(repo, sha)
120
+ case type
121
+ when 'json'
122
+ Oj.load(Base64.decode64(blob.content))
123
+ when 'yml'
124
+ YAML.load(Base64.decode64(blob.content))
125
+ end
126
+ end
127
+
128
+ # Generate a "state" key that can be passed to the OAuth endpoints
129
+ def oauth_state
130
+ org = retrieve_organization
131
+ state = "#{org.name}:#{org.salt}:#{org.owner_email}"
132
+ Base64.urlsafe_encode64(Digest::SHA256.digest(state))
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -13,6 +13,12 @@
13
13
  # See the License for the specific language governing permissions and
14
14
  # limitations under the License.
15
15
 
16
+ require_relative 'helpers'
17
+ require_relative 'config'
18
+ require_relative 'oauth'
19
+ require_relative 'pull_request'
20
+ require_relative 'push'
21
+
16
22
  # Top level module for the core Cyclid code.
17
23
  module Cyclid
18
24
  # Module for the Cyclid API
@@ -25,6 +31,12 @@ module Cyclid
25
31
  module GithubMethods
26
32
  include Methods
27
33
 
34
+ include Helpers
35
+ include Config
36
+ include OAuth
37
+ include PullRequest
38
+ include Push
39
+
28
40
  # Return a reference to the plugin that is associated with this
29
41
  # controller; used by the lower level code.
30
42
  def controller_plugin
@@ -32,7 +44,7 @@ module Cyclid
32
44
  end
33
45
 
34
46
  # HTTP POST callback
35
- def post(data, headers, config)
47
+ def post(headers, config)
36
48
  return_failure(400, 'no event specified') \
37
49
  unless headers.include? 'X-Github-Event'
38
50
 
@@ -47,7 +59,9 @@ module Cyclid
47
59
 
48
60
  case event
49
61
  when 'pull_request'
50
- result = gh_pull_request(data, config)
62
+ result = event_pull_request(config)
63
+ when 'push'
64
+ result = event_push(config)
51
65
  when 'ping'
52
66
  result = true
53
67
  when 'status'
@@ -58,142 +72,6 @@ module Cyclid
58
72
 
59
73
  return result
60
74
  end
61
-
62
- # Handle a Github Pull Request event
63
- def gh_pull_request(data, config)
64
- action = data['action'] || nil
65
- pr = data['pull_request'] || nil
66
-
67
- Cyclid.logger.debug "action=#{action}"
68
- return true unless action == 'opened' \
69
- or action == 'reopened' \
70
- or action == 'synchronize'
71
-
72
- # Get the list of files in the root of the repository in the
73
- # Pull Request branch
74
- html_url = URI(pr['base']['repo']['html_url'])
75
- pr_sha = pr['head']['sha']
76
- ref = pr['head']['ref']
77
-
78
- Cyclid.logger.debug "sha=#{pr_sha} ref=#{ref}"
79
-
80
- # Get some useful endpoints & interpolate the SHA for this PR
81
- url = pr['head']['repo']['statuses_url']
82
- statuses = url.gsub('{sha}', pr_sha)
83
-
84
- url = pr['head']['repo']['trees_url']
85
- trees = url.gsub('{/sha}', "/#{pr_sha}")
86
-
87
- # Get an OAuth token, if one is set for this repo
88
- Cyclid.logger.debug "attempting to find auth token for #{html_url}"
89
- auth_token = nil
90
- config['repository_tokens'].each do |entry|
91
- entry_url = URI(entry['url'])
92
- auth_token = entry['token'] if entry_url.host == html_url.host && \
93
- entry_url.path == html_url.path
94
- end
95
-
96
- # XXX We probably don't want to be logging auth tokens in plain text
97
- Cyclid.logger.debug "auth token=#{auth_token}"
98
-
99
- # Set the PR to 'pending'
100
- GithubStatus.set_status(statuses, auth_token, 'pending', 'Preparing build')
101
-
102
- # Get the Pull Request
103
- begin
104
- trees_url = URI(trees)
105
- Cyclid.logger.debug "Getting root for #{trees_url}"
106
-
107
- request = Net::HTTP::Get.new(trees_url)
108
- request.add_field('Authorization', "token #{auth_token}") \
109
- unless auth_token.nil?
110
-
111
- http = Net::HTTP.new(trees_url.hostname, trees_url.port)
112
- http.use_ssl = (trees_url.scheme == 'https')
113
- response = http.request(request)
114
-
115
- Cyclid.logger.debug response.inspect
116
- raise "couldn't get repository root" \
117
- unless response.code == '200'
118
-
119
- root = Oj.load response.body
120
- rescue StandardError => ex
121
- Cyclid.logger.error "failed to retrieve Pull Request root: #{ex}"
122
- return_failure(500, 'could not retrieve Pull Request root')
123
- end
124
-
125
- # See if a .cyclid.yml or .cyclid.json file exists in the project
126
- # root
127
- job_url = nil
128
- job_type = nil
129
- root['tree'].each do |file|
130
- match = file['path'].match(/\A\.cyclid\.(json|yml)\z/)
131
- next unless match
132
-
133
- job_url = URI(file['url'])
134
- job_type = match[1]
135
- end
136
-
137
- Cyclid.logger.debug "job_url=#{job_url}"
138
-
139
- if job_url.nil?
140
- GithubStatus.set_status(statuses, auth_token, 'error', 'No Cyclid job file found')
141
- return_failure(400, 'not a Cyclid repository')
142
- end
143
-
144
- # Pull down the job file
145
- begin
146
- Cyclid.logger.info "Retrieving PR job from #{job_url}"
147
-
148
- request = Net::HTTP::Get.new(job_url)
149
- request.add_field('Authorization', "token #{auth_token}") \
150
- unless auth_token.nil?
151
-
152
- http = Net::HTTP.new(job_url.hostname, job_url.port)
153
- http.use_ssl = (job_url.scheme == 'https')
154
- response = http.request(request)
155
- raise "couldn't get Cyclid job" unless response.code == '200'
156
-
157
- job_blob = Oj.load response.body
158
- case job_type
159
- when 'json'
160
- job_definition = Oj.load(Base64.decode64(job_blob['content']))
161
- when 'yml'
162
- job_definition = YAML.load(Base64.decode64(job_blob['content']))
163
- end
164
-
165
- # Insert this repository & branch into the sources
166
- #
167
- # XXX Could this cause collisions between the existing sources in
168
- # the job definition? Not entirely sure what the workflow will
169
- # look like.
170
- job_sources = job_definition['sources'] || []
171
- job_sources << { 'type' => 'git',
172
- 'url' => html_url.to_s,
173
- 'branch' => ref,
174
- 'token' => auth_token }
175
- job_definition['sources'] = job_sources
176
-
177
- Cyclid.logger.debug "sources=#{job_definition['sources']}"
178
- rescue StandardError => ex
179
- GithubStatus.set_status(statuses,
180
- auth_token,
181
- 'error',
182
- "Couldn't retrieve Cyclid job file")
183
- Cyclid.logger.error "failed to retrieve Github Pull Request job: #{ex}"
184
- raise
185
- end
186
-
187
- Cyclid.logger.debug "job_definition=#{job_definition}"
188
-
189
- begin
190
- callback = GithubCallback.new(statuses, auth_token)
191
- job_from_definition(job_definition, callback)
192
- rescue StandardError => ex
193
- GithubStatus.set_status(statuses, auth_token, 'failure', ex)
194
- return_failure(500, 'job failed')
195
- end
196
- end
197
75
  end
198
76
  end
199
77
  end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+ # Copyright 2016 Liqwyd Ltd.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ # Top level module for the core Cyclid code.
17
+ module Cyclid
18
+ # Module for the Cyclid API
19
+ module API
20
+ # Module for Cyclid Plugins
21
+ module Plugins
22
+ # Container for the Sinatra related controllers modules
23
+ module ApiExtension
24
+ # Github plugin method callbacks
25
+ module GithubMethods
26
+ # OAuth related methods
27
+ module OAuth
28
+ # Begin the OAuth authentication flow
29
+ def oauth_request(_headers, _config)
30
+ Cyclid.logger.debug('OAuth request')
31
+ # authorize('get')
32
+
33
+ begin
34
+ # Retrieve the plugin configuration
35
+ plugins_config = Cyclid.config.plugins
36
+ github_config = load_github_config(plugins_config)
37
+
38
+ api_url = github_config[:api_url]
39
+ redirect_uri = "#{api_url}/organizations/#{organization_name}" \
40
+ '/plugins/github/oauth/callback'
41
+
42
+ # Redirect the user to the Github OAuth authorization endpoint
43
+ u = URI.parse('https://github.com/login/oauth/authorize')
44
+ u.query = URI.encode_www_form(client_id: github_config[:client_id],
45
+ scope: 'repo',
46
+ state: oauth_state,
47
+ redirect_uri: redirect_uri)
48
+ redirect u
49
+ rescue StandardError => ex
50
+ Cyclid.logger.debug "OAuth redirect failed: #{ex}"
51
+ return_failure(500, 'OAuth redirect failed')
52
+ end
53
+ end
54
+
55
+ # OAuth authentication callback
56
+ def oauth_callback(_headers, _config)
57
+ Cyclid.logger.debug('OAuth callback')
58
+
59
+ return_failure(500, 'Github OAuth response does not provide a code') \
60
+ unless params.key? 'code'
61
+
62
+ state = oauth_state
63
+
64
+ return_failure(500, 'Github OAuth response does not provide a valid state') \
65
+ unless params.key? 'state' or params['state'] != state
66
+
67
+ begin
68
+ # Retrieve the plugin configuration
69
+ plugins_config = Cyclid.config.plugins
70
+ github_config = load_github_config(plugins_config)
71
+
72
+ # Exchange the code for a bearer token
73
+ u = URI.parse('https://github.com/login/oauth/access_token')
74
+ u.query = URI.encode_www_form(client_id: github_config[:client_id],
75
+ client_secret: github_config[:client_secret],
76
+ state: state,
77
+ code: params['code'])
78
+
79
+ request = Net::HTTP::Post.new(u)
80
+ request['Accept'] = 'application/json'
81
+ http = Net::HTTP.new(u.hostname, u.port)
82
+ http.use_ssl = (u.scheme == 'https')
83
+ response = http.request(request)
84
+ rescue StandardError => ex
85
+ Cyclid.logger.debug "failed to request OAuth token: #{ex}"
86
+ return_failure(500, 'could not complete OAuth token exchange')
87
+ end
88
+
89
+ return_failure(500, "couldn't get OAuth token") \
90
+ unless response.code == '200'
91
+
92
+ # Parse the response and extract the OAuth token
93
+ begin
94
+ token = JSON.parse(response.body, symbolize_names: true)
95
+ access_token = token[:access_token]
96
+ rescue StandardError => ex
97
+ Cyclid.logger.debug "failed to parse OAuth response: #{ex}"
98
+ return_failure(500, 'failed to parse OAuth response')
99
+ end
100
+
101
+ # XXX Encrypt the token
102
+ begin
103
+ org = retrieve_organization
104
+ controller_plugin.set_config({ oauth_token: access_token }, org)
105
+ rescue StandardError => ex
106
+ Cyclid.logger.debug "failed to set plugin configuration: #{ex}"
107
+ end
108
+
109
+ # Redirect to something worth looking at
110
+ redirect github_config[:ui_url]
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end