Skip to content

Commit 4ea6857

Browse files
committed
Merge branch 'release_20.09' into dev
2 parents a5708dd + 79a018f commit 4ea6857

File tree

10 files changed

+95
-54
lines changed

10 files changed

+95
-54
lines changed

client/src/components/User/ExternalIdentities/ExternalIdentities.vue

+2-2
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ import Vue from "vue";
8989
import BootstrapVue from "bootstrap-vue";
9090
import { getGalaxyInstance } from "app";
9191
import svc from "./service";
92-
import { logoutClick } from "layout/menu";
92+
import { userLogout } from "layout/menu";
9393
import ExternalLogin from "components/User/ExternalIdentities/ExternalLogin.vue";
9494
9595
Vue.use(BootstrapVue);
@@ -180,7 +180,7 @@ export default {
180180
disconnectAndReset() {
181181
// Disconnects the user's final ext id and logouts of current session
182182
this.disconnectID();
183-
logoutClick();
183+
userLogout();
184184
},
185185
removeItem(item) {
186186
this.items = this.items.filter((o) => o != item);

client/src/components/User/UserPreferences.vue

+4-9
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ import _l from "utils/localization";
8888
import axios from "axios";
8989
import QueryStringParsing from "utils/query-string-parsing";
9090
import { getUserPreferencesModel } from "components/User/UserPreferencesModel";
91+
import { userLogoutAll, userLogoutClient } from "layout/menu";
9192
import "@fortawesome/fontawesome-svg-core";
9293
9394
Vue.use(BootstrapVue);
@@ -217,11 +218,7 @@ export default {
217218
Cancel: function () {
218219
Galaxy.modal.hide();
219220
},
220-
"Sign out": function () {
221-
window.location.href = `${getAppRoot()}user/logout?session_csrf_token=${
222-
Galaxy.session_csrf_token
223-
}`;
224-
},
221+
"Sign out": userLogoutAll,
225222
},
226223
});
227224
},
@@ -241,23 +238,21 @@ export default {
241238
this.handleSubmit();
242239
},
243240
async handleSubmit() {
244-
const Galaxy = getGalaxyInstance();
245-
const userId = Galaxy.user.id;
246241
if (!this.checkFormValidity()) {
247242
return false;
248243
}
249244
if (this.email === this.name) {
250245
this.nameState = true;
251246
try {
252-
await axios.delete(`${getAppRoot()}api/users/${userId}`);
247+
await axios.delete(`${getAppRoot()}api/users/${this.userId}`);
253248
} catch (e) {
254249
if (e.response.status === 403) {
255250
this.deleteError =
256251
"User deletion must be configured on this instance in order to allow user self-deletion. Please contact an administrator for assistance.";
257252
return false;
258253
}
259254
}
260-
window.location.href = `${getAppRoot()}user/logout?session_csrf_token=${Galaxy.session_csrf_token}`;
255+
userLogoutClient();
261256
} else {
262257
this.nameState = false;
263258
return false;

client/src/layout/menu.js

+26-5
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,16 @@ import { getGalaxyInstance } from "app";
33
import _l from "utils/localization";
44
import { CommunicationServerView } from "layout/communication-server-view";
55

6-
export function logoutClick() {
6+
const POST_LOGOUT_URL = "root/login?is_logout_redirect=true";
7+
8+
/**
9+
* Handles user logout. Invalidates the current session, checks to see if we
10+
* need to log out of OIDC too, and goes to our POST_LOGOUT_URL (or some other
11+
* configured redirect). */
12+
export function userLogout(logoutAll = false) {
713
const galaxy = getGalaxyInstance();
814
const session_csrf_token = galaxy.session_csrf_token;
9-
const url = `${galaxy.root}user/logout?session_csrf_token=${session_csrf_token}`;
15+
const url = `${galaxy.root}user/logout?session_csrf_token=${session_csrf_token}&logout_all=${logoutAll}`;
1016
axios
1117
.get(url)
1218
.then(() => {
@@ -21,14 +27,29 @@ export function logoutClick() {
2127
}
2228
})
2329
.then((response) => {
24-
if (response.data && response.data.redirect_uri) {
30+
if (response.data?.redirect_uri) {
2531
window.top.location.href = response.data.redirect_uri;
2632
} else {
27-
window.top.location.href = `${galaxy.root}root/login?is_logout_redirect=true`;
33+
window.top.location.href = `${galaxy.root}${POST_LOGOUT_URL}`;
2834
}
2935
});
3036
}
3137

38+
/** User logout with 'log out all sessions' flag set. This will invalidate all
39+
* active sessions a user might have. */
40+
export function userLogoutAll() {
41+
return userLogout(true);
42+
}
43+
44+
/** Purely clientside logout, dumps session and redirects without invalidating
45+
* serverside. Currently only used when marking an account deleted -- any
46+
* subsequent navigation after the deletion API request would fail otherwise */
47+
export function userLogoutClient() {
48+
const galaxy = getGalaxyInstance();
49+
galaxy.user?.clearSessionStorage();
50+
window.top.location.href = `${galaxy.root}${POST_LOGOUT_URL}`;
51+
}
52+
3253
export function fetchMenu(options = {}) {
3354
const Galaxy = getGalaxyInstance();
3455
const menu = [];
@@ -244,7 +265,7 @@ export function fetchMenu(options = {}) {
244265
{
245266
title: _l("Logout"),
246267
divider: true,
247-
onclick: logoutClick,
268+
onclick: userLogout,
248269
},
249270
{
250271
title: _l("Datasets"),

lib/galaxy/jobs/runners/kubernetes.py

+4-21
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
Offload jobs to a Kubernetes cluster.
33
"""
44

5-
import errno
65
import logging
76
import math
87
import os
@@ -459,30 +458,14 @@ def check_watched_item(self, job_state):
459458
# there is no job responding to this job_id, it is either lost or something happened.
460459
log.error("No Jobs are available under expected selector app=%s", job_state.job_id)
461460
self.mark_as_failed(job_state)
462-
try:
463-
with open(job_state.error_file, 'w') as error_file:
464-
error_file.write("No Kubernetes Jobs are available under expected selector app=%s\n" % job_state.job_id)
465-
except OSError as e:
466-
# Python 2/3 compatible handling of FileNotFoundError
467-
if e.errno == errno.ENOENT:
468-
log.error("Job directory already cleaned up. Assuming already handled for selector app=%s", job_state.job_id)
469-
else:
470-
raise
471-
return job_state
461+
# job is no longer viable - remove from watched jobs
462+
return None
472463
else:
473464
# there is more than one job associated to the expected unique job id used as selector.
474465
log.error("More than one Kubernetes Job associated to job id '%s'", job_state.job_id)
475466
self.mark_as_failed(job_state)
476-
try:
477-
with open(job_state.error_file, 'w') as error_file:
478-
error_file.write("More than one Kubernetes Job associated with job id '%s'\n" % job_state.job_id)
479-
except OSError as e:
480-
# Python 2/3 compatible handling of FileNotFoundError
481-
if e.errno == errno.ENOENT:
482-
log.error("Job directory already cleaned up. Assuming already handled for selector app=%s", job_state.job_id)
483-
else:
484-
raise
485-
return job_state
467+
# job is no longer viable - remove from watched jobs
468+
return None
486469

487470
def _handle_job_failure(self, job, job_state):
488471
# Figure out why job has failed

lib/galaxy/tool_util/cwl/util.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ def __init__(self, contents, path=None, **kwargs):
338338
self.path = path
339339

340340
def __str__(self):
341-
return "FileLiteralTarget[path={}] with {}".format(self.path, self.properties)
341+
return "FileLiteralTarget[contents={}] with {}".format(self.contents, self.properties)
342342

343343

344344
class FileUploadTarget:

lib/galaxy/tools/repositories.py

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def __init__(self, app_name,
2929
self.temporary_path = tempfile.mkdtemp(prefix='tool_validation_')
3030
self.config.tool_data_table_config = os.path.join(self.temporary_path, 'tool_data_table_conf.xml')
3131
self.config.shed_tool_data_table_config = os.path.join(self.temporary_path, 'shed_tool_data_table_conf.xml')
32+
self.config.interactivetools_enable = True
3233
self.tool_data_tables = tool_data_tables
3334
self.tool_shed_registry = Bunch(tool_sheds={})
3435
self.datatypes_registry = registry

lib/galaxy/webapps/galaxy/controllers/tool_runner.py

+3
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ def __tool_404__():
4747
# tool id not available, redirect to main page
4848
if tool_id is None:
4949
return trans.response.send_redirect(url_for(controller='root', action='welcome'))
50+
if tool_id.endswith('/'):
51+
# Probably caused by a redirect
52+
tool_id = tool_id[:-1]
5053
tool = self.__get_tool(tool_id)
5154
# tool id is not matching, display an error
5255
if not tool:

lib/galaxy/webapps/galaxy/controllers/user.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ def resend_activation_email(self, trans, email, username):
200200
"""
201201
if email is None: # User is coming from outside registration form, load email from trans
202202
if not trans.user:
203-
trans.show_error_message("No session found, cannot send activation email.")
203+
return "No session found, cannot send activation email.", None
204204
email = trans.user.email
205205
if username is None: # User is coming from outside registration form, load email from trans
206206
username = trans.user.username

lib/galaxy_test/selenium/test_sign_out.py

+1
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@ def test_sign_out(self):
1818
self.assertTrue(email == new_email)
1919
self.components.preferences.sign_out.wait_for_and_click()
2020
self.components.sign_out.sign_out_button.wait_for_and_click()
21+
self.sleep_for(self.wait_types.UX_TRANSITION)
2122
assert not self.is_logged_in()

test/integration/test_kubernetes_runner.py

+52-15
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,17 @@ def test_job_environment(self):
216216
job_env = self._run_and_get_environment_properties()
217217
assert job_env.some_env == '42'
218218

219+
@staticmethod
220+
def _wait_for_external_state(sa_session, job, expected):
221+
# Not checking the state here allows the change from queued to running to overwrite
222+
# the change from queued to deleted_new in the API thread - this is a problem because
223+
# the job will still run. See issue https://github.com/galaxyproject/galaxy/issues/4960.
224+
max_tries = 60
225+
while max_tries > 0 and job.job_runner_external_id is None or job.state != expected:
226+
sa_session.refresh(job)
227+
time.sleep(1)
228+
max_tries -= 1
229+
219230
@skip_without_tool('cat_data_and_sleep')
220231
def test_kill_process(self):
221232
with self.dataset_populator.test_history() as history_id:
@@ -234,22 +245,12 @@ def test_kill_process(self):
234245

235246
app = self._app
236247
sa_session = app.model.context.current
237-
external_id = None
238-
state = False
239-
240-
job = sa_session.query(app.model.Job).filter_by(tool_id="cat_data_and_sleep").one()
241-
# Not checking the state here allows the change from queued to running to overwrite
242-
# the change from queued to deleted_new in the API thread - this is a problem because
243-
# the job will still run. See issue https://github.com/galaxyproject/galaxy/issues/4960.
244-
max_tries = 60
245-
while max_tries > 0 and external_id is None or state != app.model.Job.states.RUNNING:
246-
sa_session.refresh(job)
247-
assert not job.finished
248-
external_id = job.job_runner_external_id
249-
state = job.state
250-
time.sleep(1)
251-
max_tries -= 1
248+
job = sa_session.query(app.model.Job).get(app.security.decode_id(job_dict["id"]))
249+
250+
self._wait_for_external_state(sa_session, job, app.model.Job.states.RUNNING)
251+
assert not job.finished
252252

253+
external_id = job.job_runner_external_id
253254
output = unicodify(subprocess.check_output(['kubectl', 'get', 'job', external_id, '-o', 'json']))
254255
status = json.loads(output)
255256
assert status['status']['active'] == 1
@@ -264,6 +265,42 @@ def test_kill_process(self):
264265
subprocess.check_output(['kubectl', 'get', 'job', external_id, '-o', 'json'], stderr=subprocess.STDOUT)
265266
assert "not found" in unicodify(excinfo.value.output)
266267

268+
@skip_without_tool('cat_data_and_sleep')
269+
def test_external_job_delete(self):
270+
with self.dataset_populator.test_history() as history_id:
271+
hda1 = self.dataset_populator.new_dataset(history_id, content="1 2 3")
272+
running_inputs = {
273+
"input1": {"src": "hda", "id": hda1["id"]},
274+
"sleep_time": 240,
275+
}
276+
running_response = self.dataset_populator.run_tool(
277+
"cat_data_and_sleep",
278+
running_inputs,
279+
history_id,
280+
assert_ok=False,
281+
)
282+
job_dict = running_response.json()["jobs"][0]
283+
284+
app = self._app
285+
sa_session = app.model.context.current
286+
job = sa_session.query(app.model.Job).get(app.security.decode_id(job_dict["id"]))
287+
288+
self._wait_for_external_state(sa_session, job, app.model.Job.states.RUNNING)
289+
290+
external_id = job.job_runner_external_id
291+
output = unicodify(subprocess.check_output(['kubectl', 'get', 'job', external_id, '-o', 'json']))
292+
status = json.loads(output)
293+
assert status['status']['active'] == 1
294+
295+
output = unicodify(subprocess.check_output(['kubectl', 'delete', 'job', external_id, '-o', 'name']))
296+
assert 'job.batch/%s' % external_id in output
297+
298+
result = self.dataset_populator.wait_for_tool_run(run_response=running_response, history_id=history_id,
299+
assert_ok=False).json()
300+
details = self.dataset_populator.get_job_details(result['jobs'][0]['id'], full=True).json()
301+
302+
assert details['state'] == app.model.Job.states.ERROR, details
303+
267304
@skip_without_tool('job_properties')
268305
def test_exit_code_127(self):
269306
inputs = {

0 commit comments

Comments
 (0)