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

custom images for interactive environments #1109

Merged
merged 10 commits into from
Dec 4, 2020
6 changes: 4 additions & 2 deletions client/src/api-client/notebook-servers.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,17 +76,19 @@ function addNotebookServersMethods(client) {
});
};

client.startNotebook = (namespacePath, projectPath, branchName, commitId, options) => {
client.startNotebook = (namespacePath, projectPath, branchName, commitId, image, options) => {
const headers = client.getBasicHeaders();
headers.append("Content-Type", "application/json");
const url = `${client.baseUrl}/notebooks/servers`;
const parameters = {
let parameters = {
namespace: decodeURIComponent(namespacePath),
project: projectPath,
commit_sha: commitId,
branch: branchName,
...options
};
if (image)
parameters.image = image;

return client.clientFetch(url, {
method: "POST",
Expand Down
2 changes: 1 addition & 1 deletion client/src/api-client/repository.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ function addRepositoryMethods(client) {


// TODO: Merge to following methods into one
client.getRepositoryFile = (projectId, path, ref = "master", encoding = "base64") => {
client.getRepositoryFile = async (projectId, path, ref = "master", encoding = "base64") => {
let headers = client.getBasicHeaders();
const pathEncoded = encodeURIComponent(path);
const raw = encoding === "raw" ? "/raw" : "";
Expand Down
2 changes: 1 addition & 1 deletion client/src/notebooks/Notebooks.container.js
Original file line number Diff line number Diff line change
Expand Up @@ -305,8 +305,8 @@ class StartNotebookServer extends Component {

async refreshPipelines() {
if (this._isMounted) {
await this.coordinator.fetchNotebookOptions();
await this.coordinator.startPipelinePolling();
this.coordinator.fetchNotebookOptions();
}
}

Expand Down
166 changes: 128 additions & 38 deletions client/src/notebooks/Notebooks.present.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import Media from "react-media";
import { Link } from "react-router-dom";
import {
Form, FormGroup, FormText, Label, Input, Button, ButtonGroup, Row, Col, Table, DropdownItem, UncontrolledTooltip,
UncontrolledPopover, PopoverHeader, PopoverBody, Badge, Modal, ModalHeader, ModalBody, ModalFooter, CustomInput
UncontrolledPopover, PopoverHeader, PopoverBody, Badge, Modal, ModalHeader, ModalBody, ModalFooter, CustomInput,
Collapse
} from "reactstrap";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faStopCircle, faExternalLinkAlt, faInfoCircle, faSyncAlt } from "@fortawesome/free-solid-svg-icons";
Expand Down Expand Up @@ -178,6 +179,7 @@ class NotebookServersList extends Component {
standalone={this.props.standalone}
annotations={validAnnotations}
resources={resources}
image={this.props.servers[k].image}
name={this.props.servers[k].name}
startTime={startTime}
status={this.props.servers[k].status}
Expand Down Expand Up @@ -266,7 +268,8 @@ class NotebookServerRow extends Component {
const commitDetails = this.props.commits[annotations["commit-sha"]] ?
this.props.commits[annotations["commit-sha"]] :
null;
const newProps = { annotations, status, details, uid, resources, repositoryLinks, commitDetails };
const image = this.props.image;
const newProps = { annotations, status, details, uid, resources, repositoryLinks, commitDetails, image };

return (
<Media query={Sizes.md}>
Expand Down Expand Up @@ -338,14 +341,16 @@ class NotebookServerRowCommitInfo extends Component {
class NotebookServerRowFull extends Component {
render() {
const {
annotations, details, status, url, uid, resources, repositoryLinks, name, commitDetails, fetchCommit
annotations, details, status, url, uid, resources, repositoryLinks, name, commitDetails, fetchCommit, image
} = this.props;

const icon = <td className="align-middle">
<NotebooksServerRowStatusIcon details={details} status={status} uid={uid} />
<NotebooksServerRowStatusIcon
details={details} status={status} uid={uid} image={image} annotations={annotations}
/>
</td>;
const project = this.props.standalone ?
(<td className="align-middle"><NotebookServerRowProject annotations={this.props.annotations} /></td>) :
(<td className="align-middle"><NotebookServerRowProject annotations={annotations} /></td>) :
null;
const branch = (<td className="align-middle">
<ExternalLink url={repositoryLinks.branch} title={annotations["branch"]} role="text" />
Expand All @@ -359,7 +364,9 @@ class NotebookServerRowFull extends Component {
});
const resourceObject = (<td>{resourceList}</td>);
const statusOut = (<td className="align-middle">
<NotebooksServerRowStatus details={details} status={status} uid={uid} startTime={this.props.startTime} />
<NotebooksServerRowStatus
details={details} status={status} uid={uid} startTime={this.props.startTime} annotations={annotations}
/>
</td>);
const action = (<td className="align-middle">
<NotebookServerRowAction
Expand Down Expand Up @@ -395,11 +402,13 @@ class NotebookServerRowFull extends Component {
class NotebookServerRowCompact extends Component {
render() {
const {
annotations, details, status, url, uid, resources, repositoryLinks, name, commitDetails, fetchCommit
annotations, details, status, url, uid, resources, repositoryLinks, name, commitDetails, fetchCommit, image
} = this.props;

const icon = <span>
<NotebooksServerRowStatusIcon details={details} status={status} uid={uid} />
<NotebooksServerRowStatusIcon
details={details} status={status} uid={uid} image={image} annotations={annotations}
/>
</span>;
const project = this.props.standalone ?
(<Fragment>
Expand Down Expand Up @@ -436,7 +445,9 @@ class NotebookServerRowCompact extends Component {
details={details}
status={status}
uid={uid}
startTime={this.props.startTime} />
startTime={this.props.startTime}
annotations={annotations}
/>
</span>);
const action = (<span>
<NotebookServerRowAction
Expand Down Expand Up @@ -472,12 +483,16 @@ class NotebookServerRowCompact extends Component {
}
}

function getStatusObject(status) {
function getStatusObject(status, defaultImage) {
switch (status) {
case "running":
return {
color: "success",
icon: <FontAwesomeIcon icon={faCheckCircle} size="lg" />,
color: defaultImage ?
"warning" :
"success",
icon: defaultImage ?
(<FontAwesomeIcon icon={faExclamationTriangle} inverse={true} size="lg" />) :
(<FontAwesomeIcon icon={faCheckCircle} size="lg" />),
text: "Running"
};
case "pending":
Expand All @@ -503,8 +518,8 @@ function getStatusObject(status) {

class NotebooksServerRowStatus extends Component {
render() {
const { status, details, uid } = this.props;
const data = getStatusObject(status);
const { status, details, uid, annotations } = this.props;
const data = getStatusObject(status, annotations.default_image_used);
const spacing = this.props.spaced ?
" " :
(<br />);
Expand All @@ -528,14 +543,31 @@ class NotebooksServerRowStatus extends Component {

class NotebooksServerRowStatusIcon extends Component {
render() {
const { status } = this.props;
const data = getStatusObject(status);
const { status, uid, image, annotations } = this.props;
const data = getStatusObject(status, annotations.default_image_used);
const classes = this.props.spaced ?
"text-nowrap p-1 mb-2" :
"text-nowrap p-1";
const id = `${uid}-status`;
const policy = annotations.default_image_used ?
(<span><br /><span className="font-weight-bold">Warning:</span> a fallback image was used.</span>) :
null;

const popover = !image || status !== "running" ?
null :
(
<UncontrolledPopover target={id} trigger="legacy" placement="bottom">
<PopoverHeader>Details</PopoverHeader>
<PopoverBody>
<span className="font-weight-bold">Image source:</span> {image}
{policy}
</PopoverBody>
</UncontrolledPopover>
);

return (<div>
<Badge color={data.color} className={classes}>{data.icon}</Badge>
<Badge id={id} color={data.color} className={classes}>{data.icon}</Badge>
{popover}
</div>);
}
}
Expand Down Expand Up @@ -705,18 +737,19 @@ class StartNotebookServer extends Component {
const { branch, commit } = this.props.filters;
const { branches } = this.props.data;
const { pipelines, message } = this.props;
const projectOptions = this.props.options.project;
const fetching = {
branches: StatusHelper.isUpdating(branches) ? true : false,
pipelines: pipelines.fetching,
commits: this.props.data.fetching
};
const anyPipeline = this.props.justStarted || this.state.ignorePipeline || pipelineAvailable(pipelines);
const noPipelinesNeeded = projectOptions && projectOptions.image;

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

const messageOutput = message ?
(<div key="message">{message}</div>) :
Expand Down Expand Up @@ -866,7 +899,7 @@ class StartNotebookBranchesOptions extends Component {
class StartNotebookPipelines extends Component {
constructor(props) {
super(props);
this.state = { justTriggered: false };
this.state = { justTriggered: false, showInfo: false };
}

async reTriggerPipeline() {
Expand All @@ -875,16 +908,33 @@ class StartNotebookPipelines extends Component {
this.setState({ justTriggered: false });
}

toggleInfo() {
this.setState({ showInfo: !this.state.showInfo });
}

render() {
if (!this.props.pipelines.fetched)
return (<Label>Checking Docker image status... <Loader size="14" inline="true" /></Label>);
if (this.state.justTriggered)
return (<Label>Triggering Docker image build... <Loader size="14" inline="true" /></Label>);

const customImage = this.props.pipelines.type === NotebooksHelper.pipelineTypes.customImage ?
true :
false;
const { showInfo } = this.state;
let infoButton = null;
if (customImage) {
const text = showInfo ?
"less info" :
"more info";
infoButton = (<Button size="sm" onClick={() => { this.toggleInfo(); }} color="link">{text}</Button>);
}
return (
<FormGroup>
<StartNotebookPipelinesBadge {...this.props} />
<StartNotebookPipelinesContent {...this.props} buildAgain={this.reTriggerPipeline.bind(this)} />
<StartNotebookPipelinesBadge {...this.props} infoButton={infoButton} />
<Collapse isOpen={!customImage || showInfo}>
<StartNotebookPipelinesContent {...this.props} buildAgain={this.reTriggerPipeline.bind(this)} />
</Collapse>
</FormGroup>
);
}
Expand All @@ -894,6 +944,7 @@ class StartNotebookPipelinesBadge extends Component {
render() {
const pipelineType = this.props.pipelines.type;
const pipeline = this.props.pipelines.main;
const { infoButton } = this.props;

let color, text;
if (pipelineType === NotebooksHelper.pipelineTypes.logged) {
Expand Down Expand Up @@ -924,12 +975,16 @@ class StartNotebookPipelinesBadge extends Component {
text = "not available";
}
}
else if (pipelineType === NotebooksHelper.pipelineTypes.customImage) {
color = "primary";
text = "pinned";
}
else {
color = "danger";
text = "error";
}

return (<p>Docker Image <Badge color={color}>{text}</Badge></p>);
return (<p>Docker Image <Badge color={color}>{text}</Badge>{infoButton}</p>);
}
}

Expand All @@ -939,6 +994,28 @@ class StartNotebookPipelinesContent extends Component {
const pipelineType = this.props.pipelines.type;
const { pipelineTypes } = NotebooksHelper;

// customImage
if (pipelineType === pipelineTypes.customImage) {
const projectOptions = this.props.options.project;
if (!projectOptions || !projectOptions.image)
return null;

// this style trick makes it appear as the other Label + Input components
const style = { marginTop: -8 };
const url = "https://renku.readthedocs.io/en/latest/user/templates.html?highlight=.dockerignore#renku";
return (
<Fragment>
<Input type="input" disabled={true} id="customImage" style={style} value={projectOptions.image}></Input>
<FormText>
<FontAwesomeIcon className="no-pointer" icon={faInfoCircle} /> This project specifies
a <ExternalLink role="text" iconSup={true} iconAfter={true} url={url} title="pinned image" />. A
pinned image has advantages for projects with many forks, but it will not reflect changes
to the <code>Dockerfile</code> or any project dependency files.
</FormText>
</Fragment>
);
}

// anonymous
if (pipelineType === pipelineTypes.anonymous) {
if (pipeline && pipeline.path)
Expand Down Expand Up @@ -1256,12 +1333,14 @@ class StartNotebookServerOptions extends Component {
const onChange = (event, value) => {
this.props.handlers.setServerOption(key, event, value);
};
const warning = !warnings.includes(key)
? null
: <Warning>
Cannot set <b>{serverOption.displayName}</b> to
the project default value <i>{projectOptions[key]}</i> in this Renkulab deployment.
</Warning>;
const warning = warnings.includes(key) ?
(
<Warning>
Cannot set <b>{serverOption.displayName}</b> to
the project default value <i>{projectOptions[key]}</i> in this Renkulab deployment.
</Warning>
) :
null;

switch (serverOption.type) {
case "enum": {
Expand Down Expand Up @@ -1296,14 +1375,25 @@ class StartNotebookServerOptions extends Component {
});

const unmatchedWarnings = warnings.filter(x => !sortedOptionKeys.includes(x));
const globalWarning = unmatchedWarnings && unmatchedWarnings.length
? <Warning key="globalWarning">
Project environment default contains
variable{unmatchedWarnings.length > 1 ? "s" : ""} {
unmatchedWarnings.map((w, i) => <span key={i}>&ldquo;{w}&rdquo;, </span>)}
which {unmatchedWarnings.length > 1 ? "are" : "is"} not known in this Renkulab deployment.
</Warning>
: null;
let globalWarning = null;
if (unmatchedWarnings && unmatchedWarnings.length) {
const language = unmatchedWarnings.length > 1 ?
{ verb: "", plural: "s", aux: "are", article: "" } :
{ verb: "s", plural: "", aux: "is", article: "a " };
const wrongVariables = unmatchedWarnings.map((w, i) => (
<span key={i}><i>{w}</i>: <code>{projectOptions[w].toString()}</code><br /></span>
));

globalWarning = (
<Warning key="globalWarning">
The project configuration for interactive environments
contains {language.article}variable{language.plural} that {language.aux} either
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

unknown in this Renkulab deployment or
contain{language.verb} {language.article}wrong value{language.plural}:
<br /> { wrongVariables}
</Warning>
);
}

return renderedServerOptions.length ?
renderedServerOptions.concat(globalWarning) :
Expand Down
Loading