Skip to content

Commit 124ab86

Browse files
authored
Merge pull request #1076 from travis-ci/ha-json-log-egalitarian
Serve JSON data for archived job log requests, regardless of the User-Agent value
2 parents c2d9b81 + ed2f327 commit 124ab86

File tree

10 files changed

+222
-21
lines changed

10 files changed

+222
-21
lines changed

lib/travis/api/app/endpoint/jobs.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ class Jobs < Endpoint
9292
elsif resource.archived?
9393
# the way we use responders makes it hard to validate proper format
9494
# automatically here, so we need to check it explicitly
95-
if accepts?('text/plain') || request.user_agent.to_s.start_with?('Travis')
95+
if accepts?('text/plain')
9696
archived_log_path = resource.archived_url
9797

9898
if params[:cors_hax]
@@ -103,6 +103,9 @@ class Jobs < Endpoint
103103
else
104104
redirect archived_log_path, 307
105105
end
106+
elsif accepts?('application/json')
107+
attach_log_token if job.try(:private?)
108+
respond_with resource.as_json
106109
else
107110
status 406
108111
end

lib/travis/api/app/endpoint/logs.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ class Logs < Endpoint
1616
elsif resource.archived?
1717
# the way we use responders makes it hard to validate proper format
1818
# automatically here, so we need to check it explicitly
19-
if accepts?('text/plain') || request.user_agent.to_s.start_with?('Travis')
19+
if accepts?('text/plain')
2020
redirect resource.archived_url, 307
21+
elsif accepts?('application/json')
22+
respond_with resource.as_json
2123
else
2224
status 406
2325
end

lib/travis/remote_log.rb

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ def archived_url(expires: nil)
7575
@archived_url ||= remote.fetch_archived_url(job_id, expires: expires)
7676
end
7777

78+
def archived_log_content
79+
@archived_content ||= remote.fetch_archived_log_content(job_id)
80+
end
81+
7882
def to_json(chunked: false, after: nil, part_numbers: [])
7983
as_json(
8084
chunked: chunked,
@@ -101,7 +105,7 @@ def as_json(chunked: false, after: nil, part_numbers: [])
101105
part_numbers: part_numbers
102106
).map(&:as_json)
103107
else
104-
ret['body'] = content
108+
ret['body'] = archived_log_content || content
105109
end
106110

107111
{ 'log' => ret }
@@ -243,6 +247,12 @@ def fetch_archived_url(job_id, expires: nil)
243247
file.url(expires)
244248
end
245249

