diff options
author | Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> | 2018-05-16 12:01:13 +0300 |
---|---|---|
committer | Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> | 2018-05-25 17:16:41 +0300 |
commit | 4220e914db356f4a55c771a7ad7f559e2507dd56 (patch) | |
tree | 104c2b12cc9369985dd6e0f938a94077e680ddb3 /app | |
parent | 8a1ac8f4ce0d8e96234ef32cd032adaf7cc57b1a (diff) | |
download | gitlab-ce-4220e914db356f4a55c771a7ad7f559e2507dd56.tar.gz |
Add support for Jupyter in GitLab via Kubernetes
Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
Diffstat (limited to 'app')
-rw-r--r-- | app/assets/javascripts/clusters/clusters_bundle.js | 2 | ||||
-rw-r--r-- | app/assets/javascripts/clusters/components/applications.vue | 99 | ||||
-rw-r--r-- | app/assets/javascripts/clusters/constants.js | 1 | ||||
-rw-r--r-- | app/assets/javascripts/clusters/services/clusters_service.js | 10 | ||||
-rw-r--r-- | app/assets/javascripts/clusters/stores/clusters_store.js | 12 | ||||
-rw-r--r-- | app/assets/stylesheets/pages/clusters.scss | 2 | ||||
-rw-r--r-- | app/controllers/projects/clusters/applications_controller.rb | 1 | ||||
-rw-r--r-- | app/models/clusters/applications/jupyter.rb | 28 | ||||
-rw-r--r-- | app/models/clusters/cluster.rb | 4 | ||||
-rw-r--r-- | app/serializers/cluster_application_entity.rb | 1 | ||||
-rw-r--r-- | app/services/clusters/applications/install_service.rb | 4 | ||||
-rw-r--r-- | app/views/projects/clusters/show.html.haml | 1 |
12 files changed, 155 insertions, 10 deletions
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index 01aec4f36af..6bf9dca1112 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -31,6 +31,7 @@ export default class Clusters { installHelmPath, installIngressPath, installRunnerPath, + installJupyterPath, installPrometheusPath, managePrometheusPath, clusterStatus, @@ -51,6 +52,7 @@ export default class Clusters { installIngressEndpoint: installIngressPath, installRunnerEndpoint: installRunnerPath, installPrometheusEndpoint: installPrometheusPath, + installJupyterEndpoint: installJupyterPath, }); this.installApplication = this.installApplication.bind(this); diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index 9c12b89240c..e03db7b8974 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -37,6 +37,11 @@ export default { default: '', }, }, + data() { + return { + jupyterSuggestHostnameValue: '', + }; + }, computed: { generalApplicationDescription() { return sprintf( @@ -121,6 +126,20 @@ export default { false, ); }, + jupyterInstalled() { + return this.applications.jupyter.status === APPLICATION_INSTALLED; + }, + jupyterHostname() { + return this.applications.jupyter.hostname; + }, + jupyterSuggestHostname() { + return `jupyter.${this.applications.ingress.externalIp}.xip.io`; + }, + }, + watch: { + jupyterSuggestHostname() { + this.jupyterSuggestHostnameValue = this.jupyterSuggestHostname; + }, }, }; </script> @@ -278,11 +297,89 @@ export default { applications to production.`) }} </div> </application-row> + <application-row + id="jupyter" + :title="applications.jupyter.title" + title-link="https://jupyterhub.readthedocs.io/en/stable/" + :status="applications.jupyter.status" + :status-reason="applications.jupyter.statusReason" + :request-status="applications.jupyter.requestStatus" + :request-reason="applications.jupyter.requestReason" + > + <div slot="description"> + <p> + {{ s__(`ClusterIntegration|JupyterHub, a multi-user Hub, spawns, + manages, and proxies multiple instances of the single-user + Jupyter notebook server. JupyterHub can be used to serve + notebooks to a class of students, a corporate data science group, + or a scientific research group.`) }} + </p> + <template v-if="jupyterInstalled"> + <div class="form-group"> + <label for="jupyter-hostname"> + {{ s__('ClusterIntegration|Jupyter Hostname') }} + </label> + <div + v-if="jupyterHostname" + class="input-group" + > + <input + type="text" + id="jupyter-hostname" + class="form-control js-hostname" + :value="jupyterHostname" + readonly + /> + <span class="input-group-btn"> + <clipboard-button + :text="jupyterHostname" + :title="s__('ClusterIntegration|Copy Jupyter Hostname to clipboard')" + class="js-clipboard-btn" + /> + </span> + </div> + </div> + </template> + <template v-else-if="ingressInstalled"> + <div class="form-group"> + <label for="jupyter-hostname"> + {{ s__('ClusterIntegration|Jupyter Hostname') }} + </label> + <div class="input-group"> + <input + type="text" + id="jupyter-hostname" + class="form-control js-hostname" + v-model="jupyterSuggestHostnameValue" + /> + <span class="input-group-btn"> + <clipboard-button + :text="jupyterHostname" + :title="s__('ClusterIntegration|Copy Jupyter Hostname to clipboard')" + class="js-clipboard-btn" + /> + </span> + </div> + </div> + <p> + {{ s__(`ClusterIntegration|Replace this with your own hostname if you want. + If you do so, point hostname to Ingress IP Address from above.`) }} + <a + :href="ingressDnsHelpPath" + target="_blank" + rel="noopener noreferrer" + > + {{ __('More information') }} + </a> + </p> + </template> + </div> + </application-row> <!-- NOTE: Don't forget to update `clusters.scss` min-height for this block and uncomment `application_spec` tests --> - <!-- Add GitLab Runner row, all other plumbing is complete --> + <!-- Add Jupyter row, all other plumbing is complete --> </div> </div> </section> diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js index b7179f52bb3..371f71fde44 100644 --- a/app/assets/javascripts/clusters/constants.js +++ b/app/assets/javascripts/clusters/constants.js @@ -11,3 +11,4 @@ export const REQUEST_LOADING = 'request-loading'; export const REQUEST_SUCCESS = 'request-success'; export const REQUEST_FAILURE = 'request-failure'; export const INGRESS = 'ingress'; +export const JUPYTER = 'jupyter'; diff --git a/app/assets/javascripts/clusters/services/clusters_service.js b/app/assets/javascripts/clusters/services/clusters_service.js index 13468578f4f..e49db9c2f4f 100644 --- a/app/assets/javascripts/clusters/services/clusters_service.js +++ b/app/assets/javascripts/clusters/services/clusters_service.js @@ -1,4 +1,5 @@ import axios from '../../lib/utils/axios_utils'; +import { JUPYTER } from '../constants'; export default class ClusterService { constructor(options = {}) { @@ -8,6 +9,7 @@ export default class ClusterService { ingress: this.options.installIngressEndpoint, runner: this.options.installRunnerEndpoint, prometheus: this.options.installPrometheusEndpoint, + jupyter: this.options.installJupyterEndpoint, }; } @@ -16,7 +18,13 @@ export default class ClusterService { } installApplication(appId) { - return axios.post(this.appInstallEndpointMap[appId]); + const data = {}; + + if (appId === JUPYTER) { + data.hostname = document.getElementById('jupyter-hostname').value; + } + + return axios.post(this.appInstallEndpointMap[appId], data); } static updateCluster(endpoint, data) { diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js index 348bbec3b25..f609b425190 100644 --- a/app/assets/javascripts/clusters/stores/clusters_store.js +++ b/app/assets/javascripts/clusters/stores/clusters_store.js @@ -1,5 +1,5 @@ import { s__ } from '../../locale'; -import { INGRESS } from '../constants'; +import { INGRESS, JUPYTER } from '../constants'; export default class ClusterStore { constructor() { @@ -38,6 +38,14 @@ export default class ClusterStore { requestStatus: null, requestReason: null, }, + jupyter: { + title: s__('ClusterIntegration|JupyterHub'), + status: null, + statusReason: null, + requestStatus: null, + requestReason: null, + hostname: null, + }, }, }; } @@ -83,6 +91,8 @@ export default class ClusterStore { if (appId === INGRESS) { this.state.applications.ingress.externalIp = serverAppEntry.external_ip; + } else if (appId === JUPYTER) { + this.state.applications.jupyter.hostname = serverAppEntry.hostname; } }); } diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss index 3fd13078131..cfcce91f514 100644 --- a/app/assets/stylesheets/pages/clusters.scss +++ b/app/assets/stylesheets/pages/clusters.scss @@ -6,7 +6,7 @@ .cluster-applications-table { // Wait for the Vue to kick-in and render the applications block - min-height: 400px; + min-height: 500px; } .clusters-dropdown-menu { diff --git a/app/controllers/projects/clusters/applications_controller.rb b/app/controllers/projects/clusters/applications_controller.rb index 35885543622..9198a66b73d 100644 --- a/app/controllers/projects/clusters/applications_controller.rb +++ b/app/controllers/projects/clusters/applications_controller.rb @@ -6,6 +6,7 @@ class Projects::Clusters::ApplicationsController < Projects::ApplicationControll def create application = @application_class.find_or_create_by!(cluster: @cluster) + application.update(hostname: params[:hostname]) if application.respond_to?(:hostname) Clusters::Applications::ScheduleInstallationService.new(project, current_user).execute(application) diff --git a/app/models/clusters/applications/jupyter.rb b/app/models/clusters/applications/jupyter.rb index ec75c120dac..ef62be34abd 100644 --- a/app/models/clusters/applications/jupyter.rb +++ b/app/models/clusters/applications/jupyter.rb @@ -12,17 +12,39 @@ module Clusters default_value_for :version, VERSION def chart - # TODO: publish jupyterhub charts that we can use for our installation - # and provide path to it here. + "#{name}/jupyterhub" + end + + def repository + 'https://jupyterhub.github.io/helm-chart/' + end + + def values + content_values.to_yaml end def install_command Gitlab::Kubernetes::Helm::InstallCommand.new( name, chart: chart, - values: values + values: values, + repository: repository ) end + + private + + def specification + { + "ingress" => { "hosts" => [hostname] }, + "hub" => { "cookieSecret" => SecureRandom.hex(32) }, + "proxy" => { "secretToken" => SecureRandom.hex(32) } + } + end + + def content_values + YAML.load_file(chart_values_file).deep_merge!(specification) + end end end end diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 92e5da77066..d99f858e0c0 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -27,6 +27,7 @@ module Clusters has_one :application_ingress, class_name: 'Clusters::Applications::Ingress' has_one :application_prometheus, class_name: 'Clusters::Applications::Prometheus' has_one :application_runner, class_name: 'Clusters::Applications::Runner' + has_one :application_jupyter, class_name: 'Clusters::Applications::Jupyter' accepts_nested_attributes_for :provider_gcp, update_only: true accepts_nested_attributes_for :platform_kubernetes, update_only: true @@ -75,7 +76,8 @@ module Clusters application_helm || build_application_helm, application_ingress || build_application_ingress, application_prometheus || build_application_prometheus, - application_runner || build_application_runner + application_runner || build_application_runner, + application_jupyter || build_application_jupyter ] end diff --git a/app/serializers/cluster_application_entity.rb b/app/serializers/cluster_application_entity.rb index b22a0b666ef..77fc3336521 100644 --- a/app/serializers/cluster_application_entity.rb +++ b/app/serializers/cluster_application_entity.rb @@ -3,4 +3,5 @@ class ClusterApplicationEntity < Grape::Entity expose :status_name, as: :status expose :status_reason expose :external_ip, if: -> (e, _) { e.respond_to?(:external_ip) } + expose :hostname, if: -> (e, _) { e.respond_to?(:hostname) } end diff --git a/app/services/clusters/applications/install_service.rb b/app/services/clusters/applications/install_service.rb index 4c25a09814b..7ec3a9baa6e 100644 --- a/app/services/clusters/applications/install_service.rb +++ b/app/services/clusters/applications/install_service.rb @@ -12,8 +12,8 @@ module Clusters ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id) rescue Kubeclient::HttpError => ke app.make_errored!("Kubernetes error: #{ke.message}") - rescue StandardError - app.make_errored!("Can't start installation process") + rescue StandardError => e + app.make_errored!("Can't start installation process. #{e.message}") end end end diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml index 4c510293204..08d2deff6f8 100644 --- a/app/views/projects/clusters/show.html.haml +++ b/app/views/projects/clusters/show.html.haml @@ -11,6 +11,7 @@ install_ingress_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :ingress), install_prometheus_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :prometheus), install_runner_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :runner), + install_jupyter_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :jupyter), toggle_status: @cluster.enabled? ? 'true': 'false', cluster_status: @cluster.status_name, cluster_status_reason: @cluster.status_reason, |