Skip to content

Commit

Permalink
feat(environments): allow anonymous users to start environments
Browse files Browse the repository at this point in the history
Anonymous users can start an environment. They will get a temporary identity and they will see
warnings suggesting to log in.

BREAKING CHANGE: Requires backend components supporting anonymous users environments

fix #857
  • Loading branch information
lorenzo-cavazzi committed Apr 6, 2020
1 parent 0bad31a commit 081ce8c
Show file tree
Hide file tree
Showing 9 changed files with 309 additions and 103 deletions.
47 changes: 27 additions & 20 deletions src/api-client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ const ACCESS_LEVELS = {
OWNER: 50,
};

const FETCH_DEFAULT = {
options: { headers: new Headers() },
returnType: RETURN_TYPES.json,
alertOnErr: false,
reLogin: true,
anonymousLogin: false
};

class APIClient {

// GitLab api client for Renku. Note that we do some
Expand Down Expand Up @@ -76,49 +84,43 @@ class APIClient {
// contain a user token.
clientFetch(
url,
options = { headers: new Headers() },
returnType = RETURN_TYPES.json,
alertOnErr = false,
reLogin = true
options = FETCH_DEFAULT.options,
returnType = FETCH_DEFAULT.returnType,
alertOnErr = FETCH_DEFAULT.alertOnErr,
reLogin = FETCH_DEFAULT.reLogin,
anonymousLogin = FETCH_DEFAULT.anonymousLogin
) {

return renkuFetch(url, options)
.catch((error) => {

// For permission errors we send the user to login
if (reLogin && error.case === API_ERRORS.unauthorizedError)
if (reLogin && error.case === API_ERRORS.unauthorizedError) {
if (anonymousLogin)
return this.doAnonymousLogin();
return this.doLogin();


}
// Alert only if corresponding option is set to true
else if (alertOnErr)
else if (alertOnErr) {
alertAPIErrors(error);


}
// Default case: Re-raise the error for the application
// to take care of it.
else
else {
return Promise.reject(error);

}
})

.then(response => {
switch (returnType) {

case RETURN_TYPES.json:
return response.json().then(data => {
return {
data,
pagination: processPaginationHeaders(this, response.headers)
};
});

case RETURN_TYPES.text:
return response.text();

case RETURN_TYPES.full:
return response;

default:
return response;
}
Expand Down Expand Up @@ -151,6 +153,11 @@ class APIClient {
return fetch(urlObject, { headers, method });
}

doAnonymousLogin() {
window.location = `${this.baseUrl}/auth/jupyterhub/login-tmp` +
`?redirect-url=${encodeURIComponent(window.location.href)}`;
}

doLogin() {
window.location = `${this.baseUrl}/auth/login?redirect_url=${encodeURIComponent(window.location.href)}`;
}
Expand All @@ -168,4 +175,4 @@ class APIClient {
}

export default APIClient;
export { alertAPIErrors, APIError, ACCESS_LEVELS, API_ERRORS, testClient };
export { alertAPIErrors, APIError, ACCESS_LEVELS, API_ERRORS, FETCH_DEFAULT, testClient };
17 changes: 11 additions & 6 deletions src/api-client/notebook-servers.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
* limitations under the License.
*/

import { FETCH_DEFAULT } from "./index";

function addNotebookServersMethods(client) {
client.getNotebookServers = (namespace, project, branch, commit) => {
client.getNotebookServers = (namespace, project, branch, commit, anonymous = false) => {
const headers = client.getBasicHeaders();
const url = `${client.baseUrl}/notebooks/servers`;
let parameters = {};
Expand All @@ -26,11 +28,14 @@ function addNotebookServersMethods(client) {
if (branch) parameters.branch = branch;
if (commit) parameters.commit_sha = commit;

return client.clientFetch(url, {
method: "GET",
headers,
queryParams: parameters
}).then(resp => {
return client.clientFetch(
url,
{ method: "GET", headers, queryParams: parameters },
FETCH_DEFAULT.returnType,
FETCH_DEFAULT.alertOnErr,
FETCH_DEFAULT.reLogin,
anonymous
).then(resp => {
return { "data": resp.data.servers };
});
};
Expand Down
41 changes: 41 additions & 0 deletions src/api-client/pipeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
* limitations under the License.
*/

import { API_ERRORS } from "./errors";

function addPipelineMethods(client) {
client.runPipeline = (projectId) => {
const headers = client.getBasicHeaders();
Expand Down Expand Up @@ -63,6 +65,7 @@ function addPipelineMethods(client) {
method: "GET",
headers,
}).then(response => response.data);
//return Promise.resolve([{ id: projectId, "status": "success" }]);
};

/**
Expand All @@ -81,6 +84,44 @@ function addPipelineMethods(client) {
headers,
}).then(response => response.data);
};

/**
* Get the array of pipeline from GitLab
*
* @param {number|string} projectId - project id or slug
*/
client.getRegistries = (projectId) => {
const headers = client.getBasicHeaders();
headers.append("Content-Type", "application/json");
const url = `${client.baseUrl}/projects/${projectId}/registry/repositories`;

return client.clientFetch(url, {
method: "GET",
headers,
}).then(response => response.data);
};

/**
* Get the array of pipeline from GitLab
*
* @param {number|string} projectId - project id or slug
* @param {number} registryId - registry id
* @param {string} tag - tag name, our convention uses the first 7 chars from commit id
*/
client.getRegistryTag = (projectId, registryId, tag) => {
const headers = client.getBasicHeaders();
headers.append("Content-Type", "application/json");
const url = `${client.baseUrl}/projects/${projectId}/registry/repositories` +
`/${registryId}/tags/${tag}`;

return client.clientFetch(url, { method: "GET", headers })
.then(response => response.data)
.catch((error) => {
if (error.case === API_ERRORS.notFoundError)
return null;
throw error;
});
};
}

export default addPipelineMethods;
1 change: 1 addition & 0 deletions src/landing/NavBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ class AnonymousNavBar extends Component {
<ul className="navbar-nav mr-auto">
<RenkuNavLink to="/projects" title="Projects" />
<RenkuNavLink to="/datasets" title="Datasets" />
<RenkuNavLink to="/environments" title="Environments" />
</ul>
<ul className="navbar-nav">
<RenkuToolbarHelpMenu />
Expand Down
1 change: 1 addition & 0 deletions src/model/RenkuModels.js
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ const notebooksSchema = new Schema({
poller: { initial: null },
fetched: { initial: null },
fetching: { initial: false },
type: { initial: null },

lastParameters: { initial: null },
lastMainId: { initial: null },
Expand Down
6 changes: 4 additions & 2 deletions src/notebooks/Notebooks.container.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ class Notebooks extends Component {
constructor(props) {
super(props);
this.model = props.model.subModel("notebooks");
this.coordinator = new NotebooksCoordinator(props.client, this.model);
this.userModel = props.model.subModel("user");
this.coordinator = new NotebooksCoordinator(props.client, this.model, this.userModel);
// temporarily reset data since notebooks model was not designed to be static
this.coordinator.reset();

Expand Down Expand Up @@ -134,7 +135,8 @@ class StartNotebookServer extends Component {
constructor(props) {
super(props);
this.model = props.model.subModel("notebooks");
this.coordinator = new NotebooksCoordinator(props.client, this.model);
this.userModel = props.model.subModel("user");
this.coordinator = new NotebooksCoordinator(props.client, this.model, this.userModel);
// temporarily reset data since notebooks model was not designed to be static
this.coordinator.reset();

Expand Down
82 changes: 69 additions & 13 deletions src/notebooks/Notebooks.present.js
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,22 @@ class EnvironmentLogs extends Component {
}
}

function pipelineAvailable(pipelines) {
const { pipelineTypes } = NotebooksHelper;
const mainPipeline = pipelines.main;

if (pipelines.type === pipelineTypes.logged) {
if (mainPipeline.status === "success" || mainPipeline.status === undefined)
return true;
}
else if (pipelines.type === pipelineTypes.anonymous) {
if (mainPipeline && mainPipeline.path)
return true;
}

return false;
}

// * StartNotebookServer code * //
class StartNotebookServer extends Component {
constructor(props) {
Expand All @@ -604,13 +620,12 @@ class StartNotebookServer extends Component {
pipelines: pipelines.fetching,
commits: this.props.data.fetching
};

let show = {};
show.commits = !fetching.branches && branch.name ? true : false;
show.pipelines = show.commits && !fetching.commits && commit.id;
show.options = show.pipelines && pipelines.fetched && (
pipelines.main.status === "success" || pipelines.main.status === undefined
|| this.state.ignorePipeline
|| this.props.justStarted
this.props.justStarted || this.state.ignorePipeline || pipelineAvailable(pipelines)
);

const messageOutput = message ?
Expand Down Expand Up @@ -787,19 +802,37 @@ class StartNotebookPipelines extends Component {

class StartNotebookPipelinesBadge extends Component {
render() {
const pipelineType = this.props.pipelines.type;
const pipeline = this.props.pipelines.main;

let color, text;
if (pipeline.status === "success") {
color = "success";
text = "available";
}
else if (pipeline.status === undefined) {
color = "danger";
text = "not available";
if (pipelineType === NotebooksHelper.pipelineTypes.logged) {
if (pipeline.status === "success") {
color = "success";
text = "available";
}
else if (pipeline.status === undefined) {
color = "danger";
text = "not available";
}
else if (pipeline.status === "running" || pipeline.status === "pending") {
color = "warning";
text = "building";
}
else {
color = "danger";
text = "error";
}
}
else if (pipeline.status === "running" || pipeline.status === "pending") {
color = "warning";
text = "building";
else if (pipelineType === NotebooksHelper.pipelineTypes.anonymous) {
if (pipeline && pipeline.path) {
color = "success";
text = "available";
}
else {
color = "warning";
text = "pending";
}
}
else {
color = "danger";
Expand All @@ -813,6 +846,29 @@ class StartNotebookPipelinesBadge extends Component {
class StartNotebookPipelinesContent extends Component {
render() {
const pipeline = this.props.pipelines.main;
const pipelineType = this.props.pipelines.type;
const { pipelineTypes } = NotebooksHelper;

// unexpected case
if (pipelineType !== pipelineTypes.anonymous && pipelineType !== pipelineTypes.logged)
return (<div><p>Unexpected state.</p></div>);

// anonymous
if (pipelineType === pipelineTypes.anonymous) {
if (pipeline && pipeline.path)
return null;

return (
<div>
<Label>
<FontAwesomeIcon icon={faCog} spin /> The Docker image for the environment is not ready.
Please wait a moment...
</Label>
</div>
);
}

// logged in
if (pipeline.status === "success")
return null;

Expand Down
Loading

0 comments on commit 081ce8c

Please sign in to comment.