├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SageMaker-Studio-Docker-UI.png ├── THIRD-PARTY-LICENSES ├── jupyter-config └── sagemaker_studio_docker_ui.json ├── labextension └── sagemaker_studio_docker_ui │ └── package │ ├── lib │ ├── components │ │ ├── Alert.js │ │ ├── GetContainers.js │ │ ├── GetContexts.js │ │ ├── GetImages.js │ │ ├── SdockerPanel.js │ │ ├── SelectColumn.js │ │ ├── SwitchContext.js │ │ └── TerminateHost.js │ ├── index.js │ ├── sagemaker-studio-docker-ui.js │ ├── style │ │ ├── Alert.js │ │ ├── SdockerWidgetStyle.js │ │ ├── SelectColumn.js │ │ ├── SettingsPanel.js │ │ ├── Widget.js │ │ └── getContextsStyle.js │ └── widgets │ │ └── SdockerWidget.js │ ├── package.json │ └── style │ ├── container-icon.svg │ ├── docker-icon.svg │ ├── idle-host.svg │ ├── image-icon.svg │ ├── index.css │ ├── power-icon.svg │ └── running-host.svg ├── pyproject.toml ├── sagemaker_studio_docker_ui ├── __init__.py ├── _version.py ├── checkers.py ├── context_switcher.py ├── handlers.py ├── host_creator.py └── host_terminator.py ├── setup.py └── setup.sh /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [DEPRECATED] Sagemaker Studio Docker UI Extension - UI to manage Docker integration for SageMaker Studio 2 | **Refer to new [Local Mode](https://docs.aws.amazon.com/sagemaker/latest/dg/studio-updated-local.html) by SageMaker service for recommended alternative** 3 | This JupyterLab extension interacts with docker hosts and SageMaker Studio Docker CLI to provide customers with interactive UI to launch and manage docker hosts from within SageMaker Studio. 4 | 5 | ![image](https://github.com/aws-samples/sagemaker-studio-docker-ui-extension/raw/main/SageMaker-Studio-Docker-UI.png) 6 | 7 | This extension is composed of a Python package named `sagemaker-studio-docker-ui` 8 | for the server extension and a NPM package named `sagemaker-studio-docker-ui` 9 | for the frontend extension. 10 | 11 | ## Requirements 12 | * Requires [SageMaker Studio Docker CLI Extension](https://github.com/aws-samples/sagemaker-studio-docker-cli-extension) to be installed. 13 | * Compatible with *JupyterServer* running *Jupyter Lab* v1.0 or v3.0 14 | 15 | ## Installation Steps 16 | 17 | Use Studio LifeCycle configuration and attach it to *JupyterServer* app. 18 | ``` 19 | #!/bin/bash 20 | 21 | set -eux 22 | 23 | cd ~ 24 | if cd sagemaker-studio-docker-cli-extension 25 | then 26 | git reset --hard 27 | git pull 28 | else 29 | git clone https://github.com/aws-samples/sagemaker-studio-docker-cli-extension.git 30 | cd sagemaker-studio-docker-cli-extension 31 | fi 32 | nohup ./setup.sh > docker_setup.out 2>&1 & 33 | 34 | if cd ~/sagemaker-studio-docker-ui-extension 35 | then 36 | git reset --hard 37 | git pull 38 | cd 39 | else 40 | cd 41 | git clone https://github.com/aws-samples/sagemaker-studio-docker-ui-extension.git 42 | fi 43 | 44 | nohup ~/sagemaker-studio-docker-ui-extension/setup.sh > docker_setup.out 2>&1 & 45 | ``` 46 | 47 | ## Setting up proxy 48 | To setup a proxy through environment variables, use either `/home/sagemaker/.bash_profile` or `/home/sagemaker/.bashrc` to set any required environment variables. This extension will make sure to source one of these files if it was able to detect them before running any CLI commands. 49 | 50 | Below is an example of a script you can use in your life cycle configuration: 51 | ``` 52 | # Required to enable yum access proxy for JupyterServer App 53 | sudo bash -c "echo proxy=: >> /etc/yum.conf" 54 | 55 | # Required to enable both CLI and UI extension to access proxy for JupyterServer or KernelGateway 56 | cat > ~/.bash_profile << EOF 57 | export http_proxy=: 58 | export https_proxy=: 59 | export HTTPS_PROXY=: 60 | export HTTP_PROXY=: 61 | export NO_PROXY=127.0.0.1,localhost,169.254.169.254,.ec2.internal 62 | export no_proxy=127.0.0.1,localhost,169.254.169.254,.ec2.internal 63 | EOF 64 | 65 | source ~/.bash_profile 66 | 67 | # Required for installing docker on JupyterServer 68 | sudo -u root bash -c "cat > /root/.bash_profile << EOF 69 | export http_proxy=: 70 | export https_proxy=: 71 | export HTTPS_PROXY=: 72 | export HTTP_PROXY=: 73 | export NO_PROXY=127.0.0.1,localhost,169.254.169.254,.ec2.internal 74 | export no_proxy=127.0.0.1,localhost,169.254.169.254,.ec2.internal 75 | EOF" 76 | ``` 77 | 78 | ## Troubleshooting 79 | This extension logs its activity to `/home/sagemaker-user/.sagemaker_studio_docker_cli/ui_extension.log` 80 | 81 | ## Security 82 | 83 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 84 | 85 | ## License 86 | 87 | This project is licensed under the MIT-0 License. 88 | -------------------------------------------------------------------------------- /SageMaker-Studio-Docker-UI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/sagemaker-studio-docker-ui-extension/2d663c2dc44c6648bf954629d9627c7d67295f8d/SageMaker-Studio-Docker-UI.png -------------------------------------------------------------------------------- /THIRD-PARTY-LICENSES: -------------------------------------------------------------------------------- 1 | Amazon Sagemaker Studio Docker UI Extension Product includes the following third-party software/licensing: 2 | 3 | Copyright (c) 2017, Project Jupyter 4 | All rights reserved. 5 | Distributed under the terms of the Modified BSD License. 6 | 7 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 8 | 9 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 10 | 11 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 12 | 13 | Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /jupyter-config/sagemaker_studio_docker_ui.json: -------------------------------------------------------------------------------- 1 | { 2 | "NotebookApp": { 3 | "nbserver_extensions": { 4 | "sagemaker_studio_docker_ui": true 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /labextension/sagemaker_studio_docker_ui/package/lib/components/Alert.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { alert, alertDanger, alertWarning, alertInfo, alertSuccess, } from "../style/Alert"; 3 | export class Alert extends React.Component { 4 | render() { 5 | return (React.createElement("div", { className: `${alert} ${this.alertClass(this.props.type)}` }, this.props.message)); 6 | } 7 | alertClass(type) { 8 | const classes = { 9 | error: alertDanger, 10 | alert: alertWarning, 11 | notice: alertInfo, 12 | success: alertSuccess, 13 | }; 14 | return classes[type] || classes.success; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /labextension/sagemaker_studio_docker_ui/package/lib/components/GetContainers.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { requestAPIServer } from "../sagemaker-studio-docker-ui"; 3 | import { 4 | defaultHeight, 5 | itemIconSpan, 6 | instanceDescriptionDiv, 7 | containerIconStyle 8 | } from '../style/getContextsStyle'; 9 | 10 | export async function getContainers(instance){ 11 | // console.log("Checking containers list"); 12 | var contexts = []; 13 | try { 14 | const reply = await requestAPIServer("containers", { 15 | method: "GET", 16 | }); 17 | contexts = reply 18 | } 19 | catch (reason) { 20 | console.error(`Error on GET /docker-host/containers.\n${reason}`); 21 | instance.addAlert({ 22 | type: "error", 23 | message: `Error checking containers list! "`, 24 | wait: 5000 25 | }); 26 | contexts = [] 27 | }; 28 | return contexts; 29 | } 30 | 31 | function getItem( 32 | container, 33 | iconClass = '', 34 | turnOffHeightStyle = false 35 | ){ 36 | const ITEM_CLASS = 'jp-RunningSessions-item'; 37 | const SHUTDOWN_BUTTON_CLASS = 'jp-RunningSessions-itemShutdown'; 38 | return React.createElement( 39 | "div", 40 | { 41 | className: 'jp-RunningSessions-sectionContainer' 42 | }, 43 | React.createElement( 44 | "ul", 45 | { className: "jp-RunningSessions-sectionList" }, 46 | React.createElement( 47 | "li", 48 | { 49 | className: `${ITEM_CLASS} ${turnOffHeightStyle ? defaultHeight : ''}` 50 | }, 51 | React.createElement( 52 | "span", 53 | { 54 | className: `${itemIconSpan} jp-RunningSessions-itemIcon ${iconClass}` 55 | } 56 | ), 57 | React.createElement( 58 | "span", 59 | { 60 | className: instanceDescriptionDiv 61 | }, 62 | container 63 | ) 64 | ) 65 | ) 66 | ); 67 | } 68 | 69 | export function getContainerList(instance, turnOffHeightStyle = false){ 70 | const ItemList = getContainers(instance) 71 | .then(res => { 72 | const ItemList = res.length > 0 ? res.map(containers => { 73 | return getItem( 74 | containers['Names'][0].slice(1), 75 | containerIconStyle, 76 | turnOffHeightStyle 77 | )} 78 | ) : null; 79 | return ItemList 80 | }); 81 | return ItemList; 82 | } -------------------------------------------------------------------------------- /labextension/sagemaker_studio_docker_ui/package/lib/components/GetContexts.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { requestAPIServer } from "../sagemaker-studio-docker-ui"; 3 | import { ToolbarButtonComponent } from '@jupyterlab/apputils'; 4 | import { 5 | defaultHeight, 6 | itemIconSpan, 7 | instanceDescriptionDiv, 8 | runningHostStyle, 9 | idleHostStyle, 10 | powerIconStyle 11 | } from '../style/getContextsStyle'; 12 | import { ContextSwitcher } from './SwitchContext'; 13 | import { HostTerminator } from './TerminateHost'; 14 | 15 | export async function getContexts(instance){ 16 | //console.log("Checking contexts list"); 17 | var contexts = []; 18 | try { 19 | const reply = await requestAPIServer("contexts", { 20 | method: "GET", 21 | }); 22 | contexts = reply 23 | } 24 | catch (reason) { 25 | console.error(`Error on GET /docker-host/contexts.\n${reason}`); 26 | instance.addAlert({ 27 | type: "error", 28 | message: `Error checking contexts list! "`, 29 | wait: 5000 30 | }); 31 | contexts = [] 32 | }; 33 | return contexts; 34 | } 35 | 36 | function getItem( 37 | instance, 38 | instanceId, 39 | instanceType, 40 | turnOffHeightStyle = false, 41 | children, 42 | tooltip, 43 | current = false 44 | ){ 45 | const ITEM_CLASS = 'jp-RunningSessions-item'; 46 | const SHUTDOWN_BUTTON_CLASS = 'jp-RunningSessions-itemShutdown'; 47 | const switcherInstance = new ContextSwitcher(instance, instanceType, instanceId); 48 | const terminatorInstance = new HostTerminator(instance, instanceId); 49 | const secondCol = current? 50 | React.createElement( 51 | "span", 52 | { 53 | className: `${itemIconSpan} jp-RunningSessions-itemIcon ${runningHostStyle}` 54 | } 55 | ): 56 | React.createElement( 57 | ToolbarButtonComponent, 58 | { 59 | className: SHUTDOWN_BUTTON_CLASS, 60 | tooltip: 'Set as default docker host', 61 | iconClass: idleHostStyle, 62 | onClick: switcherInstance.switcher 63 | } 64 | ); 65 | return ( 66 | React.createElement( 67 | "li", 68 | { 69 | className: `${ITEM_CLASS} ${turnOffHeightStyle ? defaultHeight : ''}` 70 | }, 71 | secondCol, 72 | children, 73 | React.createElement( 74 | "span", 75 | { 76 | className: instanceDescriptionDiv 77 | }, 78 | instanceId 79 | ), 80 | React.createElement( 81 | "span", 82 | { 83 | className: instanceDescriptionDiv 84 | }, 85 | instanceType 86 | ), 87 | React.createElement( 88 | ToolbarButtonComponent, 89 | { 90 | className: SHUTDOWN_BUTTON_CLASS, 91 | tooltip: tooltip, 92 | iconClass: powerIconStyle, 93 | onClick: terminatorInstance.terminator 94 | } 95 | ) 96 | ) 97 | ); 98 | } 99 | 100 | export function getItemList(instance, turnOffHeightStyle = false){ 101 | const ItemList = getContexts(instance) 102 | .then( res => { 103 | const ItemList = res.length > 0 ? res.map(context => { 104 | return getItem( 105 | instance, 106 | context['InstanceId'], 107 | context['InstanceType'], 108 | turnOffHeightStyle, 109 | null, 110 | 'Shutdown docker host', 111 | context['Current']==='true'?true:false 112 | )} 113 | ) : null; 114 | return ItemList 115 | }); 116 | return ItemList 117 | } -------------------------------------------------------------------------------- /labextension/sagemaker_studio_docker_ui/package/lib/components/GetImages.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { requestAPIServer } from "../sagemaker-studio-docker-ui"; 3 | import { 4 | defaultHeight, 5 | itemIconSpan, 6 | instanceDescriptionDiv, 7 | imageIconStyle 8 | } from '../style/getContextsStyle'; 9 | 10 | export async function getImages(instance){ 11 | // console.log("Checking images list"); 12 | var images= []; 13 | try { 14 | const reply = await requestAPIServer("images", { 15 | method: "GET", 16 | }); 17 | images = reply 18 | } 19 | catch (reason) { 20 | console.error(`Error on GET /docker-host/images.\n${reason}`); 21 | instance.addAlert({ 22 | type: "error", 23 | message: `Error checking images list! "`, 24 | wait: 5000 25 | }); 26 | images = [] 27 | }; 28 | return images; 29 | } 30 | 31 | function getItem( 32 | image, 33 | iconClass = '', 34 | turnOffHeightStyle = false 35 | ){ 36 | const ITEM_CLASS = 'jp-RunningSessions-item'; 37 | const SHUTDOWN_BUTTON_CLASS = 'jp-RunningSessions-itemShutdown'; 38 | return React.createElement( 39 | "div", 40 | { 41 | className: 'jp-RunningSessions-sectionContainer' 42 | }, 43 | React.createElement( 44 | "ul", 45 | { 46 | className: "jp-RunningSessions-sectionList" 47 | }, 48 | React.createElement( 49 | "li", 50 | { 51 | className: `${ITEM_CLASS} ${turnOffHeightStyle ? defaultHeight : ''}` 52 | }, 53 | React.createElement( 54 | "span", 55 | { 56 | className: `${itemIconSpan} jp-RunningSessions-itemIcon ${iconClass}` 57 | } 58 | ), 59 | React.createElement( 60 | "span", 61 | { 62 | className: instanceDescriptionDiv 63 | }, 64 | image 65 | ) 66 | ) 67 | ) 68 | ); 69 | } 70 | 71 | export function getImageList(instance, turnOffHeightStyle = false){ 72 | const ItemList = getImages(instance) 73 | .then(res => { 74 | const ItemList = res.length > 0 ? res.map(images => { 75 | return getItem( 76 | images['RepoTags'], 77 | imageIconStyle, 78 | turnOffHeightStyle 79 | )} 80 | ) : null; 81 | return ItemList 82 | }); 83 | return ItemList; 84 | } -------------------------------------------------------------------------------- /labextension/sagemaker_studio_docker_ui/package/lib/components/SdockerPanel.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { requestAPIServer } from "../sagemaker-studio-docker-ui"; 3 | import { 4 | jpRunningSectionClass, 5 | jpRunningSectionHeader, 6 | runSidebarSectionClass, 7 | sidebarButtonClass, 8 | alertAreaClass 9 | } from "../style/SettingsPanel"; 10 | import { SelectColumn, LabeledTextInput } from "./SelectColumn"; 11 | import { getItemList } from "./GetContexts"; 12 | import { getImageList } from "./GetImages"; 13 | import { getContainerList } from "./GetContainers"; 14 | import { Alert } from "./Alert"; 15 | const KEY = "sagemaker-studio-docker-ui:settings:data"; 16 | function isPromise(p) { 17 | if (p === null) { return false; }; 18 | if (typeof p === 'object' && typeof p.then === 'function') { 19 | return true; 20 | } 21 | return false; 22 | } 23 | /** A React component for the sagemaker_studio_docker_ui extension's main display */ 24 | export class SdockerPanel extends React.Component { 25 | constructor(props) { 26 | super(props); 27 | this.state = { 28 | contextList: null, 29 | imageList: null, 30 | containerList: null, 31 | }; 32 | /** 33 | * Renders the component. 34 | * 35 | * @returns React element 36 | */ 37 | this.render = () => { 38 | const notebookIndependent = React.createElement("div", null, this.renderViewButtons()); 39 | return React.createElement("div", null, notebookIndependent); 40 | }; 41 | this.onInstanceTypeChange = (event) => { 42 | this.setState({ INSTANCE_TYPE: event.target.value }, () => this.saveState()); 43 | }; 44 | this.handleSubmit = async () => { 45 | console.log("Creating new docker host"); 46 | const dataToSend = { instance_type: this.state.INSTANCE_TYPE }; 47 | try { 48 | const reply = await requestAPIServer("create_host", { 49 | body: JSON.stringify(dataToSend), 50 | method: "POST", 51 | }); 52 | console.log(reply); 53 | this.addAlert({ message: `Creating new docker host - instance will appear once it is healthy, might take few minutes`, wait: 15000 }); 54 | } 55 | catch (reason) { 56 | console.error(`Error on POST /docker-host/create_host ${dataToSend}.\n${reason}`); 57 | this.addAlert({ 58 | type: "error", 59 | message: `Error creating new docker host! "`, 60 | wait: 5000 61 | }); 62 | } 63 | }; 64 | this.alertKey = 0; 65 | this.state = { 66 | INSTANCE_TYPE: "m5.large", 67 | alerts: [], 68 | }; 69 | this.loadState(); 70 | this.instanceTypes = [ 71 | "t2.xlarge", 72 | "t2.medium", 73 | "t2.large", 74 | "t2.2xlarge", 75 | "m6gd.xlarge", 76 | "m6gd.large", 77 | "m6gd.8xlarge", 78 | "m6gd.4xlarge", 79 | "m6gd.2xlarge", 80 | "m6gd.16xlarge", 81 | "m6g.xlarge", 82 | "m6g.large", 83 | "m6g.8xlarge", 84 | "m6g.4xlarge", 85 | "m6g.2xlarge", 86 | "m6g.16xlarge", 87 | "m6gd.12xlarge", 88 | "m6g.12xlarge", 89 | "m5d.xlarge", 90 | "m5d.large", 91 | "m5d.4xlarge", 92 | "m5d.2xlarge", 93 | "m5d.24xlarge", 94 | "m5d.12xlarge", 95 | "m5.xlarge", 96 | "m5.large", 97 | "m5.4xlarge", 98 | "m5.2xlarge", 99 | "m5.24xlarge", 100 | "m5.12xlarge", 101 | "m4.xlarge", 102 | "m4.4xlarge", 103 | "m4.2xlarge", 104 | "m4.16xlarge", 105 | "m4.10xlarge", 106 | "r6gd.xlarge", 107 | "r6gd.large", 108 | "r6gd.8xlarge", 109 | "r6gd.4xlarge", 110 | "r6gd.2xlarge", 111 | "r6gd.16xlarge", 112 | "r6gd.12xlarge", 113 | "r6g.xlarge", 114 | "r6g.large", 115 | "r6g.8xlarge", 116 | "r6g.4xlarge", 117 | "r6g.2xlarge", 118 | "r6g.16xlarge", 119 | "r6g.12xlarge", 120 | "r5d.xlarge", 121 | "r5d.large", 122 | "r5d.4xlarge", 123 | "r5d.2xlarge", 124 | "r5d.24xlarge", 125 | "r5d.12xlarge", 126 | "r5.xlarge", 127 | "r5.large", 128 | "r5.4xlarge", 129 | "r5.2xlarge", 130 | "r5.24xlarge", 131 | "r5.12xlarge", 132 | "c7g.xlarge", 133 | "c7g.large", 134 | "c7g.8xlarge", 135 | "c7g.4xlarge", 136 | "c7g.2xlarge", 137 | "c7g.16xlarge", 138 | "c7g.12xlarge", 139 | "c6i.xlarge", 140 | "c6i.large", 141 | "c6i.8xlarge", 142 | "c6i.4xlarge", 143 | "c6i.32xlarge", 144 | "c6i.2xlarge", 145 | "c6i.24xlarge", 146 | "c6i.16xlarge", 147 | "c6i.12xlarge", 148 | "c6gn.xlarge", 149 | "c6gn.large", 150 | "c6gn.8xlarge", 151 | "c6gn.4xlarge", 152 | "c6gn.2xlarge", 153 | "c6gn.16xlarge", 154 | "c6gn.12xlarge", 155 | "c6gd.xlarge", 156 | "c6gd.large", 157 | "c6gd.8xlarge", 158 | "c6gd.4xlarge", 159 | "c6gd.2xlarge", 160 | "c6gd.16xlarge", 161 | "c6gd.12xlarge", 162 | "c6g.xlarge", 163 | "c6g.large", 164 | "c6g.8xlarge", 165 | "c6g.4xlarge", 166 | "c6g.2xlarge", 167 | "c6g.16xlarge", 168 | "c6g.12xlarge", 169 | "c5d.xlarge", 170 | "c5d.large", 171 | "c5d.9xlarge", 172 | "c5d.4xlarge", 173 | "c5d.2xlarge", 174 | "c5d.18xlarge", 175 | "c5.xlarge", 176 | "c5.large", 177 | "c5.9xlarge", 178 | "c5.4xlarge", 179 | "c5.2xlarge", 180 | "c5.18xlarge", 181 | "c4.xlarge", 182 | "c4.large", 183 | "c4.8xlarge", 184 | "c4.4xlarge", 185 | "c4.2xlarge", 186 | "trn1.32xlarge", 187 | "trn1.2xlarge", 188 | "p4de.24xlarge", 189 | "p4d.24xlarge", 190 | "p3.8xlarge", 191 | "p3.2xlarge", 192 | "p3.16xlarge", 193 | "p2.xlarge", 194 | "p2.8xlarge", 195 | "p2.16xlarge", 196 | "inf1.xlarge", 197 | "inf1.6xlarge", 198 | "inf1.2xlarge", 199 | "inf1.24xlarge", 200 | "g5.xlarge", 201 | "g5.8xlarge", 202 | "g5.4xlarge", 203 | "g5.48xlarge", 204 | "g5.2xlarge", 205 | "g5.24xlarge", 206 | "g5.16xlarge", 207 | "g5.12xlarge", 208 | "g4dn.xlarge", 209 | "g4dn.8xlarge", 210 | "g4dn.4xlarge", 211 | "g4dn.2xlarge", 212 | "g4dn.16xlarge", 213 | "g4dn.12xlarge" 214 | ]; 215 | } 216 | tick() { 217 | try { 218 | getItemList(this, false).then( 219 | contextsRes => getImageList(this, false).then( 220 | imagesRes => getContainerList(this, false).then( 221 | containerRes => { 222 | this.setState({ 223 | contextList: contextsRes, 224 | imageList: imagesRes, 225 | containerList: containerRes, 226 | }); 227 | } 228 | ) 229 | ) 230 | ) 231 | } catch (err){ 232 | console.log(err); 233 | } 234 | } 235 | componentDidMount() { 236 | this.tick(); 237 | this.interval = setInterval(() => this.tick(), 1000); 238 | } 239 | componentWillUnmount() { 240 | clearInterval(this.interval); 241 | } 242 | renderViewButtons() { 243 | return (React.createElement("form", { onSubmit: this.handleSubmit }, 244 | React.createElement("div", { 245 | className: runSidebarSectionClass 246 | }, 247 | React.createElement("tr", null, 248 | React.createElement("td", null, 249 | React.createElement("span"), 250 | React.createElement(LabeledTextInput, 251 | { 252 | label: "Instance type:", 253 | value: this.state.INSTANCE_TYPE, 254 | options: this.instanceTypes, 255 | onChange: this.onInstanceTypeChange 256 | })), 257 | React.createElement("td", null, 258 | React.createElement(SelectColumn, null)), 259 | React.createElement("td", null, 260 | React.createElement("input", { 261 | className: sidebarButtonClass, 262 | type: "button", 263 | title: "Start new docker host", 264 | value: "Start Host", 265 | onClick: this.handleSubmit 266 | })), 267 | )), 268 | React.createElement( 269 | "div", 270 | { 271 | className: runSidebarSectionClass 272 | }, 273 | null 274 | ), 275 | React.createElement( 276 | "header", 277 | { 278 | className: jpRunningSectionClass 279 | }, 280 | React.createElement( 281 | "h2", 282 | { 283 | className: jpRunningSectionHeader 284 | }, 285 | 'Docker Hosts' 286 | ), 287 | ), 288 | React.createElement( 289 | "div", 290 | { 291 | className: 'jp-RunningSessions-sectionContainer' 292 | }, 293 | React.createElement("ul", { className: "jp-RunningSessions-sectionList" }, isPromise(this.state.contextList)?null:this.state.contextList) 294 | ), 295 | React.createElement("header", { className: jpRunningSectionClass}, 296 | React.createElement("h2", { className: jpRunningSectionHeader}, 'Images')),this.state.imageList?this.state.imageList:null, 297 | React.createElement("header", { className: jpRunningSectionClass}, 298 | React.createElement("h2", { className: jpRunningSectionHeader}, 'Containers')),this.state.containerList?this.state.containerList:null, 299 | React.createElement("div", { className: alertAreaClass }, this.state.alerts.map((alert) => (React.createElement(Alert, { key: `alert-${alert.key}`, type: alert.type, message: alert.message })))))) 300 | } 301 | addAlert(alert) { 302 | const key = this.alertKey++; 303 | const keyedAlert = Object.assign(Object.assign({}, alert), { key: `alert-${key}` }); 304 | this.setState({ alerts: [keyedAlert] }); 305 | setTimeout(() => { this.setState({ alerts: [] }) }, alert.wait ); 306 | } 307 | saveState() { 308 | const state = { 309 | INSTANCE_TYPE: this.state.INSTANCE_TYPE 310 | }; 311 | console.log('save state', state); 312 | this.props.stateDB.save(KEY, state); 313 | } 314 | loadState() { 315 | this.props.stateDB.fetch(KEY).then((s) => { 316 | const state = s; 317 | console.log('load state: ', state); 318 | if (state) { 319 | this.setState({ 320 | INSTANCE_TYPE: state["INSTANCE_TYPE"] 321 | }); 322 | } 323 | }); 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /labextension/sagemaker_studio_docker_ui/package/lib/components/SelectColumn.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { selectColumnClass, selectDorpDownClass } from '../style/SelectColumn'; 3 | export class SelectColumn extends React.Component { 4 | render() { 5 | return (React.createElement("table", { className: selectColumnClass + ' selectColumnMarker' }, 6 | React.createElement("tbody", null, this.props.children))); 7 | } 8 | } 9 | export class LabeledTextInput extends React.Component { 10 | render() { 11 | return (React.createElement( 12 | "tr", 13 | null, 14 | React.createElement( 15 | "td", 16 | null, 17 | this.props.label 18 | ), 19 | React.createElement( 20 | "td", 21 | null, 22 | React.createElement( 23 | 'select', 24 | { 25 | className: selectDorpDownClass, 26 | name: "pick an instance", 27 | onChange: this.props.onChange, 28 | value: this.props.value 29 | }, 30 | this.props.options.map((item, i)=> React.createElement("option", {key:i}, item)) 31 | ) 32 | ) 33 | )); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /labextension/sagemaker_studio_docker_ui/package/lib/components/SwitchContext.js: -------------------------------------------------------------------------------- 1 | import { requestAPIServer } from "../sagemaker-studio-docker-ui"; 2 | 3 | async function switchContext(instance, instanceType, instanceId){ 4 | console.log(`Switching to context ${instanceType}_${instanceId}`); 5 | const dataToSend = { 6 | context_name: `${instanceType}_${instanceId}` 7 | }; 8 | try { 9 | const reply = await requestAPIServer("switch_context", { 10 | body: JSON.stringify(dataToSend), 11 | method: 'POST', 12 | }); 13 | console.log(reply); 14 | instance.addAlert({ message: `Switching to host ${instanceId}`, wait: 5000 }); 15 | } 16 | catch (reason) { 17 | console.error(`Error on POST /docker-host/switch_context ${JSON.stringify(dataToSend)}.\n${reason}`); 18 | instance.addAlert({ 19 | type: "error", 20 | message: `Error checking switching context! "`, 21 | wait: 5000 22 | }); 23 | }; 24 | } 25 | 26 | export class ContextSwitcher { 27 | constructor(instance, instanceType, instanceId) { 28 | this.instanceId = instanceId; 29 | this.instanceType = instanceType; 30 | this.switcher = () => { 31 | switchContext(instance, this.instanceType, this.instanceId); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /labextension/sagemaker_studio_docker_ui/package/lib/components/TerminateHost.js: -------------------------------------------------------------------------------- 1 | import { requestAPIServer } from "../sagemaker-studio-docker-ui"; 2 | 3 | async function terminateHost(instance, instanceId){ 4 | console.log(`Terminating Host ${instanceId}`); 5 | const dataToSend = { 6 | instance_id: instanceId 7 | }; 8 | try { 9 | const reply = await requestAPIServer("terminate_host", { 10 | body: JSON.stringify(dataToSend), 11 | method: 'POST', 12 | }); 13 | console.log(reply); 14 | instance.addAlert({ message: `Terminating host ${instanceId}`, wait: 5000 }); 15 | } 16 | catch (reason) { 17 | console.error(`Error on POST /docker-host/terminate_host ${JSON.stringify(dataToSend)}.\n${reason}`); 18 | instance.addAlert({ 19 | type: "error", 20 | message: `Error terminating host! "`, 21 | wait: 5000 22 | }); 23 | }; 24 | } 25 | 26 | export class HostTerminator { 27 | constructor(instance, instanceId) { 28 | this.instanceId = instanceId 29 | this.terminator = () => { 30 | terminateHost(instance, this.instanceId); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /labextension/sagemaker_studio_docker_ui/package/lib/index.js: -------------------------------------------------------------------------------- 1 | import { ILayoutRestorer } from '@jupyterlab/application'; 2 | import { IStateDB } from '@jupyterlab/statedb'; 3 | import { requestAPIServer } from './sagemaker-studio-docker-ui'; 4 | import { SdockerWidget } from './widgets/SdockerWidget'; 5 | /** 6 | * Initialization data for the sagemaker_studio_docker_ui extension. 7 | */ 8 | const extension = { 9 | id: 'sagemaker-studio-docker-ui', 10 | autoStart: true, 11 | requires: [ILayoutRestorer, IStateDB], 12 | activate: async (app, restorer, stateDB) => { 13 | console.log('JupyterLab extension sagemaker-studio-docker-ui is activated!'); 14 | let INSTANCE_TYPE = 'm5.large'; 15 | const KEY = 'sagemaker-studio-docker-ui:settings:data'; 16 | // Create sdocker widget sidebar 17 | const sdockerWidget = new SdockerWidget(stateDB); 18 | sdockerWidget.id = 'jp-sdocker'; 19 | sdockerWidget.title.iconClass = 'jp-SideBar-tabIcon jp-dockerIcon sdocker-sidebar-icon'; 20 | sdockerWidget.title.caption = 'Sagemaker Studio Docker UI'; 21 | // Let the application restorer track the running panel for restoration of 22 | // application state (e.g. setting the running panel as the current side bar 23 | // widget). 24 | restorer.add(sdockerWidget, 'sagemaker-studio-docker-ui-sidebar'); 25 | // Rank has been chosen somewhat arbitrarily to give priority to the running 26 | // sessions widget in the sidebar. 27 | app.shell.add(sdockerWidget, 'left', { rank: 220 }); 28 | // POST request 29 | app.restored.then(() => stateDB.fetch(KEY)).then((s) => { 30 | const state = s; 31 | if (state) { 32 | if (state['INSTANCE_TYPE']) { 33 | console.log(state['INSTANCE_TYPE']); 34 | INSTANCE_TYPE = state['INSTANCE_TYPE']; 35 | } 36 | } 37 | }).then(async () => { 38 | const dataToSend = { 39 | instance_type: INSTANCE_TYPE 40 | }; 41 | try { 42 | const reply = await requestAPIServer('contexts', { 43 | body: JSON.stringify(dataToSend), 44 | method: 'POST' 45 | }); 46 | console.log(reply); 47 | } 48 | catch (reason) { 49 | console.error(`Error on POST /docker-host/contexts ${JSON.stringify(dataToSend)}.\n${reason}`); 50 | } 51 | }); 52 | } 53 | }; 54 | export default extension; -------------------------------------------------------------------------------- /labextension/sagemaker_studio_docker_ui/package/lib/sagemaker-studio-docker-ui.js: -------------------------------------------------------------------------------- 1 | import { URLExt } from '@jupyterlab/coreutils'; 2 | import { ServerConnection } from '@jupyterlab/services'; 3 | /** 4 | * Call the API extension 5 | * 6 | * @param endPoint API REST end point for the extension 7 | * @param init Initial values for the request 8 | * @returns The response body interpreted as JSON 9 | */ 10 | export async function requestAPIServer(endPoint = '', init = {}) { 11 | // Make request to Jupyter API 12 | const settings = ServerConnection.makeSettings(); 13 | // console.log(JSON.stringify(settings)); 14 | const requestUrl = URLExt.join(settings.baseUrl, 'docker-host', endPoint); 15 | let response; 16 | try { 17 | response = await ServerConnection.makeRequest(requestUrl, init, settings); 18 | } 19 | catch (error) { 20 | throw new ServerConnection.NetworkError(error); 21 | } 22 | const data = await response.json(); 23 | if (!response.ok) { 24 | throw new ServerConnection.ResponseError(response, data.message); 25 | } 26 | return data; 27 | } -------------------------------------------------------------------------------- /labextension/sagemaker_studio_docker_ui/package/lib/style/Alert.js: -------------------------------------------------------------------------------- 1 | import { style } from 'typestyle'; 2 | // Alert styles are cloned from Bootstrap 3 3 | export const alert = style({ 4 | padding: '15px', 5 | marginBottom: '20px', 6 | border: '1px solid transparent', 7 | borderRadius: '4px', 8 | }); 9 | export const alertDanger = style({ 10 | color: '#a94442', 11 | backgroundColor: '#f2dede', 12 | borderColor: '#ebccd1', 13 | }); 14 | export const alertWarning = style({ 15 | color: '#8a6d3b', 16 | backgroundColor: '#fcf8e3', 17 | borderColor: '#faebcc', 18 | }); 19 | export const alertInfo = style({ 20 | color: '#31708f', 21 | backgroundColor: '#d9edf7', 22 | borderColor: '#bce8f1', 23 | }); 24 | export const alertSuccess = style({ 25 | color: '#3c763d', 26 | backgroundColor: '#dff0d8', 27 | borderColor: '#d6e9c6', 28 | }); 29 | -------------------------------------------------------------------------------- /labextension/sagemaker_studio_docker_ui/package/lib/style/SdockerWidgetStyle.js: -------------------------------------------------------------------------------- 1 | import { style } from 'typestyle'; 2 | export const sdockerWidgetStyle = style({ 3 | display: 'flex', 4 | flexDirection: 'column', 5 | minWidth: '300px', 6 | color: 'var(--jp-ui-font-color1)', 7 | background: 'var(--jp-layout-color1)', 8 | fontSize: 'var(--jp-ui-font-size1)', 9 | overflow: 'auto', 10 | }); 11 | -------------------------------------------------------------------------------- /labextension/sagemaker_studio_docker_ui/package/lib/style/SelectColumn.js: -------------------------------------------------------------------------------- 1 | import { style } from 'typestyle'; 2 | export const selectColumnClass = style({ 3 | $nest: { 4 | '& td:first-child': { 5 | paddingLeft: '8px', 6 | paddingRight: '8px' 7 | }, 8 | }, 9 | }); 10 | export const selectDorpDownClass = style({ 11 | display: 'flex', 12 | flexDirection: 'column', 13 | color: 'var(--sm-ui-font-color1)', 14 | fontFamily: 'var(--sm-ui-font-family)', 15 | fontSize: 'var(--sm-ui-font-size1)', 16 | letterSpacing: 'var(--sm-custom-ui-letter-spacing)', 17 | lineHeight: 'var(--sm-custom-ui-text-line-height)', 18 | height: '100%', 19 | background: 'var(--sm-layout-color1)', 20 | color: 'var(--sm-ui-font-color1)', 21 | fontSize: 'var(--sm-ui-font-size1)', 22 | overflowY: 'auto', 23 | position: 'relative', 24 | boxSizing: 'border-box', 25 | cursor: 'inherit', 26 | opacity: '1', 27 | }); 28 | -------------------------------------------------------------------------------- /labextension/sagemaker_studio_docker_ui/package/lib/style/SettingsPanel.js: -------------------------------------------------------------------------------- 1 | import { style } from 'typestyle'; 2 | export const jpRunningSectionHeader = style({ 3 | fonteight: 600, 4 | textTransform: 'uppercase', 5 | letterSpacing: '1px', 6 | fontSize: 'var(--jp-ui-font-size0)', 7 | padding: '8px 8px 8px 12px', 8 | margin: '0px' 9 | }); 10 | export const jpRunningSectionClass = style({ 11 | alignItems: 'center', 12 | height: '28px', 13 | display: 'flex', 14 | borderBottom: 'var(--jp-border-width) solid var(--jp-border-color2)', 15 | marginTop: '8px' 16 | }); 17 | export const runSidebarSectionClass = style({ 18 | background: 'var(--jp-layout-color1)', 19 | overflow: 'visible', 20 | color: 'var(--jp-ui-font-color1)', 21 | /* This is needed so that all font sizing of children done in ems is 22 | * relative to this base size */ 23 | fontSize: 'var(--jp-ui-font-size1)', 24 | marginBottom: '12px', 25 | marginTop: '12px', 26 | marginLeft: '12px', 27 | marginRight: '12px', 28 | $nest: { 29 | '& header': { 30 | borderBottom: 'var(--jp-border-width) solid var(--jp-border-color2)', 31 | flex: '0 0 auto', 32 | fontSize: 'var(--jp-ui-font-size0)', 33 | fontWeight: 600, 34 | letterSpacing: '1px', 35 | margin: '0px 0px 8px 0px', 36 | padding: '8px 12px', 37 | textTransform: 'uppercase', 38 | }, 39 | '&>*': { 40 | marginLeft: '12px', 41 | }, 42 | '&>.inputColumnMarker': { 43 | marginLeft: '10px', 44 | }, 45 | }, 46 | }); 47 | export const runSidebarNoHeaderClass = style({ 48 | borderTop: 'var(--jp-border-width) solid var(--jp-border-color2)', 49 | paddingTop: '8px', 50 | }); 51 | export const runSidebarNotebookNameClass = style({ 52 | marginBottom: 'auto', 53 | fontWeight: 700, 54 | }); 55 | export const runSidebarNoNotebookClass = style({ 56 | fontStyle: 'italic', 57 | padding: '0px 12px', 58 | }); 59 | export const sidebarHeaderClass = style({ 60 | flex: '0 0 auto', 61 | display: 'flex', 62 | flexDirection: 'row', 63 | margin: 'var(--jp-toolbar-header-margin)', 64 | justifyContent: 'flex-end', 65 | }); 66 | export const sidebarButtonClass = style({ 67 | boxSizing: 'border-box', 68 | width: '9.7em', 69 | height: '2em', 70 | color: 'white', 71 | fontSize: 'var(--jp-ui-font-size1)', 72 | backgroundColor: 'var(--jp-brand-color1)', 73 | border: '0', 74 | borderRadius: '3px', 75 | $nest: { 76 | '&:hover': { 77 | backgroundColor: 'var(--jp-brand-color0)', 78 | }, 79 | '&:active': { 80 | color: 'var(--md-grey-200)', 81 | fontWeight: 600, 82 | }, 83 | }, 84 | }); 85 | // Emulate flexbox gaps even in browsers that don't support it yet, per https://coryrylan.com/blog/css-gap-space-with-flexbox 86 | const buttonGapX = '20px'; 87 | const buttonGapY = '8px'; 88 | export const flexButtonsClass = style({ 89 | display: 'inline-flex', 90 | flexWrap: 'wrap', 91 | margin: `-${buttonGapY} 0 0 -${buttonGapX}`, 92 | width: `calc(100% + ${buttonGapX})`, 93 | $nest: { 94 | '> *': { 95 | margin: `${buttonGapY} 0 0 ${buttonGapX}`, 96 | }, 97 | }, 98 | }); 99 | export const alertAreaClass = style({ 100 | marginLeft: '12px', 101 | marginRight: '12px', 102 | }); 103 | export const toolbarButtonClass = style({ 104 | boxSizing: 'border-box', 105 | height: '24px', 106 | width: 'var(--jp-private-running-button-width)', 107 | margin: 'auto 0 auto 0', 108 | padding: '0px 6px', 109 | border: 'none', 110 | outline: 'none', 111 | $nest: { 112 | '&:hover': { 113 | backgroundColor: 'var(--jp-layout-color2)' 114 | }, 115 | '&:active': { 116 | backgroundColor: 'var(--jp-layout-color3)' 117 | } 118 | } 119 | }); 120 | export const refreshButtonClass = style({ 121 | marginRight: '4px', 122 | background: 'var(--jp-layout-color1)', 123 | backgroundImage: 'var(--jp-icon-refresh)', 124 | backgroundSize: '16px', 125 | backgroundRepeat: 'no-repeat', 126 | backgroundPosition: 'center' 127 | }); -------------------------------------------------------------------------------- /labextension/sagemaker_studio_docker_ui/package/lib/style/Widget.js: -------------------------------------------------------------------------------- 1 | import { style } from 'typestyle'; 2 | export const scrollableWidgetClass = style({ 3 | overflow: 'auto', 4 | }); 5 | -------------------------------------------------------------------------------- /labextension/sagemaker_studio_docker_ui/package/lib/style/getContextsStyle.js: -------------------------------------------------------------------------------- 1 | import { style } from 'typestyle'; 2 | 3 | export const defaultHeight = style( 4 | { 5 | hieght: 'auto' 6 | } 7 | ) 8 | export const itemIconSpan = style( 9 | { 10 | flex: '0 0 auto', 11 | padding: '0px 8px', 12 | marginRight: '4px', 13 | marginLeft: '12px', 14 | verticalAlign: 'baseline', 15 | backgroundSize: '16px', 16 | backgroundRepeat: 'no-repeat', 17 | backgroundPosition: 'center' 18 | } 19 | ) 20 | export const instanceDescriptionDiv = style( 21 | { 22 | paddingLeft: '8px', 23 | overflow: 'hidden', 24 | textOverflow: 'ellipsis', 25 | whiteSpace: 'nowrap', 26 | flex: '1 1 95px', 27 | transition: 'background-color 0.1s ease', 28 | $nest: { 29 | '&:hover': { 30 | background: 'var(--jp-layout-color2)' 31 | }, 32 | '&:focus': { 33 | background: 'rgba(153, 153, 153, 0.2)' 34 | } 35 | } 36 | } 37 | ) 38 | export const runningHostStyle = style( 39 | { 40 | backgroundImage: 'var(--jp-runningHostIcon)', 41 | flex: '0 0 auto', 42 | padding: '0px 8px', 43 | marginRight: '6px', 44 | marginLeft: '12px', 45 | verticalAlign: 'middle', 46 | backgroundSize: '16px', 47 | backgroundRepeat: 'no-repeat', 48 | backgroundPosition: 'center center', 49 | display: 'flex', 50 | flexDirection: 'row', 51 | justifyContent: 'space-around', 52 | color: 'var(--jp-ui-font-color1)', 53 | lineHeight: 'var(--jp-private-running-item-height)' 54 | } 55 | ) 56 | export const idleHostStyle = style( 57 | { 58 | backgroundImage: 'var(--jp-idleHostIcon)', 59 | flex: '0 0 auto', 60 | padding: '0px 8px', 61 | marginRight: '0px', 62 | marginLeft: '6px', 63 | verticalAlign: 'middle', 64 | backgroundSize: '16px', 65 | backgroundRepeat: 'no-repeat', 66 | backgroundPosition: 'center center', 67 | display: 'flex', 68 | flexDirection: 'row', 69 | justifyContent: 'space-around', 70 | color: 'var(--jp-ui-font-color1)', 71 | lineHeight: 'var(--jp-private-running-item-height)' 72 | } 73 | ) 74 | export const imageIconStyle = style( 75 | { 76 | backgroundImage: 'var(--jp-imageIcon)', 77 | flex: '0 0 auto', 78 | padding: '0px 8px', 79 | marginRight: '4px', 80 | marginLeft: '12px', 81 | verticalAlign: 'middle', 82 | backgroundSize: '16px', 83 | backgroundRepeat: 'no-repeat', 84 | backgroundPosition: 'center center', 85 | display: 'flex', 86 | flexDirection: 'row', 87 | justifyContent: 'space-around', 88 | color: 'var(--jp-ui-font-color1)', 89 | lineHeight: 'var(--jp-private-running-item-height)' 90 | } 91 | ) 92 | export const containerIconStyle = style( 93 | { 94 | backgroundImage: 'var(--jp-containerIcon)', 95 | flex: '0 0 auto', 96 | padding: '0px 8px', 97 | marginRight: '4px', 98 | marginLeft: '12px', 99 | verticalAlign: 'middle', 100 | backgroundSize: '16px', 101 | backgroundRepeat: 'no-repeat', 102 | backgroundPosition: 'center center', 103 | display: 'flex', 104 | flexDirection: 'row', 105 | justifyContent: 'space-around', 106 | color: 'var(--jp-ui-font-color1)', 107 | lineHeight: 'var(--jp-private-running-item-height)' 108 | } 109 | ) 110 | export const powerIconStyle = style( 111 | { 112 | backgroundImage: 'var(--jp-powerIcon)', 113 | flex: '0 0 auto', 114 | padding: '0px 8px', 115 | marginRight: '6px', 116 | marginLeft: '12px', 117 | verticalAlign: 'middle', 118 | backgroundSize: '16px', 119 | backgroundRepeat: 'no-repeat', 120 | backgroundPosition: 'center center', 121 | display: 'flex', 122 | flexDirection: 'row', 123 | justifyContent: 'space-around', 124 | color: 'var(--jp-ui-font-color1)', 125 | lineHeight: 'var(--jp-private-running-item-height)' 126 | } 127 | ) -------------------------------------------------------------------------------- /labextension/sagemaker_studio_docker_ui/package/lib/widgets/SdockerWidget.js: -------------------------------------------------------------------------------- 1 | import { ReactWidget } from '@jupyterlab/apputils'; 2 | import * as React from 'react'; 3 | import { SdockerPanel } from '../components/SdockerPanel'; 4 | import { sdockerWidgetStyle } from '../style/SdockerWidgetStyle'; 5 | /** 6 | * A class that exposes the Sdocker server plugin Widget. 7 | */ 8 | export class SdockerWidget extends ReactWidget { 9 | constructor(stateDB, options) { 10 | super(options); 11 | this.node.id = 'SdockerSession-root'; 12 | this.addClass(sdockerWidgetStyle); 13 | this.stateDB = stateDB; 14 | console.log('Sdocker widget created'); 15 | } 16 | render() { 17 | return (React.createElement(SdockerPanel, { stateDB: this.stateDB })); 18 | } 19 | } -------------------------------------------------------------------------------- /labextension/sagemaker_studio_docker_ui/package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sagemaker-studio-docker-ui", 3 | "version": "0.1.0", 4 | "description": "A JupyterLab extension to for sagemaker_studio_docker_ui", 5 | "keywords": [ 6 | "jupyter", 7 | "jupyterlab", 8 | "jupyterlab-extension" 9 | ], 10 | "homepage": "https://github.com/aws-samples/sagemaker-studio-docker-ui-extension", 11 | "bugs": { 12 | "url": "https://github.com/aws-samples/sagemaker-studio-docker-ui-extension/issues" 13 | }, 14 | "license": "BSD-3-Clause", 15 | "author": "Sam Edwards", 16 | "files": [ 17 | "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", 18 | "style/**/*.{css,eot,gif,html,jpg,json,png,svg,woff2,ttf}" 19 | ], 20 | "main": "lib/index.js", 21 | "types": "lib/index.d.ts", 22 | "style": "style/index.css", 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/aws-samples/sagemaker-studio-docker-ui-extension.git" 26 | }, 27 | "scripts": { 28 | "build": "jlpm run build:lib && jlpm run build:labextension:dev", 29 | "build:prod": "jlpm run build:lib && jlpm run build:labextension", 30 | "build:lib": "tsc", 31 | "build:labextension": "jupyter labextension build .", 32 | "build:labextension:dev": "jupyter labextension build --development True .", 33 | "clean": "rimraf lib tsconfig.tsbuildinfo sagemaker-studio-docker-ui-extension/labextension", 34 | "clean:all": "jlpm run clean:lib && jlpm run clean:labextension", 35 | "clean:labextension": "rimraf sagemaker-studio-docker-ui-extension/labextension", 36 | "clean:slate": "jlpm clean:more && jlpm clean:labextension && rimraf node_modules", 37 | "eslint": "eslint . --fix --ext .ts,.tsx", 38 | "eslint:check": "eslint . --ext .ts,.tsx", 39 | "watch": "tsc -w" 40 | }, 41 | "dependencies": { 42 | "@jupyterlab/application": "^3.0.0", 43 | "@jupyterlab/apputils": "^3.0.0", 44 | "@jupyterlab/ui-components": "^3.0.0", 45 | "@jupyterlab/notebook": "^3.0.0", 46 | "@lumino/coreutils": "^1.3.1", 47 | "@lumino/messaging": "^1.3.0", 48 | "classnames": "^2.2.6", 49 | "react": "17.0.1", 50 | "@jupyterlab/services": "^6.0.0" 51 | }, 52 | "devDependencies": { 53 | "@jupyterlab/builder": "^3.0.0", 54 | "@typescript-eslint/eslint-plugin": "^2.25.0", 55 | "@typescript-eslint/parser": "^2.25.0", 56 | "eslint": "^6.8.0", 57 | "eslint-config-prettier": "^6.10.1", 58 | "eslint-plugin-prettier": "^3.1.2", 59 | "mkdirp": "^1.0.3", 60 | "prettier": "1.16.4", 61 | "rimraf": "^2.6.1", 62 | "typescript": "~3.7.0" 63 | }, 64 | "sideEffects": [ 65 | "style/*.css" 66 | ], 67 | "jupyterlab": { 68 | "discovery": { 69 | "server": { 70 | "managers": [ 71 | "pip" 72 | ], 73 | "base": { 74 | "name": "sagemaker_studio_docker_ui" 75 | } 76 | } 77 | }, 78 | "extension": true, 79 | "outputDir": "sagemaker-studio-docker-ui-extension/labextension" 80 | } 81 | } -------------------------------------------------------------------------------- /labextension/sagemaker_studio_docker_ui/package/style/container-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 61 | 64 | 66 | 70 | 74 | 78 | 79 | 81 | 83 | 85 | 87 | 89 | 91 | 93 | 95 | 97 | 99 | 101 | 103 | 105 | 107 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /labextension/sagemaker_studio_docker_ui/package/style/docker-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 31 | 55 | 59 | 64 | 69 | 73 | 74 | -------------------------------------------------------------------------------- /labextension/sagemaker_studio_docker_ui/package/style/idle-host.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 61 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /labextension/sagemaker_studio_docker_ui/package/style/image-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | 27 | 28 | 29 | 30 | 32 | 56 | 59 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /labextension/sagemaker_studio_docker_ui/package/style/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --jp-runningHostIcon: url('./running-host.svg'); 3 | --jp-idleHostIcon: url('./idle-host.svg'); 4 | --jp-containerIcon: url('./container-icon.svg'); 5 | --jp-imageIcon: url('./image-icon.svg'); 6 | --jp-powerIcon: url('./power-icon.svg'); 7 | } 8 | 9 | .jp-dockerIcon { 10 | background-image: url('./docker-icon.svg'); 11 | background-size: contain; 12 | } -------------------------------------------------------------------------------- /labextension/sagemaker_studio_docker_ui/package/style/power-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /labextension/sagemaker_studio_docker_ui/package/style/running-host.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 61 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["jupyter_packaging~=0.4.0", "jupyterlab~=1.2", "setuptools>=40.8.0", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /sagemaker_studio_docker_ui/__init__.py: -------------------------------------------------------------------------------- 1 | from .handlers import setup_handlers 2 | 3 | 4 | def _jupyter_server_extension_paths(): 5 | return [{"module": "sagemaker_studio_docker_ui"}] 6 | 7 | 8 | def load_jupyter_server_extension(lab_app): 9 | """Registers the API handler to receive HTTP requests from the frontend extension. 10 | 11 | Parameters 12 | ---------- 13 | lab_app: jupyterlab.labapp.LabApp 14 | JupyterLab application instance 15 | """ 16 | 17 | url_path = "docker-host" 18 | setup_handlers(lab_app.web_app, url_path) 19 | 20 | lab_app.log.info( 21 | "Registered sagemaker_studio_docker_ui extension at URL path /{}".format( 22 | url_path 23 | ) 24 | ) 25 | -------------------------------------------------------------------------------- /sagemaker_studio_docker_ui/_version.py: -------------------------------------------------------------------------------- 1 | version_info = (0, 1, 0) 2 | __version__ = ".".join(map(str, version_info)) 3 | -------------------------------------------------------------------------------- /sagemaker_studio_docker_ui/checkers.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import traceback 3 | from contextlib import suppress 4 | import subprocess 5 | import requests 6 | import json 7 | from enum import Enum 8 | import logging as log 9 | 10 | 11 | def get_context(): 12 | cmd = ["docker", "context", "list", "--format={{.Current}},{{.DockerEndpoint}},{{.Name}}"] 13 | log.debug("Listing docker contexts") 14 | contexts_raw = subprocess.run(cmd, stdout=subprocess.PIPE) 15 | contexts = contexts_raw.stdout.decode("utf-8").split("\n")[:-1] 16 | response = [] 17 | for context in contexts: 18 | context_list = context.split(",") 19 | if not context_list[2] == "default": 20 | if "_" in context_list[2]: 21 | json_context = { 22 | "Current": context_list[0], 23 | "DockerEndpoint": context_list[1], 24 | "InstanceId": context_list[2].split("_")[1], 25 | "InstanceType": context_list[2].split("_")[0] 26 | } 27 | response.append(json_context) 28 | return response 29 | 30 | 31 | class ContextChecker(object): 32 | def __init__(self): 33 | self.interval = 1 # frequency for checking contexts in seconds 34 | self._running = False 35 | self.count = 0 36 | self.task = None 37 | self.errors = None 38 | self.ignore_connections = True 39 | self.tornado_client = None 40 | self.base_url = None 41 | self.app_url = "http://0.0.0.0:8888" 42 | self.contexts = [] 43 | self.current_instance_id = "" 44 | 45 | # Invoke context_checks() function 46 | async def run_context_checks(self): 47 | while True: 48 | self.count += 1 49 | await asyncio.sleep(self.interval) 50 | try: 51 | await self.context_checks() 52 | except Exception: 53 | self.errors = traceback.format_exc() 54 | self.log.error(self.errors) 55 | 56 | # Entrypoint function to get the value from handlers(POST API call) and start background job 57 | def start(self, base_url, log_handler, client): 58 | self.tornado_client = client 59 | self.base_url = base_url 60 | self.log = log_handler 61 | self.errors = None # clear error array at start 62 | 63 | if not self._running: 64 | self.count += 1 65 | self._running = True 66 | self.task = asyncio.ensure_future(self.run_context_checks()) 67 | 68 | async def stop(self): 69 | if self._running: 70 | self._running = False 71 | if self.task: 72 | self.task.cancel() 73 | with suppress(asyncio.CancelledError): 74 | await self.task 75 | 76 | def get_runcounts(self): 77 | return self.count 78 | 79 | def get_runerrors(self): 80 | return self.errors 81 | 82 | async def context_checks(self): 83 | self.contexts = get_context() 84 | current_flag = False 85 | for context in self.contexts: 86 | if context["Current"] == "true": 87 | current_flag = True 88 | if self.current_instance_id != context["InstanceId"]: 89 | await checkers.image_checker.stop() 90 | await checkers.container_checker.stop() 91 | await checkers.ping_checker.stop() 92 | checkers.image_checker.start(self.base_url, self.log, self.tornado_client, context["InstanceId"]) 93 | checkers.container_checker.start(self.base_url, self.log, self.tornado_client, context["InstanceId"]) 94 | checkers.ping_checker.start(self.base_url, self.log, self.tornado_client, context["InstanceId"]) 95 | self.current_instance_id = context["InstanceId"] 96 | 97 | if not current_flag: 98 | await checkers.image_checker.stop() 99 | await checkers.container_checker.stop() 100 | self.current_instance_id = "" 101 | 102 | class ContainerChecker(object): 103 | def __init__(self): 104 | self.interval = 1 # frequency for checking containers in seconds 105 | self._running = False 106 | self.count = 0 107 | self.task = None 108 | self.errors = None 109 | self.ignore_connections = True 110 | self.tornado_client = None 111 | self.base_url = None 112 | self.app_url = "http://0.0.0.0:8888" 113 | self.containers = [] 114 | 115 | # Invoke context_checks() function 116 | async def run_container_checks(self, instance_id): 117 | while True: 118 | self.count += 1 119 | await asyncio.sleep(self.interval) 120 | try: 121 | await self.container_checks(instance_id) 122 | except Exception: 123 | self.errors = traceback.format_exc() 124 | self.log.error(self.errors) 125 | 126 | # Entrypoint function to get the value from handlers(POST API call) and start background job 127 | def start(self, base_url, log_handler, client, instance_id): 128 | self.tornado_client = client 129 | self.base_url = base_url 130 | self.log = log_handler 131 | self.errors = None # clear error array at start 132 | 133 | if not self._running: 134 | self.count += 1 135 | self._running = True 136 | self.task = asyncio.ensure_future(self.run_container_checks(instance_id)) 137 | 138 | async def stop(self): 139 | self.containers = [] 140 | if self._running: 141 | self._running = False 142 | if self.task: 143 | self.task.cancel() 144 | with suppress(asyncio.CancelledError): 145 | await self.task 146 | 147 | def get_runcounts(self): 148 | return self.count 149 | 150 | def get_runerrors(self): 151 | return self.errors 152 | 153 | async def container_checks(self, instance_id): 154 | contexts = get_context() 155 | reponse = [] 156 | dns_address = None 157 | for context in contexts: 158 | if context["InstanceId"] == instance_id: 159 | # example - tcp://ip-172-31-76-33.ap-southeast-2.compute.internal:1111 160 | port = context["DockerEndpoint"].split(":")[-1] 161 | dns_address = context["DockerEndpoint"].split(":")[1].split("//")[1] 162 | instance_type = context["InstanceType"] 163 | if dns_address: 164 | try: 165 | path_to_cert = f"/home/sagemaker-user/.sagemaker_studio_docker_cli/{instance_type}_{instance_id}/certs/" 166 | cert=(path_to_cert + "client/cert.pem", path_to_cert + "client/key.pem") 167 | response = json.loads(requests.get(f"https://{dns_address}:{port}/containers/json", cert=cert, verify=path_to_cert + "ca/cert.pem").content.decode("utf-8")) 168 | except: 169 | response = [] 170 | self.containers = response 171 | 172 | class ImageChecker(object): 173 | def __init__(self): 174 | self.interval = 1 # frequency for checking images in seconds 175 | self._running = False 176 | self.count = 0 177 | self.task = None 178 | self.errors = None 179 | self.ignore_connections = True 180 | self.tornado_client = None 181 | self.base_url = None 182 | self.app_url = "http://0.0.0.0:8888" 183 | self.images = [] 184 | self.instance_id = "" 185 | 186 | # Invoke image_checks() function 187 | async def run_image_checks(self, instance_id): 188 | while True: 189 | self.count += 1 190 | await asyncio.sleep(self.interval) 191 | try: 192 | await self.image_checks(instance_id) 193 | except Exception: 194 | self.errors = traceback.format_exc() 195 | self.log.error(self.errors) 196 | 197 | # Entrypoint function to get the value from handlers(POST API call) and start background job 198 | def start(self, base_url, log_handler, client, instance_id): 199 | self.tornado_client = client 200 | self.base_url = base_url 201 | self.log = log_handler 202 | self.errors = None # clear error array at start 203 | 204 | if instance_id != self.instance_id: 205 | if self._running: 206 | self.stop() 207 | self.instance_id = "" 208 | 209 | if not self._running: 210 | self.count += 1 211 | self._running = True 212 | self.task = asyncio.ensure_future(self.run_image_checks(instance_id)) 213 | self.instance_id = instance_id 214 | 215 | async def stop(self): 216 | self.images = [] 217 | if self._running: 218 | self._running = False 219 | self.instance_id = "" 220 | if self.task: 221 | self.task.cancel() 222 | with suppress(asyncio.CancelledError): 223 | await self.task 224 | 225 | def get_runcounts(self): 226 | return self.count 227 | 228 | def get_runerrors(self): 229 | return self.errors 230 | 231 | async def image_checks(self, instance_id): 232 | contexts = get_context() 233 | dns_address = None 234 | for context in contexts: 235 | if context["InstanceId"] == instance_id: 236 | # example: tcp://ip-172-31-76-33.ap-southeast-2.compute.internal:1111 237 | port = context["DockerEndpoint"].split(":")[-1] 238 | dns_address = context["DockerEndpoint"].split(":")[1].split("//")[1] 239 | instance_type = context["InstanceType"] 240 | if dns_address: 241 | try: 242 | path_to_cert = f"/home/sagemaker-user/.sagemaker_studio_docker_cli/{instance_type}_{instance_id}/certs/" 243 | cert=(path_to_cert + "client/cert.pem", path_to_cert + "client/key.pem") 244 | response = json.loads(requests.get(f"https://{dns_address}:{port}/images/json", cert=cert, verify=path_to_cert + "ca/cert.pem").content.decode("utf-8")) 245 | except: 246 | response = [] 247 | self.images = response 248 | 249 | class HostStatus(Enum): 250 | HEALTHY = 1 251 | UNHEALTHY = 0 252 | 253 | class PingChecker(object): 254 | def __init__(self): 255 | self.interval = 10 # frequency for checking idle sessions in seconds 256 | self._running = False 257 | self.count = 0 258 | self.task = None 259 | self.errors = None 260 | self.ignore_connections = True 261 | self.tornado_client = None 262 | self.base_url = None 263 | self.app_url = "http://0.0.0.0:8888" 264 | self.status = HostStatus(0) 265 | 266 | # Invoke context_checks() function 267 | async def run_health_checks(self, instance_id): 268 | while True: 269 | self.count += 1 270 | await asyncio.sleep(self.interval) 271 | try: 272 | await self.ping_checks(instance_id) 273 | except Exception: 274 | self.errors = traceback.format_exc() 275 | self.log.error(self.errors) 276 | 277 | # Entrypoint function to get the value from handlers(POST API call) and start background job 278 | def start(self, base_url, log_handler, client, instance_id): 279 | self.tornado_client = client 280 | self.base_url = base_url 281 | self.log = log_handler 282 | self.errors = None # clear error array at start 283 | 284 | if not self._running: 285 | self.count += 1 286 | self._running = True 287 | self.task = asyncio.ensure_future(self.run_health_checks(instance_id)) 288 | 289 | async def stop(self): 290 | if self._running: 291 | self._running = False 292 | if self.task: 293 | self.task.cancel() 294 | with suppress(asyncio.CancelledError): 295 | await self.task 296 | 297 | def get_runcounts(self): 298 | return self.count 299 | 300 | def get_runerrors(self): 301 | return self.errors 302 | 303 | async def ping_checks(self, instance_id): 304 | contexts = get_context() 305 | dns_address = None 306 | current_context = "" 307 | instance_type = "" 308 | for context in contexts: 309 | if context["InstanceId"] == instance_id: 310 | port = context["DockerEndpoint"].split(":")[-1] 311 | dns_address = context["DockerEndpoint"].split(":")[1].split("//")[1] 312 | current_context = context 313 | instance_type = context["InstanceType"] 314 | if dns_address: 315 | try: 316 | path_to_cert = f"/home/sagemaker-user/.sagemaker_studio_docker_cli/{instance_type}_{instance_id}/certs/" 317 | cert=(path_to_cert + "client/cert.pem", path_to_cert + "client/key.pem") 318 | response = json.loads(requests.get(f"https://{dns_address}:{port}/version", cert=cert, verify=path_to_cert + "ca/cert.pem").content.decode("utf-8")) 319 | self.status = HostStatus(1) 320 | except: 321 | self.status = HostStatus(0) 322 | log.error("Failed to ping host, removing docker context!") 323 | cmd = ["docker", "context", "use", "default"] 324 | contexts_raw = subprocess.run(cmd, stdout=subprocess.PIPE) 325 | cmd = ["docker", "context", "rm", f"{current_context['InstanceType']}_{current_context['InstanceId']}"] 326 | contexts_raw = subprocess.run(cmd, stdout=subprocess.PIPE) 327 | await checkers.image_checker.stop() 328 | await checkers.container_checker.stop() 329 | await checkers.ping_checker.stop() 330 | 331 | 332 | 333 | class CheckersClass: 334 | def __init__(self): 335 | self.context_checker = ContextChecker() 336 | self.container_checker = ContainerChecker() 337 | self.image_checker = ImageChecker() 338 | self.ping_checker = PingChecker() 339 | 340 | checkers = CheckersClass() -------------------------------------------------------------------------------- /sagemaker_studio_docker_ui/context_switcher.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import logging as log 3 | 4 | def switch_context(context_name): 5 | log.info(f"Switching context to {context_name}") 6 | subprocess.Popen(["docker","context","use", context_name]) -------------------------------------------------------------------------------- /sagemaker_studio_docker_ui/handlers.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import tornado 4 | import urllib3 5 | from notebook.base.handlers import APIHandler 6 | from notebook.utils import url_path_join 7 | from .host_creator import create_host 8 | from .host_terminator import terminate_host 9 | from .context_switcher import switch_context 10 | from .checkers import checkers 11 | 12 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 13 | base_url = None 14 | 15 | context_checker = checkers.context_checker 16 | container_checker = checkers.container_checker 17 | image_checker = checkers.image_checker 18 | ping_checker = checkers.ping_checker 19 | 20 | class HostCreateHandler(APIHandler): 21 | 22 | # The following decorator should be present on all verb methods (head, get, post, 23 | # patch, put, delete, options) to ensure only authorized user can request the 24 | # Jupyter server 25 | 26 | @tornado.web.authenticated 27 | async def post(self): 28 | global base_url 29 | 30 | client = tornado.httpclient.AsyncHTTPClient() 31 | input_data = self.get_json_body() 32 | 33 | try: 34 | instance_type = input_data["instance_type"] 35 | data = { 36 | "instance_type": instance_type 37 | } 38 | create_host(instance_type) 39 | self.finish(json.dumps(data)) 40 | except Exception as e: 41 | # Other errors are possible, such as IOError. 42 | self.log.error("Error: " + str(e)) 43 | self.finish('["Error"]') 44 | 45 | class HostTerminateHandler(APIHandler): 46 | 47 | # The following decorator should be present on all verb methods (head, get, post, 48 | # patch, put, delete, options) to ensure only authorized user can request the 49 | # Jupyter server 50 | 51 | @tornado.web.authenticated 52 | async def post(self): 53 | global base_url 54 | 55 | client = tornado.httpclient.AsyncHTTPClient() 56 | input_data = self.get_json_body() 57 | 58 | try: 59 | instance_id = input_data["instance_id"] 60 | data = { 61 | "instance_id": instance_id 62 | } 63 | terminate_host(instance_id) 64 | self.finish(json.dumps(data)) 65 | except Exception as e: 66 | # Other errors are possible, such as IOError. 67 | self.log.error(f"Error on POST /docker-host/terminate_host for instance {instance_id}\n{e}") 68 | self.finish(f'["Error": {e}]') 69 | 70 | class ContextSwitchHandler(APIHandler): 71 | 72 | # The following decorator should be present on all verb methods (head, get, post, 73 | # patch, put, delete, options) to ensure only authorized user can request the 74 | # Jupyter server 75 | 76 | @tornado.web.authenticated 77 | async def post(self): 78 | global base_url 79 | 80 | client = tornado.httpclient.AsyncHTTPClient() 81 | input_data = self.get_json_body() 82 | context_name = '' 83 | try: 84 | context_name = input_data["context_name"] 85 | data = { 86 | "context_name": context_name 87 | } 88 | switch_context(context_name) 89 | self.finish(json.dumps(data)) 90 | except Exception as e: 91 | # Other errors are possible, such as IOError. 92 | self.log.error(f"Error on POST /docker-host/switch_context for context {context_name}\n{e}") 93 | self.finish(f'["Error": {e}]') 94 | 95 | class ContextHandler(APIHandler): 96 | 97 | # The following decorator should be present on all verb methods (head, get, post, 98 | # patch, put, delete, options) to ensure only authorized user can request the 99 | # Jupyter server 100 | @tornado.web.authenticated 101 | async def get(self): 102 | global context_checker 103 | 104 | self.finish(json.dumps(context_checker.contexts)) 105 | 106 | @tornado.web.authenticated 107 | async def post(self): 108 | global base_url 109 | global context_checker 110 | 111 | client = tornado.httpclient.AsyncHTTPClient() 112 | input_data = self.get_json_body() 113 | 114 | try: 115 | data = { 116 | "greetings": "Hello, from JupyterLab Sagemaker Studio Docker UI Extension!" 117 | } 118 | # start background job 119 | context_checker.start( 120 | self.base_url, self.log, client 121 | ) 122 | data["count"] = context_checker.get_runcounts() 123 | self.finish(json.dumps(data)) 124 | except Exception as e: 125 | # Other errors are possible, such as IOError. 126 | self.log.error(f"Error on POST /docker-host/contexts\n{e}") 127 | self.finish(f'["Error": {e}]') 128 | 129 | class ImageHandler(APIHandler): 130 | 131 | # The following decorator should be present on all verb methods (head, get, post, 132 | # patch, put, delete, options) to ensure only authorized user can request the 133 | # Jupyter server 134 | @tornado.web.authenticated 135 | async def get(self): 136 | global image_checker 137 | 138 | self.finish(json.dumps(image_checker.images)) 139 | 140 | @tornado.web.authenticated 141 | async def post(self): 142 | global base_url 143 | global image_checker 144 | 145 | client = tornado.httpclient.AsyncHTTPClient() 146 | input_data = self.get_json_body() 147 | 148 | try: 149 | instance_id = input_data["instance_id"] 150 | # stop any previous running job 151 | await image_checker.stop() 152 | # start background job 153 | image_checker.start( 154 | self.base_url, self.log, client, instance_id 155 | ) 156 | self.finish(json.dumps(image_checker.images)) 157 | except Exception as e: 158 | # Other errors are possible, such as IOError. 159 | self.log.error(f"Error on POST /docker-host/images for instance {instance_id}\n{e}") 160 | self.finish(f'["Error": {e}]') 161 | 162 | class ContainerHandler(APIHandler): 163 | 164 | # The following decorator should be present on all verb methods (head, get, post, 165 | # patch, put, delete, options) to ensure only authorized user can request the 166 | # Jupyter server 167 | @tornado.web.authenticated 168 | async def get(self): 169 | global container_checker 170 | 171 | self.finish(json.dumps(container_checker.containers)) 172 | 173 | @tornado.web.authenticated 174 | async def post(self): 175 | global base_url 176 | global container_checker 177 | 178 | client = tornado.httpclient.AsyncHTTPClient() 179 | input_data = self.get_json_body() 180 | 181 | try: 182 | instance_id = input_data["instance_id"] 183 | # stop any previous running job 184 | await container_checker.stop() 185 | # start background job 186 | container_checker.start( 187 | self.base_url, self.log, client, instance_id 188 | ) 189 | self.finish(json.dumps(container_checker.containers)) 190 | except Exception as e: 191 | # Other errors are possible, such as IOError. 192 | self.log.error(f"Error on POST /docker-host/containers for instance {instance_id}\n{e}") 193 | self.finish(f'["Error": {e}]') 194 | 195 | class PingHandler(APIHandler): 196 | 197 | # The following decorator should be present on all verb methods (head, get, post, 198 | # patch, put, delete, options) to ensure only authorized user can request the 199 | # Jupyter server 200 | @tornado.web.authenticated 201 | async def get(self): 202 | global ping_checker 203 | 204 | self.finish(json.dumps(ping_checker.status.name)) 205 | 206 | @tornado.web.authenticated 207 | async def post(self): 208 | global base_url 209 | global ping_checker 210 | 211 | client = tornado.httpclient.AsyncHTTPClient() 212 | input_data = self.get_json_body() 213 | 214 | try: 215 | instance_id = input_data["instance_id"] 216 | # stop any previous running job 217 | await ping_checker.stop() 218 | # start background job 219 | ping_checker.start( 220 | self.base_url, self.log, client, instance_id 221 | ) 222 | self.finish(json.dumps(ping_checker.status.name)) 223 | except Exception as e: 224 | # Other errors are possible, such as IOError. 225 | self.log.error(f"Error on POST /docker-host/ping\n{e}") 226 | self.finish(f'["Error": {e}]') 227 | 228 | # Function to setup the web handdlers 229 | def setup_handlers(web_app, url_path): 230 | global base_url 231 | logging.basicConfig(format='%(asctime)s %(levelname)s [%(filename)s:%(lineno)d]: %(message)s', 232 | datefmt='%m/%d/%Y %H:%M:%S', 233 | filename=f'/home/sagemaker-user/.sagemaker_studio_docker_cli/ui_extension.log', 234 | level=logging.INFO) 235 | logging.info("Starting UI extension server...") 236 | 237 | host_pattern = ".*$" 238 | 239 | base_url = web_app.settings["base_url"] 240 | context_pattern = url_path_join(base_url, url_path, "contexts") 241 | images_pattern = url_path_join(base_url, url_path, "images") 242 | containers_pattern = url_path_join(base_url, url_path, "containers") 243 | create_host_pattern = url_path_join(base_url, url_path, "create_host") 244 | terminate_host_pattern = url_path_join(base_url, url_path, "terminate_host") 245 | switch_context_pattern = url_path_join(base_url, url_path, "switch_context") 246 | ping_host_pattern = url_path_join(base_url, url_path, "ping") 247 | handlers = [ 248 | (context_pattern, ContextHandler), 249 | (images_pattern, ImageHandler), 250 | (containers_pattern, ContainerHandler), 251 | (create_host_pattern, HostCreateHandler), 252 | (terminate_host_pattern, HostTerminateHandler), 253 | (switch_context_pattern, ContextSwitchHandler), 254 | (ping_host_pattern, PingHandler) 255 | ] 256 | web_app.add_handlers(host_pattern, handlers) 257 | -------------------------------------------------------------------------------- /sagemaker_studio_docker_ui/host_creator.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging as log 3 | 4 | log_cmd = "&>> /home/sagemaker-user/.sagemaker_studio_docker_cli/ui_extension.log" 5 | 6 | def create_host(instance_type): 7 | log.info(f"Creating host with instance type: {instance_type}") 8 | is_bash_profile = os.path.isfile("/home/sagemaker-user/.bash_profile") 9 | is_bashrc = os.path.isfile("/home/sagemaker-user/.bashrc") 10 | command = f"sdocker create-host --instance-type {instance_type} {log_cmd} &" 11 | bash_comm = "" 12 | if is_bash_profile: 13 | bash_comm = f"source ~/.bash_profile {log_cmd} && " 14 | elif is_bashrc: 15 | bash_comm = f"source ~/.bashrc {log_cmd} && " 16 | 17 | os.system(bash_comm + command) 18 | -------------------------------------------------------------------------------- /sagemaker_studio_docker_ui/host_terminator.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging as log 3 | 4 | log_cmd = "&>> /home/sagemaker-user/.sagemaker_studio_docker_cli/ui_extension.log" 5 | 6 | def terminate_host(instance_id): 7 | log.info(f"Terminating host with instance id: {instance_id}") 8 | is_bash_profile = os.path.isfile("/home/sagemaker-user/.bash_profile") 9 | is_bashrc = os.path.isfile("/home/sagemaker-user/.bashrc") 10 | command = f"sdocker terminate-host --instance-id {instance_id} {log_cmd} &" 11 | bash_comm = "" 12 | if is_bash_profile: 13 | bash_comm = f"source ~/.bash_profile {log_cmd} && " 14 | elif is_bashrc: 15 | bash_comm = f"source ~/.bashrc {log_cmd} && " 16 | 17 | os.system(bash_comm + command) 18 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Setup Module to setup Python Handlers for the sagemaker_studio_docker_ui extension. 3 | """ 4 | 5 | # BEFORE importing distutils, remove MANIFEST. distutils doesn't properly 6 | # update it when the contents of directories change. 7 | 8 | import functools 9 | import io 10 | import os 11 | import pipes 12 | import re 13 | import shlex 14 | import subprocess 15 | import sys 16 | from collections import defaultdict 17 | from distutils import log 18 | from distutils.cmd import Command 19 | from distutils.command.build_py import build_py 20 | from distutils.command.sdist import sdist 21 | from os.path import join as pjoin 22 | from pathlib import Path 23 | from subprocess import CalledProcessError 24 | 25 | import setuptools 26 | from setuptools.command.bdist_egg import bdist_egg 27 | from setuptools.command.develop import develop 28 | 29 | # Original Copyright (c) Jupyter Development Team. Distributed under the terms of the Modified BSD License. 30 | # Modifications Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. 31 | 32 | 33 | """ 34 | This file originates from the 'jupyter-packaging' package, and 35 | contains a set of useful utilities for including npm packages 36 | within a Python package. 37 | """ 38 | 39 | if os.path.exists("MANIFEST"): 40 | os.remove("MANIFEST") 41 | 42 | try: 43 | from wheel.bdist_wheel import bdist_wheel 44 | except ImportError: 45 | bdist_wheel = None 46 | 47 | if sys.platform == "win32": 48 | from subprocess import list2cmdline 49 | else: 50 | 51 | def list2cmdline(cmd_list): 52 | return " ".join(map(pipes.quote, cmd_list)) 53 | 54 | 55 | __version__ = "0.2.0" 56 | 57 | # --------------------------------------------------------------------------- 58 | # Top Level Variables 59 | # --------------------------------------------------------------------------- 60 | 61 | HERE = os.path.abspath(os.path.dirname(__file__)) 62 | is_repo = os.path.exists(pjoin(HERE, ".git")) 63 | node_modules = pjoin(HERE, "node_modules") 64 | 65 | SEPARATORS = os.sep if os.altsep is None else os.sep + os.altsep 66 | 67 | npm_path = ":".join( 68 | [ 69 | pjoin(HERE, "node_modules", ".bin"), 70 | os.environ.get("PATH", os.defpath), 71 | ] 72 | ) 73 | 74 | if "--skip-npm" in sys.argv: 75 | print("Skipping npm install as requested.") 76 | skip_npm = True 77 | sys.argv.remove("--skip-npm") 78 | else: 79 | skip_npm = False 80 | 81 | 82 | # --------------------------------------------------------------------------- 83 | # Public Functions 84 | # --------------------------------------------------------------------------- 85 | 86 | 87 | def get_version(file, name="__version__"): 88 | """Get the version of the package from the given file by 89 | executing it and extracting the given `name`. 90 | """ 91 | path = os.path.realpath(file) 92 | version_ns = {} 93 | with io.open(path, encoding="utf8") as f: 94 | exec(f.read(), {}, version_ns) 95 | return version_ns[name] 96 | 97 | 98 | def ensure_python(specs): 99 | """Given a list of range specifiers for python, ensure compatibility.""" 100 | if not isinstance(specs, (list, tuple)): 101 | specs = [specs] 102 | v = sys.version_info 103 | part = "%s.%s" % (v.major, v.minor) 104 | for spec in specs: 105 | if part == spec: 106 | return 107 | try: 108 | if eval(part + spec): 109 | return 110 | except SyntaxError: 111 | pass 112 | raise ValueError("Python version %s unsupported" % part) 113 | 114 | 115 | def find_packages(top=HERE): 116 | """ 117 | Find all of the packages. 118 | """ 119 | packages = [] 120 | for d, dirs, _ in os.walk(top, followlinks=True): 121 | if os.path.exists(pjoin(d, "__init__.py")): 122 | packages.append(os.path.relpath(d, top).replace(os.path.sep, ".")) 123 | elif d != top: 124 | # Do not look for packages in subfolders if current is not a package 125 | dirs[:] = [] 126 | return packages 127 | 128 | 129 | def update_package_data(distribution): 130 | """update build_py options to get package_data changes""" 131 | build_py = distribution.get_command_obj("build_py") 132 | build_py.finalize_options() 133 | 134 | 135 | class bdist_egg_disabled(bdist_egg): 136 | """Disabled version of bdist_egg 137 | 138 | Prevents setup.py install performing setuptools' default easy_install, 139 | which it should never ever do. 140 | """ 141 | 142 | def run(self): 143 | sys.exit( 144 | "Aborting implicit building of eggs. Use `pip install .` " 145 | " to install from source." 146 | ) 147 | 148 | 149 | def create_cmdclass(prerelease_cmd=None, package_data_spec=None, data_files_spec=None): 150 | """Create a command class with the given optional prerelease class. 151 | 152 | Parameters 153 | ---------- 154 | prerelease_cmd: (name, Command) tuple, optional 155 | The command to run before releasing. 156 | package_data_spec: dict, optional 157 | A dictionary whose keys are the dotted package names and 158 | whose values are a list of glob patterns. 159 | data_files_spec: list, optional 160 | A list of (path, dname, pattern) tuples where the path is the 161 | `data_files` install path, dname is the source directory, and the 162 | pattern is a glob pattern. 163 | 164 | Notes 165 | ----- 166 | We use specs so that we can find the files *after* the build 167 | command has run. 168 | 169 | The package data glob patterns should be relative paths from the package 170 | folder containing the __init__.py file, which is given as the package 171 | name. 172 | e.g. `dict(foo=['./bar/*', './baz/**'])` 173 | 174 | The data files directories should be absolute paths or relative paths 175 | from the root directory of the repository. Data files are specified 176 | differently from `package_data` because we need a separate path entry 177 | for each nested folder in `data_files`, and this makes it easier to 178 | parse. 179 | e.g. `('share/foo/bar', 'pkgname/bizz, '*')` 180 | """ 181 | wrapped = [prerelease_cmd] if prerelease_cmd else [] 182 | if package_data_spec or data_files_spec: 183 | wrapped.append("handle_files") 184 | wrapper = functools.partial(_wrap_command, wrapped) 185 | handle_files = _get_file_handler(package_data_spec, data_files_spec) 186 | 187 | if "bdist_egg" in sys.argv: 188 | egg = wrapper(bdist_egg, strict=True) 189 | else: 190 | egg = bdist_egg_disabled 191 | 192 | cmdclass = dict( 193 | build_py=wrapper(build_py, strict=is_repo), 194 | bdist_egg=egg, 195 | sdist=wrapper(sdist, strict=True), 196 | handle_files=handle_files, 197 | ) 198 | 199 | if bdist_wheel: 200 | cmdclass["bdist_wheel"] = wrapper(bdist_wheel, strict=True) 201 | 202 | cmdclass["develop"] = wrapper(develop, strict=True) 203 | return cmdclass 204 | 205 | 206 | def command_for_func(func): 207 | """Create a command that calls the given function.""" 208 | 209 | class FuncCommand(BaseCommand): 210 | def run(self): 211 | func() 212 | update_package_data(self.distribution) 213 | 214 | return FuncCommand 215 | 216 | 217 | def run(cmd, **kwargs): 218 | """Echo a command before running it. Defaults to repo as cwd""" 219 | log.info("> " + list2cmdline(cmd)) 220 | kwargs.setdefault("cwd", HERE) 221 | kwargs.setdefault("shell", os.name == "nt") 222 | if not isinstance(cmd, (list, tuple)) and os.name != "nt": 223 | cmd = shlex.split(cmd) 224 | cmd[0] = which(cmd[0]) 225 | return subprocess.check_call(cmd, **kwargs) 226 | 227 | 228 | def is_stale(target, source): 229 | """Test whether the target file/directory is stale based on the source 230 | file/directory. 231 | """ 232 | if not os.path.exists(target): 233 | return True 234 | target_mtime = recursive_mtime(target) or 0 235 | return compare_recursive_mtime(source, cutoff=target_mtime) 236 | 237 | 238 | class BaseCommand(Command): 239 | """Empty command because Command needs subclasses to override too much""" 240 | 241 | user_options = [] 242 | 243 | def initialize_options(self): 244 | pass 245 | 246 | def finalize_options(self): 247 | pass 248 | 249 | def get_inputs(self): 250 | return [] 251 | 252 | def get_outputs(self): 253 | return [] 254 | 255 | 256 | def combine_commands(*commands): 257 | """Return a Command that combines several commands.""" 258 | 259 | class CombinedCommand(Command): 260 | user_options = [] 261 | 262 | def initialize_options(self): 263 | self.commands = [] 264 | for C in commands: 265 | self.commands.append(C(self.distribution)) 266 | for c in self.commands: 267 | c.initialize_options() 268 | 269 | def finalize_options(self): 270 | for c in self.commands: 271 | c.finalize_options() 272 | 273 | def run(self): 274 | for c in self.commands: 275 | c.run() 276 | 277 | return CombinedCommand 278 | 279 | 280 | def compare_recursive_mtime(path, cutoff, newest=True): 281 | """Compare the newest/oldest mtime for all files in a directory. 282 | 283 | Cutoff should be another mtime to be compared against. If an mtime that is 284 | newer/older than the cutoff is found it will return True. 285 | E.g. if newest=True, and a file in path is newer than the cutoff, it will 286 | return True. 287 | """ 288 | if os.path.isfile(path): 289 | mt = mtime(path) 290 | if newest: 291 | if mt > cutoff: 292 | return True 293 | elif mt < cutoff: 294 | return True 295 | for dirname, _, filenames in os.walk(path, topdown=False): 296 | for filename in filenames: 297 | mt = mtime(pjoin(dirname, filename)) 298 | if newest: # Put outside of loop? 299 | if mt > cutoff: 300 | return True 301 | elif mt < cutoff: 302 | return True 303 | return False 304 | 305 | 306 | def recursive_mtime(path, newest=True): 307 | """Gets the newest/oldest mtime for all files in a directory.""" 308 | if os.path.isfile(path): 309 | return mtime(path) 310 | current_extreme = None 311 | for dirname, dirnames, filenames in os.walk(path, topdown=False): 312 | for filename in filenames: 313 | mt = mtime(pjoin(dirname, filename)) 314 | if newest: # Put outside of loop? 315 | if mt >= (current_extreme or mt): 316 | current_extreme = mt 317 | elif mt <= (current_extreme or mt): 318 | current_extreme = mt 319 | return current_extreme 320 | 321 | 322 | def mtime(path): 323 | """shorthand for mtime""" 324 | return os.stat(path).st_mtime 325 | 326 | 327 | def install_npm( 328 | path=None, build_dir=None, source_dir=None, build_cmd="build", force=False, npm=None 329 | ): 330 | """Return a Command for managing an npm installation. 331 | 332 | Note: The command is skipped if the `--skip-npm` flag is used. 333 | 334 | Parameters 335 | ---------- 336 | path: str, optional 337 | The base path of the node package. Defaults to the repo root. 338 | build_dir: str, optional 339 | The target build directory. If this and source_dir are given, 340 | the JavaScript will only be build if necessary. 341 | source_dir: str, optional 342 | The source code directory. 343 | build_cmd: str, optional 344 | The npm command to build assets to the build_dir. 345 | npm: str or list, optional. 346 | The npm executable name, or a tuple of ['node', executable]. 347 | """ 348 | 349 | class NPM(BaseCommand): 350 | description = "install package.json dependencies using npm" 351 | 352 | def run(self): 353 | if skip_npm: 354 | log.info("Skipping npm-installation") 355 | return 356 | node_package = path or HERE 357 | node_modules = pjoin(node_package, "node_modules") 358 | is_yarn = os.path.exists(pjoin(node_package, "yarn.lock")) 359 | 360 | npm_cmd = [npm] if isinstance(npm, str) else npm 361 | 362 | if npm is None: 363 | if is_yarn: 364 | npm_cmd = ["yarn"] 365 | else: 366 | npm_cmd = ["npm"] 367 | 368 | if not which(npm_cmd[0]): 369 | log.error( 370 | "`{0}` unavailable. If you're running this command " 371 | "using sudo, make sure `{0}` is available to sudo".format( 372 | npm_cmd[0] 373 | ) 374 | ) 375 | return 376 | 377 | if force or is_stale(node_modules, pjoin(node_package, "package.json")): 378 | log.info( 379 | "Installing build dependencies with npm. This may " 380 | "take a while..." 381 | ) 382 | run(npm_cmd + ["install"], cwd=node_package) 383 | if build_dir and source_dir and not force: 384 | should_build = is_stale(build_dir, source_dir) 385 | else: 386 | should_build = True 387 | if should_build: 388 | run(npm_cmd + ["run", build_cmd], cwd=node_package) 389 | 390 | return NPM 391 | 392 | 393 | def ensure_targets(targets): 394 | """Return a Command that checks that certain files exist. 395 | 396 | Raises a ValueError if any of the files are missing. 397 | 398 | Note: The check is skipped if the `--skip-npm` flag is used. 399 | """ 400 | 401 | class TargetsCheck(BaseCommand): 402 | def run(self): 403 | if skip_npm: 404 | log.info("Skipping target checks") 405 | return 406 | missing = [t for t in targets if not os.path.exists(t)] 407 | if missing: 408 | raise ValueError(("missing files: %s" % missing)) 409 | 410 | return TargetsCheck 411 | 412 | 413 | # `shutils.which` function copied verbatim from the Python-3.3 source. 414 | def which(cmd, mode=os.F_OK | os.X_OK, path=None): 415 | """Given a command, mode, and a PATH string, return the path which 416 | conforms to the given mode on the PATH, or None if there is no such 417 | file. 418 | `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result 419 | of os.environ.get("PATH"), or can be overridden with a custom search 420 | path. 421 | """ 422 | 423 | # Check that a given file can be accessed with the correct mode. 424 | # Additionally check that `file` is not a directory, as on Windows 425 | # directories pass the os.access check. 426 | def _access_check(fn, mode): 427 | return os.path.exists(fn) and os.access(fn, mode) and not os.path.isdir(fn) 428 | 429 | # Short circuit. If we're given a full path which matches the mode 430 | # and it exists, we're done here. 431 | if _access_check(cmd, mode): 432 | return cmd 433 | 434 | path = (path or os.environ.get("PATH", os.defpath)).split(os.pathsep) 435 | 436 | if sys.platform == "win32": 437 | # The current directory takes precedence on Windows. 438 | if os.curdir not in path: 439 | path.insert(0, os.curdir) 440 | 441 | # PATHEXT is necessary to check on Windows. 442 | pathext = os.environ.get("PATHEXT", "").split(os.pathsep) 443 | # See if the given file matches any of the expected path extensions. 444 | # This will allow us to short circuit when given "python.exe". 445 | matches = [cmd for ext in pathext if cmd.lower().endswith(ext.lower())] 446 | # If it does match, only test that one, otherwise we have to try 447 | # others. 448 | files = [cmd] if matches else [cmd + ext.lower() for ext in pathext] 449 | else: 450 | # On other platforms you don't have things like PATHEXT to tell you 451 | # what file suffixes are executable, so just pass on cmd as-is. 452 | files = [cmd] 453 | 454 | seen = set() 455 | for dir in path: 456 | dir = os.path.normcase(dir) 457 | if dir not in seen: 458 | seen.add(dir) 459 | for thefile in files: 460 | name = os.path.join(dir, thefile) 461 | if _access_check(name, mode): 462 | return name 463 | return None 464 | 465 | 466 | # --------------------------------------------------------------------------- 467 | # Private Functions 468 | # --------------------------------------------------------------------------- 469 | 470 | 471 | def _wrap_command(cmds, cls, strict=True): 472 | """Wrap a setup command 473 | 474 | Parameters 475 | ---------- 476 | cmds: list(str) 477 | The names of the other commands to run prior to the command. 478 | strict: boolean, optional 479 | Whether to raise errors when a pre-command fails. 480 | """ 481 | 482 | class WrappedCommand(cls): 483 | def run(self): 484 | if not getattr(self, "uninstall", None): 485 | try: 486 | [self.run_command(cmd) for cmd in cmds] 487 | except Exception: 488 | if strict: 489 | raise 490 | else: 491 | pass 492 | # update package data 493 | update_package_data(self.distribution) 494 | 495 | result = cls.run(self) 496 | return result 497 | 498 | return WrappedCommand 499 | 500 | 501 | def _get_file_handler(package_data_spec, data_files_spec): 502 | """Get a package_data and data_files handler command.""" 503 | 504 | class FileHandler(BaseCommand): 505 | def run(self): 506 | package_data = self.distribution.package_data 507 | package_spec = package_data_spec or dict() 508 | 509 | for (key, patterns) in package_spec.items(): 510 | package_data[key] = _get_package_data(key, patterns) 511 | 512 | self.distribution.data_files = _get_data_files( 513 | data_files_spec, self.distribution.data_files 514 | ) 515 | 516 | return FileHandler 517 | 518 | 519 | def _get_data_files(data_specs, existing): 520 | """Expand data file specs into valid data files metadata. 521 | 522 | Parameters 523 | ---------- 524 | data_specs: list of tuples 525 | See [createcmdclass] for description. 526 | existing: list of tuples 527 | The existing distribution data_files metadata. 528 | 529 | Returns 530 | ------- 531 | A valid list of data_files items. 532 | """ 533 | # Extract the existing data files into a staging object. 534 | file_data = defaultdict(list) 535 | for (path, files) in existing or []: 536 | file_data[path] = files 537 | 538 | # Extract the files and assign them to the proper data 539 | # files path. 540 | for (path, dname, pattern) in data_specs or []: 541 | dname = dname.replace(os.sep, "/") 542 | offset = len(dname) + 1 543 | 544 | files = _get_files(pjoin(dname, pattern)) 545 | for fname in files: 546 | # Normalize the path. 547 | root = os.path.dirname(fname) 548 | full_path = "/".join([path, root[offset:]]) 549 | if full_path.endswith("/"): 550 | full_path = full_path[:-1] 551 | file_data[full_path].append(fname) 552 | 553 | # Construct the data files spec. 554 | data_files = [] 555 | for (path, files) in file_data.items(): 556 | data_files.append((path, files)) 557 | return data_files 558 | 559 | 560 | def _get_files(file_patterns, top=HERE): 561 | """Expand file patterns to a list of paths. 562 | 563 | Parameters 564 | ----------- 565 | file_patterns: list or str 566 | A list of glob patterns for the data file locations. 567 | The globs can be recursive if they include a `**`. 568 | They should be relative paths from the top directory or 569 | absolute paths. 570 | top: str 571 | the directory to consider for data files 572 | 573 | Note: 574 | Files in `node_modules` are ignored. 575 | """ 576 | if not isinstance(file_patterns, (list, tuple)): 577 | file_patterns = [file_patterns] 578 | 579 | for i, p in enumerate(file_patterns): 580 | if os.path.isabs(p): 581 | file_patterns[i] = os.path.relpath(p, top) 582 | 583 | matchers = [_compile_pattern(p) for p in file_patterns] 584 | 585 | files = set() 586 | 587 | for root, dirnames, filenames in os.walk(top): 588 | # Don't recurse into node_modules 589 | if "node_modules" in dirnames: 590 | dirnames.remove("node_modules") 591 | for m in matchers: 592 | for filename in filenames: 593 | fn = os.path.relpath(pjoin(root, filename), top) 594 | if m(fn): 595 | files.add(fn.replace(os.sep, "/")) 596 | 597 | return list(files) 598 | 599 | 600 | def _get_package_data(root, file_patterns=None): 601 | """Expand file patterns to a list of `package_data` paths. 602 | 603 | Parameters 604 | ----------- 605 | root: str 606 | The relative path to the package root from `HERE`. 607 | file_patterns: list or str, optional 608 | A list of glob patterns for the data file locations. 609 | The globs can be recursive if they include a `**`. 610 | They should be relative paths from the root or 611 | absolute paths. If not given, all files will be used. 612 | 613 | Note: 614 | Files in `node_modules` are ignored. 615 | """ 616 | if file_patterns is None: 617 | file_patterns = ["*"] 618 | return _get_files(file_patterns, pjoin(HERE, root)) 619 | 620 | 621 | def _compile_pattern(pat, ignore_case=True): 622 | """Translate and compile a glob pattern to a regular expression matcher.""" 623 | if isinstance(pat, bytes): 624 | pat_str = pat.decode("ISO-8859-1") 625 | res_str = _translate_glob(pat_str) 626 | res = res_str.encode("ISO-8859-1") 627 | else: 628 | res = _translate_glob(pat) 629 | flags = re.IGNORECASE if ignore_case else 0 630 | return re.compile(res, flags=flags).match 631 | 632 | 633 | def _iexplode_path(path): 634 | """Iterate over all the parts of a path. 635 | 636 | Splits path recursively with os.path.split(). 637 | """ 638 | (head, tail) = os.path.split(path) 639 | if not head or (not tail and head == path): 640 | if head: 641 | yield head 642 | if tail or not head: 643 | yield tail 644 | return 645 | for p in _iexplode_path(head): 646 | yield p 647 | yield tail 648 | 649 | 650 | def _translate_glob(pat): 651 | """Translate a glob PATTERN to a regular expression.""" 652 | translated_parts = [] 653 | for part in _iexplode_path(pat): 654 | translated_parts.append(_translate_glob_part(part)) 655 | os_sep_class = "[%s]" % re.escape(SEPARATORS) 656 | res = _join_translated(translated_parts, os_sep_class) 657 | return "{res}\\Z(?ms)".format(res=res) 658 | 659 | 660 | def _join_translated(translated_parts, os_sep_class): 661 | """Join translated glob pattern parts. 662 | 663 | This is different from a simple join, as care need to be taken 664 | to allow ** to match ZERO or more directories. 665 | """ 666 | res = "" 667 | for part in translated_parts[:-1]: 668 | if part == ".*": 669 | # drop separator, since it is optional 670 | # (** matches ZERO or more dirs) 671 | res += part 672 | else: 673 | res += part + os_sep_class 674 | 675 | if translated_parts[-1] == ".*": 676 | # Final part is ** 677 | res += ".+" 678 | # Follow stdlib/git convention of matching all sub files/directories: 679 | res += "({os_sep_class}?.*)?".format(os_sep_class=os_sep_class) 680 | else: 681 | res += translated_parts[-1] 682 | return res 683 | 684 | 685 | def _translate_glob_part(pat): 686 | """Translate a glob PATTERN PART to a regular expression.""" 687 | # Code modified from Python 3 standard lib fnmatch: 688 | if pat == "**": 689 | return ".*" 690 | i, n = 0, len(pat) 691 | res = [] 692 | while i < n: 693 | c = pat[i] 694 | i = i + 1 695 | if c == "*": 696 | # Match anything but path separators: 697 | res.append("[^%s]*" % SEPARATORS) 698 | elif c == "?": 699 | res.append("[^%s]?" % SEPARATORS) 700 | elif c == "[": 701 | j = i 702 | if j < n and pat[j] == "!": 703 | j = j + 1 704 | if j < n and pat[j] == "]": 705 | j = j + 1 706 | while j < n and pat[j] != "]": 707 | j = j + 1 708 | if j >= n: 709 | res.append("\\[") 710 | else: 711 | stuff = pat[i:j].replace("\\", "\\\\") 712 | i = j + 1 713 | if stuff[0] == "!": 714 | stuff = "^" + stuff[1:] 715 | elif stuff[0] == "^": 716 | stuff = "\\" + stuff 717 | res.append("[%s]" % stuff) 718 | else: 719 | res.append(re.escape(c)) 720 | return "".join(res) 721 | 722 | 723 | # The name of the project 724 | name = "sagemaker_studio_docker_ui" 725 | 726 | # Ensure a valid python version 727 | ensure_python(">=3.7") 728 | 729 | # Get our version 730 | version = get_version(str(Path(HERE) / name / "_version.py")) 731 | 732 | lab_path = Path(HERE) / "labextension" 733 | 734 | data_files_spec = [ 735 | ("share/jupyter/lab/extensions", str(lab_path / name / "labextension"), "*.tgz"), 736 | ( 737 | "etc/jupyter/jupyter_notebook_config.d", 738 | # "sagemaker_studio_autoshutdown/jupyter-config/jupyter_notebook_config.d", 739 | "jupyter-config", 740 | "sagemaker_studio_docker_ui.json", 741 | ), 742 | ] 743 | 744 | 745 | def runPackLabextension(): 746 | if (lab_path / "package.json").is_file(): 747 | try: 748 | run(["jlpm", "build:labextension"], cwd=str(lab_path)) 749 | except CalledProcessError: 750 | pass 751 | 752 | 753 | pack_labext = command_for_func(runPackLabextension) 754 | 755 | cmdclass = create_cmdclass("pack_labext", data_files_spec=data_files_spec) 756 | cmdclass["pack_labext"] = pack_labext 757 | cmdclass.pop("develop") 758 | 759 | 760 | with open("README.md", "r") as fh: 761 | long_description = fh.read() 762 | 763 | 764 | print("finding package") 765 | print(setuptools.find_packages()) 766 | print("finding package done ") 767 | 768 | 769 | setup_args = dict( 770 | name=name, 771 | version=version, 772 | url="https://aws.amazon.com/sagemaker/", 773 | author="Sam Edwards", 774 | description="A JupyterLab Sagemaker Studio Docker UI extension", 775 | long_description=long_description, 776 | long_description_content_type="text/markdown", 777 | cmdclass=cmdclass, 778 | packages=setuptools.find_packages(), 779 | install_requires=["boto3>=1.10.44"], 780 | zip_safe=False, 781 | include_package_data=True, 782 | license="BSD-3-Clause", 783 | platforms="Linux, Mac OS X, Windows", 784 | keywords=["Jupyter", "JupyterLab"], 785 | classifiers=[ 786 | "License :: OSI Approved :: BSD License", 787 | "Programming Language :: Python", 788 | "Programming Language :: Python :: 3", 789 | "Programming Language :: Python :: 3.5", 790 | "Programming Language :: Python :: 3.6", 791 | "Programming Language :: Python :: 3.7", 792 | "Programming Language :: Python :: 3.8", 793 | "Framework :: Jupyter", 794 | ], 795 | extras_require={ 796 | "dev": [ 797 | "python-minifier", 798 | "black", 799 | "pytest", 800 | "jupyterlab~=1.2", 801 | ] 802 | }, 803 | ) 804 | 805 | 806 | if __name__ == "__main__": 807 | setuptools.setup(**setup_args) 808 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eux 4 | 5 | # Before installing extension 6 | export AWS_SAGEMAKER_JUPYTERSERVER_IMAGE="${AWS_SAGEMAKER_JUPYTERSERVER_IMAGE:-'jupyter-server'}" 7 | if [ "$AWS_SAGEMAKER_JUPYTERSERVER_IMAGE" = "jupyter-server-3" ] 8 | then 9 | eval "$(conda shell.bash hook)" 10 | conda activate studio 11 | cd ~/sagemaker-studio-docker-ui-extension/labextension/sagemaker_studio_docker_ui/ 12 | tar -czvf sagemaker_studio_docker_ui_component.tgz --exclude=.* --exclude=__py* package/ 13 | mkdir -p labextension 14 | mv sagemaker_studio_docker_ui_component.tgz labextension/ 15 | cd 16 | tar -czvf sagemaker_studio_docker_ui_extension.tar.gz --exclude=.* sagemaker-studio-docker-ui-extension/ 17 | PACKAGE=sagemaker_studio_docker_ui_extension.tar.gz 18 | else 19 | PACKAGE=https://github.com/aws-samples/sagemaker-studio-docker-ui-extension/releases/download/JL1/sagemaker_studio_docker_ui_extension.tar.gz 20 | fi; 21 | 22 | pip install $PACKAGE 23 | jlpm config set cache-folder /tmp/yarncache 24 | jupyter lab build --debug --minimize=False 25 | nohup supervisorctl -c /etc/supervisor/conf.d/supervisord.conf restart jupyterlabserver --------------------------------------------------------------------------------