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