250+
def fetch_archived_log_content(job_id)
251+
file = fetch_archived(job_id)
252+
return "" if file.nil?
253+
file.body
254+
end
255+
246256
private def fetch_archived(job_id)
247257
candidates = s3.directories.get(
248258
bucket_name,
@@ -268,7 +278,7 @@ class Remote
268278
def_delegators :client, :find_by_job_id, :find_by_id,
269279
:find_id_by_job_id, :find_parts_by_job_id, :write_content_for_job_id
270280

271-
def_delegators :archive_client, :fetch_archived_url
281+
def_delegators :archive_client, :fetch_archived_url, :fetch_archived_log_content
272282

273283
attr_accessor :platform
274284

spec/auth/v2.1/jobs_spec.rb

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,35 @@
44
let(:job) { repo.builds.first.matrix.first }
55
let(:log) { %({"job_id": #{job.id}, "content": "content"}) }
66

7-
let(:log_url) { "#{Travis.config[:logs_api][:url]}/logs/#{job.id}?by=job_id&source=api" }
8-
before { stub_request(:get, log_url).to_return(status: 200, body: log) }
7+
let :log_from_api do
8+
{
9+
aggregated_at: Time.now,
10+
archive_verified: true,
11+
archived_at: Time.now,
12+
archiving: false,
13+
content: 'hello world. this is a really cool log',
14+
created_at: Time.now,
15+
id: 1,
16+
job_id: job.id,
17+
purged_at: nil,
18+
removed_at: nil,
19+
removed_by_id: nil,
20+
updated_at: Time.now
21+
}
22+
end
23+
24+
let(:archived_content) { "$ git clean -fdx\nRemoving Gemfile.lock\n$ git fetch" }
25+
26+
let(:log_url) { "#{Travis.config[:logs_api][:url]}/logs/1?by=id&source=api" }
27+
28+
before do
29+
remote = double('remote')
30+
allow(Travis::RemoteLog::Remote).to receive(:new).and_return(remote)
31+
allow(remote).to receive(:find_by_job_id).and_return(Travis::RemoteLog.new(log_from_api))
32+
allow(remote).to receive(:find_by_id).and_return(Travis::RemoteLog.new(log_from_api))
33+
allow(remote).to receive(:fetch_archived_url).and_return('https://s3.amazonaws.com/STUFFS')
34+
end
35+
936
before { Job.update_all(state: :started) }
1037

1138
# TODO

spec/auth/v2.1/logs_spec.rb

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,34 @@
55
let(:job) { build.matrix.first }
66
let(:log) { double(id: 1) }
77

8+
let :log_from_api do
9+
{
10+
aggregated_at: Time.now,
11+
archive_verified: true,
12+
archived_at: Time.now,
13+
archiving: false,
14+
content: 'hello world. this is a really cool log',
15+
created_at: Time.now,
16+
id: 1,
17+
job_id: job.id,
18+
purged_at: nil,
19+
removed_at: nil,
20+
removed_by_id: nil,
21+
updated_at: Time.now
22+
}
23+
end
24+
25+
let(:archived_content) { "$ git clean -fdx\nRemoving Gemfile.lock\n$ git fetch" }
26+
827
let(:log_url) { "#{Travis.config[:logs_api][:url]}/logs/1?by=id&source=api" }
9-
before { stub_request(:get, log_url).to_return(status: 200, body: %({"job_id": #{job.id}, "content": "content"})) }
28+
29+
before do
30+
remote = double('remote')
31+
allow(Travis::RemoteLog::Remote).to receive(:new).and_return(remote)
32+
allow(remote).to receive(:find_by_job_id).and_return(Travis::RemoteLog.new(log_from_api))
33+
allow(remote).to receive(:find_by_id).and_return(Travis::RemoteLog.new(log_from_api))
34+
allow(remote).to receive(:fetch_archived_url).and_return('https://s3.amazonaws.com/STUFFS')
35+
end
1036

1137
describe 'in public mode, with a private repo', mode: :public, repo: :private do
1238
describe 'GET /logs/%{log.id}' do

spec/auth/v2/jobs_spec.rb

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,36 @@
55
let(:log) { %({"job_id": #{job.id}, "content": "content"}) }
66

77
let(:log_url) { "#{Travis.config[:logs_api][:url]}/logs/#{job.id}?by=job_id&source=api" }
8-
before { stub_request(:get, log_url).to_return(status: 200, body: log) }
8+
9+
let :log_from_api do
10+
{
11+
aggregated_at: Time.now,
12+
archive_verified: true,
13+
archived_at: Time.now,
14+
archiving: false,
15+
content: 'hello world. this is a really cool log',
16+
created_at: Time.now,
17+
id: 1,
18+
job_id: job.id,
19+
purged_at: nil,
20+
removed_at: nil,
21+
removed_by_id: nil,
22+
updated_at: Time.now
23+
}
24+
end
25+
26+
let(:archived_content) { "$ git clean -fdx\nRemoving Gemfile.lock\n$ git fetch" }
27+
28+
let(:log_url) { "#{Travis.config[:logs_api][:url]}/logs/1?by=id&source=api" }
29+
30+
before do
31+
remote = double('remote')
32+
allow(Travis::RemoteLog::Remote).to receive(:new).and_return(remote)
33+
allow(remote).to receive(:find_by_job_id).and_return(Travis::RemoteLog.new(log_from_api))
34+
allow(remote).to receive(:find_by_id).and_return(Travis::RemoteLog.new(log_from_api))
35+
allow(remote).to receive(:fetch_archived_url).and_return('https://s3.amazonaws.com/STUFFS')
36+
end
37+
938
before { Job.update_all(state: :started) }
1039

1140
# TODO

spec/auth/v2/logs_spec.rb

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,34 @@
55
let(:job) { build.matrix.first }
66
let(:log) { double(id: 1) }
77

8+
let :log_from_api do
9+
{
10+
aggregated_at: Time.now,
11+
archive_verified: true,
12+
archived_at: Time.now,
13+
archiving: false,
14+
content: 'hello world. this is a really cool log',
15+
created_at: Time.now,
16+
id: 1,
17+
job_id: job.id,
18+
purged_at: nil,
19+
removed_at: nil,
20+
removed_by_id: nil,
21+
updated_at: Time.now
22+
}
23+
end
24+
25+
let(:archived_content) { "$ git clean -fdx\nRemoving Gemfile.lock\n$ git fetch" }
26+
827
let(:log_url) { "#{Travis.config[:logs_api][:url]}/logs/1?by=id&source=api" }
9-
before { stub_request(:get, log_url).to_return(status: 200, body: %({"job_id": #{job.id}, "content": "content"})) }
28+
29+
before do
30+
remote = double('remote')
31+
allow(Travis::RemoteLog::Remote).to receive(:new).and_return(remote)
32+
allow(remote).to receive(:find_by_job_id).and_return(Travis::RemoteLog.new(log_from_api))
33+
allow(remote).to receive(:find_by_id).and_return(Travis::RemoteLog.new(log_from_api))
34+
allow(remote).to receive(:fetch_archived_url).and_return('https://s3.amazonaws.com/STUFFS')
35+
end
1036

1137
describe 'in public mode, with a private repo', mode: :public, repo: :private do
1238
describe 'GET /logs/%{log.id}' do

spec/integration/v2/jobs_spec.rb

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,39 @@
66
let(:job) { jobs.first }
77
let(:headers) { { 'HTTP_ACCEPT' => 'application/vnd.travis-ci.2+json' } }
88

9+
let :log_from_api do
10+
{
11+
aggregated_at: Time.now,
12+
archive_verified: true,
13+
archived_at: Time.now,
14+
archiving: false,
15+
content: 'hello world. this is a really cool log',
16+
created_at: Time.now,
17+
id: 1,
18+
job_id: job.id,
19+
purged_at: nil,
20+
removed_at: nil,
21+
removed_by_id: nil,
22+
updated_at: Time.now
23+
}
24+
end
25+
26+
let(:archived_log_url) { 'https://s3.amazonaws.com/STUFFS' } # bogus URL unused anywhere
27+
let(:archived_content) { "$ git clean -fdx\nRemoving Gemfile.lock\n$ git fetch" }
28+
29+
let(:log_url) { "#{Travis.config[:logs_api][:url]}/logs/1?by=id&source=api" }
30+
31+
before do
32+
remote = double('remote')
33+
allow(Travis::RemoteLog::Remote).to receive(:new).and_return(remote)
34+
allow(remote).to receive(:find_by_job_id).and_return(Travis::RemoteLog.new(log_from_api))
35+
allow(remote).to receive(:find_by_id).and_return(Travis::RemoteLog.new(log_from_api))
36+
allow(remote).to receive(:fetch_archived_url).and_return(archived_log_url)
37+
allow(remote).to receive(:fetch_archived_log_content).and_return(archived_content)
38+
allow(remote).to receive(:write_content_for_job_id).and_return(remote)
39+
allow(remote).to receive(:send).and_return(remote) # ignore attribute updates
40+
end
41+
942
it '/jobs?queue=builds.common' do
1043
skip('querying with a queue does not appear to be used anymore')
1144
response = get '/jobs', { queue: 'builds.common' }, headers
@@ -38,11 +71,15 @@
3871
end
3972

4073
context 'GET /jobs/:job_id/log.txt' do
41-
it 'returns log for a job' do
42-
stub_request(:get, "#{Travis.config.logs_api.url}/logs/#{job.id}?by=job_id&source=api")
43-
.to_return(status: 200, body: JSON.dump(content: 'the log'))
74+
it 'returns redirects to archived log url' do
75+
response = get("/jobs/#{job.id}/log.txt")
76+
expect(response.status).to eq(307)
77+
expect(response.location).to eq(archived_log_url)
78+
end
79+
80+
it 'returns 406 (Unprocessable) if Accept header requests JSON' do
4481
response = get("/jobs/#{job.id}/log.txt", {}, headers)
45-
expect(response).to deliver_as_txt('the log', version: 'v2')
82+
expect(response.status).to eq(406)
4683
end
4784

4885
context 'when log is archived' do
@@ -75,18 +112,16 @@
75112
end
76113

77114
context 'when log is missing' do
78-
it 'responds with an empty representation' do
115+
it 'redirects to archived log url' do
79116
stub_request(:get, "#{Travis.config.logs_api.url}/logs/#{job.id}?by=job_id&source=api")
80117
.to_return(status: 404, body: '')
81118
response = get(
82119
"/jobs/#{job.id}/log.txt",
83120
{},
84121
{ 'HTTP_ACCEPT' => 'text/plain; version=2' }
85122
)
86-
expect(response.status).to eq(200)
87-
expect(JSON.parse(response.body, symbolize_names: true)).to eq(
88-
{ log: { job_id: job.id, parts: [], :@type => 'Log' } }
89-
)
123+
expect(response.status).to eq(307)
124+
expect(response.location).to eq(archived_log_url)
90125
end
91126
end
92127

@@ -118,7 +153,7 @@
118153
end
119154

120155
context 'with chunked log requested' do
121-
it 'always responds with 406' do
156+
it 'succeeds' do
122157
stub_request(:get, "#{Travis.config.logs_api.url}/logs/#{job.id}?by=job_id&source=api")
123158
.to_return(
124159
status: 200,
@@ -134,7 +169,7 @@
134169
{},
135170
{ 'HTTP_ACCEPT' => 'application/json; version=2; chunked=true' }
136171
)
137-
expect(response.status).to eq(406)
172+
expect(response.status).to eq(200)
138173
end
139174
end
140175
end

spec/integration/visibility_spec.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
let(:response) { send(method, path, {}, { 'HTTP_ACCEPT' => 'application/vnd.travis-ci.2.1+json' }) }
77
let(:status) { response.status }
88
let(:body) { JSON.parse(response.body).deep_symbolize_keys }
9+
let(:job_id) { 42864 }
10+
let(:archived_content) { 'hello world!'}
911

1012
before { repo.update_attributes(private: false) }
1113
before { requests.update_all(private: true) }
@@ -14,6 +16,26 @@
1416
before { requests[0].update_attributes(private: false) }
1517
before { builds[0].update_attributes(private: false) }
1618
before { jobs[0].update_attributes(private: false) }
19+
before :each do
20+
Fog.mock!
21+
storage = Fog::Storage.new({
22+
aws_access_key_id: 'key',
23+
aws_secret_access_key: 'secret',
24+
provider: 'AWS'
25+
})
26+
bucket = storage.directories.create(key: 'archive.travis-ci.org')
27+
file = bucket.files.create(
28+
key: "jobs/#{job_id}/log.txt",
29+
body: archived_content
30+
)
31+
32+
allow_any_instance_of(Travis::RemoteLog::ArchiveClient).to receive(:fetch_archived).and_return(file)
33+
end
34+
35+
after do
36+
Fog.unmock!
37+
Fog::Mock.reset
38+
end
1739

1840
let(:public_request) { requests[0] }
1941
let(:public_build) { builds[0] }

spec/lib/travis/remote_log_spec.rb

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,32 @@
22

33
describe Travis::RemoteLog do
44
subject { described_class.new(attrs) }
5-
let(:attrs) { { id: 4, content: 'huh', job_id: 5 } }
5+
let(:job_id) { 5 }
6+
let(:attrs) { { id: 4, content: archived_content, job_id: job_id } }
7+
let(:archived_content) { 'hello world' }
68

79
before :each do
810
described_class::Remote.instance_variable_set(:@clients, nil)
911
described_class::Remote.instance_variable_set(:@archive_clients, nil)
12+
13+
Fog.mock!
14+
storage = Fog::Storage.new({
15+
aws_access_key_id: 'key',
16+
aws_secret_access_key: 'secret',
17+
provider: 'AWS'
18+
})
19+
bucket = storage.directories.create(key: 'archive.travis-ci.org')
20+
file = bucket.files.create(
21+
key: "jobs/#{job_id}/log.txt",
22+
body: archived_content
23+
)
24+
25+
allow_any_instance_of(Travis::RemoteLog::ArchiveClient).to receive(:fetch_archived).and_return(file)
26+
end
27+
28+
after do
29+
Fog.unmock!
30+
Fog::Mock.reset
1031
end
1132

1233
it 'has a default client' do

0 commit comments

Comments
 (0)