cyclid 0.2.1 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/cyclid/controllers/organizations.rb +1 -1
- data/app/cyclid/controllers/organizations/config.rb +23 -2
- data/app/cyclid/job/runner.rb +1 -1
- data/app/cyclid/log_buffer.rb +1 -1
- data/app/cyclid/plugins.rb +10 -0
- data/app/cyclid/plugins/action/email.rb +6 -1
- data/app/cyclid/plugins/action/slack.rb +5 -0
- data/app/cyclid/plugins/api.rb +42 -49
- data/app/cyclid/plugins/api/github.rb +28 -15
- data/app/cyclid/plugins/api/github/README.md +62 -0
- data/app/cyclid/plugins/api/github/callback.rb +19 -4
- data/app/cyclid/plugins/api/github/config.rb +45 -0
- data/app/cyclid/plugins/api/github/helpers.rb +139 -0
- data/app/cyclid/plugins/api/github/methods.rb +16 -138
- data/app/cyclid/plugins/api/github/oauth.rb +117 -0
- data/app/cyclid/plugins/api/github/pull_request.rb +134 -0
- data/app/cyclid/plugins/api/github/push.rb +121 -0
- data/app/cyclid/plugins/dispatcher/local.rb +3 -3
- data/app/cyclid/plugins/provisioner/debian.rb +15 -14
- data/app/cyclid/plugins/provisioner/ubuntu.rb +15 -14
- data/app/cyclid/plugins/source/git.rb +1 -1
- data/app/cyclid/plugins/transport/ssh.rb +3 -1
- data/app/cyclid/sinatra/api_helpers.rb +6 -0
- data/lib/cyclid/version.rb +1 -1
- metadata +22 -3
- data/app/cyclid/plugins/api/github/status.rb +0 -67
@@ -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(
|
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 =
|
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
|