Skip to content

Commit f480cae

Browse files
authored
Feature: Allow users to run their own Ruby benchmarks (#267)
* Feature: Allow certain users to run their own benchmarks * run rubocop
1 parent 6485cc7 commit f480cae

21 files changed

+545
-24
lines changed

app/assets/javascripts/application/initialize.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,48 @@ var onSelectChange = function (event) {
5656
});
5757
}
5858

59+
function onRangeTogglerChange() {
60+
if (this.checked) {
61+
$("#second-sha").parent().removeClass("hidden");
62+
} else {
63+
$("#second-sha").parent().addClass("hidden");
64+
}
65+
}
66+
67+
function onSubmitScript() {
68+
var name = $('#script-name').val();
69+
var url = $('#script-url').val();
70+
var sha = $('#first-sha').val();
71+
var sha2 = $('#second-sha').val();
72+
73+
var submitButton = $('#submit-script');
74+
submitButton.attr("disabled", true);
75+
76+
var outlet = $('#response-outlet')
77+
$.ajax("/user-scripts.json", {
78+
type: "POST",
79+
data: {
80+
name: name,
81+
url: url,
82+
sha: sha,
83+
sha2: sha2
84+
},
85+
success: function(message) {
86+
outlet.removeClass('hidden');
87+
outlet.addClass('white');
88+
outlet.html(message);
89+
},
90+
error: function(error) {
91+
outlet.removeClass('white');
92+
outlet.removeClass('hidden');
93+
outlet.html("An error occured, status: " + error.status + "<br>" + error.responseText);
94+
},
95+
complete: function() {
96+
submitButton.attr("disabled", false);
97+
}
98+
});
99+
}
100+
59101
$(document).on('turbolinks:load', function() {
60102
if (location.pathname) {
61103
$(".navbar-nav a[href='" + location.pathname + "']").addClass('current');
@@ -100,4 +142,6 @@ $(document).on('turbolinks:load', function() {
100142
});
101143

102144
$('.result-types-form select').change(onSelectChange);
145+
$('#range-toggler').on('change', onRangeTogglerChange);
146+
$('#submit-script').on('click', onSubmitScript);
103147
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
.user-script-form {
2+
width: 70%;
3+
.commit-sha {
4+
width: 50%;
5+
}
6+
7+
#response-outlet {
8+
color: white;
9+
background: $brand-primary;
10+
border-radius: 4px;
11+
padding: 6px;
12+
&.white {
13+
color: $text-color;
14+
background: $pre-bg;
15+
}
16+
}
17+
}

app/controllers/benchmark_runs_controller.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ def create
1313

1414
benchmark_type = repo.benchmark_types.find_or_create_by!(
1515
category: benchmark_type_params[:category],
16-
script_url: benchmark_type_params[:script_url]
16+
script_url: benchmark_type_params[:script_url],
17+
from_user: [true, 'true'].include?(benchmark_type_params[:from_user])
1718
)
1819

1920
benchmark_type.update_attributes(digest: benchmark_type_params[:digest])
@@ -47,7 +48,7 @@ def benchmark_run_params
4748
end
4849

4950
def benchmark_type_params
50-
params.require(:benchmark_type).permit(:category, :script_url, :digest)
51+
params.require(:benchmark_type).permit(:category, :script_url, :digest, :from_user)
5152
end
5253

5354
def benchmark_result_type_params

app/controllers/repos_controller.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,17 @@ def set_display_count
195195

196196
def set_repo_benchmarks
197197
@benchmarks = @repo.benchmark_types
198+
set_rubybench_benchmarks
199+
set_users_benchmarks
200+
@benchmarks
201+
end
202+
203+
def set_rubybench_benchmarks
204+
@rubybench_benchmarks = @benchmarks.where(from_user: false)
205+
end
206+
207+
def set_users_benchmarks
208+
@users_benchmarks = @benchmarks.where(from_user: true)
198209
end
199210

200211
def set_comparable_benchmarks

app/controllers/session_controller.rb

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
class SessionController < ApplicationController
2+
USERNAMES = %w{john osama sam tgx patrick}
3+
TRUSTED_GROUP_NAME = 'trusted-users'
4+
25
def sso
36
sso = DiscourseApi::SingleSignOn.parse(request.query_string, Rails.application.secrets.sso_secret)
47
return_url = $redis.get(sso.nonce)
58
if return_url.present?
69
session[:user] = {
710
username: sso.username,
8-
email: sso.email,
9-
external_id: sso.external_id
11+
external_id: sso.external_id,
12+
trusted: is_trusted?(sso)
1013
}
1114
redirect_to return_url
1215
else
@@ -20,7 +23,32 @@ def login
2023
sso.return_sso_url = "#{request.base_url}/sso"
2124
sso.nonce = SecureRandom.hex
2225
sso.sso_url = "#{AppSettings.forum_url}/session/sso_provider"
23-
$redis.setex(sso.nonce, 10.minutes.to_i, '/')
26+
path = session[:destination_url] || '/'
27+
$redis.setex(sso.nonce, 10.minutes.to_i, path)
2428
redirect_to sso.to_url
2529
end
30+
31+
def become
32+
if Rails.env.production?
33+
render plain: "Can't use this endpoint in production", status: 403
34+
return
35+
end
36+
37+
permitted = params.permit([:username, :external_id, :trusted])
38+
hash = {
39+
username: "#{USERNAMES.sample}#{SecureRandom.random_number(20)}",
40+
external_id: SecureRandom.random_number(1000),
41+
trusted: false
42+
}.merge(permitted.to_h.symbolize_keys.slice(:username, :external_id, :trusted))
43+
44+
hash[:trusted] = [true, 'true'].include?(hash[:trusted])
45+
session[:user] = hash
46+
redirect_to '/'
47+
end
48+
49+
private
50+
51+
def is_trusted?(sso)
52+
sso.admin || sso&.groups&.first&.split(',')&.include?(TRUSTED_GROUP_NAME)
53+
end
2654
end
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
class UserScriptsController < ApplicationController
2+
before_action :ensure_trusted, except: :index
3+
4+
def index
5+
if !current_user
6+
session[:destination_url] = request.fullpath
7+
elsif !current_user.trusted
8+
render status: 403
9+
end
10+
end
11+
12+
def create
13+
bench = UserBench.new(
14+
params[:name],
15+
params[:url],
16+
params[:sha],
17+
params[:sha2]
18+
)
19+
bench.validate!
20+
if bench.valid?
21+
bench.run
22+
23+
render plain: t('.index.successful_submit', path: "/ruby/ruby/commits?result_type=#{bench.name}")
24+
else
25+
render plain: bench.errors.join("\n"), status: 422
26+
end
27+
end
28+
29+
private
30+
31+
def ensure_trusted
32+
if !current_user || !current_user.trusted
33+
render plain: 'Not allowed to post scripts', status: 403
34+
end
35+
end
36+
end

app/jobs/remote_server_job.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class RemoteServerJob < ActiveJob::Base
2020

2121
RUBY_COMMIT_DISCOURSE = "#{SCRIPTS_PATH}/ruby/discourse/trunk.sh"
2222

23-
RUBY_CUSTOM_SCRIPTS = "#{SCRIPTS_PATH}/ruby/custom_scripts/receiver.sh"
23+
RUBY_USER_SCRIPTS = "#{SCRIPTS_PATH}/ruby/user_scripts/receiver.sh"
2424

2525
# Use keyword arguments once Rails 4.2.1 has been released.
2626
def perform(initiator_key, benchmark_group, options = {})
@@ -62,12 +62,12 @@ def ruby_release(ssh, version, options)
6262
)
6363
end
6464

65-
def ruby_custom_scripts(ssh, script_url, options)
66-
commit_a = options[:commit_a]
67-
commit_b = options[:commit_b]
65+
def ruby_user_scripts(ssh, commit, options)
66+
name = options[:name]
67+
script_url = options[:script_url]
6868
ssh_exec!(
6969
ssh,
70-
"#{RUBY_CUSTOM_SCRIPTS} #{script_url} #{commit_a} #{commit_b} #{secrets.api_name} #{secrets.api_password}"
70+
"#{RUBY_USER_SCRIPTS} #{commit} #{name} #{script_url} #{secrets.api_name} #{secrets.api_password}"
7171
)
7272
end
7373

app/jobs/run_user_bench.rb

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
class RunUserBench < ActiveJob::Base
2+
queue_as :default
3+
4+
def perform(name, script_url, start_date, stop_sha)
5+
commits = fetch_commits(start_date, stop_sha)
6+
7+
repo = Repo.find_or_create_by!(
8+
name: 'ruby',
9+
url: 'https://github.com/tgxworld/ruby',
10+
organization: Organization.find_or_create_by!(
11+
name: 'ruby',
12+
url: 'https://github.com/tgxworld/',
13+
)
14+
)
15+
16+
CommitsRunner.run(:api, commits, repo, '', smart: true, name: name, script_url: script_url, initiator_type: 'user_scripts')
17+
end
18+
19+
private
20+
21+
def fetch_commits(start_date, stop_sha)
22+
client = Octokit::Client.new(access_token: Rails.application.secrets.github_api_token, per_page: 100)
23+
commits = []
24+
done = false
25+
batch = client.commits('ruby/ruby', until: start_date)
26+
response = client.last_response
27+
while batch.size > 0 && !done
28+
index = -1
29+
batch.each_with_index do |c, ind|
30+
if c.sha == stop_sha
31+
done = true
32+
index = ind
33+
end
34+
end
35+
if done
36+
commits.push(*batch[0..index])
37+
break
38+
else
39+
commits.push(*batch)
40+
response = response.rels[:next]&.get
41+
batch = response&.data || []
42+
end
43+
end
44+
commits
45+
end
46+
end

app/services/commits_runner.rb

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
module CommitsRunner
2-
def self.run(trigger_source, commits, repo, pattern = '', smart: false)
2+
def self.run(trigger_source, commits, repo, pattern = '', opts = {})
3+
smart = !!opts[:smart]
34
formatted_commits =
45
if trigger_source == :webhook
56
format_webhook(commits, repo)
@@ -9,7 +10,7 @@ def self.run(trigger_source, commits, repo, pattern = '', smart: false)
910

1011
formatted_commits = smart_reorder(formatted_commits) if smart
1112
formatted_commits.select { |commit| valid?(commit) }
12-
.each { |commit| create_and_run(commit, pattern) }
13+
.each { |commit| create_and_run(commit, pattern, opts) }
1314
.count
1415
end
1516

@@ -45,7 +46,7 @@ def self.valid?(commit)
4546
!Commit.merge_or_skip_ci?(commit[:message]) && Commit.valid_author?(commit[:author_name])
4647
end
4748

48-
def self.create_and_run(commit, pattern)
49+
def self.create_and_run(commit, pattern, opts = {})
4950
Commit.find_or_create_by!(sha1: commit[:sha]) do |c|
5051
c.url = commit[:url]
5152
c.message = commit[:message]
@@ -54,10 +55,10 @@ def self.create_and_run(commit, pattern)
5455
end
5556

5657
BenchmarkPool.enqueue(
57-
:commit,
58+
opts[:initiator_type] || :commit,
5859
commit[:sha],
5960
commit[:repo].name,
60-
include_patterns: pattern
61+
opts.merge(include_patterns: pattern)
6162
)
6263
end
6364

app/services/user_bench.rb

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
class UserBench
2+
attr_reader :errors, :name, :url, :sha, :sha2, :commits
3+
4+
def initialize(name, url, sha, sha2 = nil)
5+
@name = name&.strip
6+
@url = url&.strip
7+
@sha = sha&.strip
8+
@sha2 = sha2&.strip
9+
@errors = []
10+
@commits = []
11+
@client = Octokit::Client.new(access_token: Rails.application.secrets.github_api_token)
12+
end
13+
14+
def validate!
15+
errors.push(err('missing_name')) if name.blank?
16+
errors.push(err('missing_url')) if url.blank?
17+
errors.push(err('missing_sha')) if sha.blank?
18+
return if !valid?
19+
20+
errors.push(err('name_already_taken')) if name_taken?
21+
errors.push(err('bad_url')) unless valid_url?
22+
errors.push(err('unallowed_characters', name: name)) unless valid_name?
23+
24+
return if !valid?
25+
26+
validate_sha(sha)
27+
validate_sha(sha2) if sha2.present?
28+
end
29+
30+
def valid?
31+
@errors.size == 0
32+
end
33+
34+
def run
35+
repo = Repo.find_or_create_by!(
36+
name: 'ruby',
37+
url: 'https://github.com/tgxworld/ruby',
38+
organization: Organization.find_or_create_by!(
39+
name: 'ruby',
40+
url: 'https://github.com/tgxworld/',
41+
)
42+
)
43+
BenchmarkType.create!(
44+
category: name,
45+
script_url: url,
46+
from_user: true,
47+
repo: repo
48+
)
49+
50+
RunUserBench.perform_later(name, url, commits.last.commit.committer.date.iso8601, commits.first.sha)
51+
end
52+
53+
private
54+
55+
def valid_url?
56+
uri = URI.parse(url)
57+
URI::HTTP === uri || URI::HTTPS === uri
58+
rescue URI::InvalidURIError
59+
false
60+
end
61+
62+
def name_taken?
63+
BenchmarkType.exists?(category: name)
64+
end
65+
66+
def valid_name?
67+
name.match?(/^[a-zA-Z0-9\-_]+$/)
68+
end
69+
70+
def validate_sha(sha)
71+
commit = @client.commit('ruby/ruby', sha)
72+
add_commit(commit)
73+
rescue Octokit::UnprocessableEntity
74+
errors.push(err('bad_sha', sha: sha))
75+
end
76+
77+
def add_commit(commit)
78+
if commits.all? { |c| c.sha != commit.sha }
79+
commits.push(commit)
80+
commits.sort_by! { |c| c.commit.committer.date }
81+
end
82+
end
83+
84+
def err(key, *args)
85+
I18n.t("user_scripts.errors.#{key}", *args)
86+
end
87+
end

0 commit comments

Comments
 (0)