Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pass allowed query parameters to spawner via user options #805

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions binderhub/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import tornado.log
from tornado.log import app_log
import tornado.web
from traitlets import Unicode, Integer, Bool, Dict, validate, TraitError, default
from traitlets import Unicode, Integer, Bool, Dict, validate, TraitError, default, Set
from traitlets.config import Application
from jupyterhub.services.auth import HubOAuthCallbackHandler

Expand Down Expand Up @@ -402,6 +402,16 @@ def _template_path_default(self):
config=True,
)

query_parameter_names = Set(
trait=Unicode(),
default_value=set(),
help="""
List of allowed names. Before launch, BinderHub checks if they exist in the query and
pass found ones with their value to spawner's user_options.
""",
config=True
)

@staticmethod
def add_url_prefix(prefix, handlers):
"""add a url prefix to handlers"""
Expand Down Expand Up @@ -512,7 +522,8 @@ def initialize(self, *args, **kwargs):
'executor': self.executor,
'auth_enabled': self.auth_enabled,
'use_named_servers': self.use_named_servers,
'event_log': self.event_log
'event_log': self.event_log,
'query_parameter_names': self.query_parameter_names
})
if self.auth_enabled:
self.tornado_settings['cookie_secret'] = os.urandom(32)
Expand Down
19 changes: 18 additions & 1 deletion binderhub/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,22 @@ async def launch(self, kube):
log("Launching pod for %s: %s other pods running this repo (%s total)",
self.repo_url, matching_pods, total_pods)

# get query parameters
user_options = {}
options_message = []
for name in self.settings['query_parameter_names']:
value = self.get_query_argument(name, None)
if value is not None:
user_options[name] = value
m = f"Passing option {name} with value {value} to spawner"
app_log.info(m)
options_message.append(m)
if options_message:
await self.emit({
'phase': 'launching',
'message': '\n'.join(options_message)+'\n',
})

await self.emit({
'phase': 'launching',
'message': 'Launching server...\n',
Expand All @@ -494,7 +510,8 @@ async def launch(self, kube):
server_name = ''
try:
server_info = await launcher.launch(image=self.image_name, username=username,
server_name=server_name, repo_url=self.repo_url)
server_name=server_name, repo_url=self.repo_url,
user_options=user_options)
LAUNCH_TIME.labels(
status='success', retries=i,
).observe(time.perf_counter() - launch_starttime)
Expand Down
23 changes: 13 additions & 10 deletions binderhub/launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,16 +117,14 @@ def unique_name_from_repo(self, repo_url):
# add a random suffix to avoid collisions for users on the same image
return '{}-{}'.format(prefix, ''.join(random.choices(SUFFIX_CHARS, k=SUFFIX_LENGTH)))

async def launch(self, image, username, server_name='', repo_url=''):
async def launch(self, image, username, server_name='', repo_url='', user_options=None):
"""Launch a server for a given image

- creates a temporary user on the Hub if authentication is not enabled
- spawns a server for temporary/authenticated user
- spawns a server for temporary/authenticated user with user options
- generates a token
- returns a dict containing:
- `url`: the URL of the server
- `image`: image spec
- `repo_url`: the url of the repo
- `token`: the token for the server
"""
# TODO: validate the image argument?
Expand All @@ -153,10 +151,13 @@ async def launch(self, image, username, server_name='', repo_url=''):
raise web.HTTPError(409, "User %s already has a running server." % username)

# data to be passed into spawner's user_options during launch
# and also to be returned into 'ready' state
data = {'image': image,
'repo_url': repo_url,
'token': base64.urlsafe_b64encode(uuid.uuid4().bytes).decode('ascii').rstrip('=\n')}
_user_options = {'image': image,
'repo_url': repo_url,
'token': base64.urlsafe_b64encode(uuid.uuid4().bytes).decode('ascii').rstrip('=\n')}
if user_options is None:
user_options = _user_options
else:
user_options.update(_user_options)

# server name to be used in logs
_server_name = " {}".format(server_name) if server_name else ''
Expand All @@ -167,7 +168,7 @@ async def launch(self, image, username, server_name='', repo_url=''):
resp = await self.api_request(
'users/{}/servers/{}'.format(username, server_name),
method='POST',
body=json.dumps(data).encode('utf8'),
body=json.dumps(user_options).encode('utf8'),
)
if resp.code == 202:
# Server hasn't actually started yet
Expand Down Expand Up @@ -196,5 +197,7 @@ async def launch(self, image, username, server_name='', repo_url=''):
format(_server_name, username, e, body))
raise web.HTTPError(500, "Failed to launch image %s" % image)

