cyclid 0.2.1 → 0.2.2

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.
@@ -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