data['url'] = self.hub_url + 'user/%s/%s' % (username, server_name)
# data to be returned into 'ready' state
data = {'url': self.hub_url + 'user/%s/%s' % (username, server_name),
'token': user_options['token']}
return data
17 changes: 11 additions & 6 deletions binderhub/static/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ function updateRepoText() {

function getBuildFormValues() {
var providerPrefix = $('#provider_prefix').val().trim();
var userOptions = $('#user_options').val().trim();
var repo = $('#repository').val().trim();
if (providerPrefix !== 'git') {
repo = repo.replace(/^(https?:\/\/)?github.com\//, '');
Expand All @@ -92,7 +93,8 @@ function getBuildFormValues() {
var ref = $('#ref').val().trim() || 'master';
var path = $('#filepath').val().trim();
return {'providerPrefix': providerPrefix, 'repo': repo,
'ref': ref, 'path': path, 'pathType': getPathType()}
'ref': ref, 'path': path, 'pathType': getPathType(),
'userOptions': userOptions}
}

function updateUrls(formValues) {
Expand Down Expand Up @@ -121,7 +123,7 @@ function updateUrls(formValues) {
}
}

function build(providerSpec, log, path, pathType) {
function build(providerSpec, log, path, pathType, userOptions) {
update_favicon(BASE_URL + "favicon_building.ico");
// split provider prefix off of providerSpec
var spec = providerSpec.slice(providerSpec.indexOf('/') + 1);
Expand All @@ -139,7 +141,7 @@ function build(providerSpec, log, path, pathType) {

$('.on-build').removeClass('hidden');

var image = new BinderImage(providerSpec);
var image = new BinderImage();

image.onStateChange('*', function(oldState, newState, data) {
if (data.message !== undefined) {
Expand Down Expand Up @@ -195,7 +197,8 @@ function build(providerSpec, log, path, pathType) {
image.launch(data.url, data.token, path, pathType);
});

image.fetch();
var apiUrl = BASE_URL + "build/" + providerSpec + userOptions;
image.fetch(apiUrl);
return image;
}

Expand Down Expand Up @@ -288,7 +291,8 @@ function indexMain() {
formValues.providerPrefix + '/' + formValues.repo + '/' + formValues.ref,
log,
formValues.path,
formValues.pathType
formValues.pathType,
formValues.userOptions
);
return false;
});
Expand All @@ -310,7 +314,8 @@ function loadingMain(providerSpec) {
pathType = 'file';
}
}
build(providerSpec, log, path, pathType);
var userOptions = window.location.search;
build(providerSpec, log, path, pathType, userOptions);
return false;
}

Expand Down
8 changes: 2 additions & 6 deletions binderhub/static/js/src/image.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
var BASE_URL = $("#base-url").data().url;

export default function BinderImage(providerSpec) {
this.providerSpec = providerSpec;
export default function BinderImage() {
this.callbacks = {};
this.state = null;
}

BinderImage.prototype.fetch = function() {
var apiUrl = BASE_URL + "build/" + this.providerSpec;
BinderImage.prototype.fetch = function(apiUrl) {
this.eventSource = new EventSource(apiUrl);
var that = this;
this.eventSource.onerror = function(err) {
Expand Down
1 change: 1 addition & 0 deletions binderhub/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ <h3>Turn a Git repo into a collection of interactive notebooks</h3>
<form id="build-form" class="form jumbotron">
<h4 id="form-header" class='row'>Build and launch a repository</h4>
<input type="hidden" id="provider_prefix" value="gh"/>
<input type="hidden" id="user_options" value=""/>
<div class="form-group row">
<label for="repository">GitHub repo or Gist name or GitLab.com URL</label>
<div class="input-group">
Expand Down