summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>2018-06-01 07:58:18 +0000
committerDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>2018-06-01 07:58:18 +0000
commitd68ded2122f899bc0f478253652c4a082c98ec60 (patch)
tree945c0cd65b78b25c2302ff7eed254367a3f5ecf9
parent83510980497ca72eb43fa9540be7abcbb6e811fc (diff)
parent69e9e957318f8c803461dc3bec7fc04d3ad50a72 (diff)
downloadgitlab-ce-d68ded2122f899bc0f478253652c4a082c98ec60.tar.gz
Merge branch '46487-add-support-for-jupyter-in-gitlab-via-kubernetes' into 'master'
Resolve "Add support for Jupyter in GitLab via Kubernetes" Closes #46487 See merge request gitlab-org/gitlab-ce!19019
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js7
-rw-r--r--app/assets/javascripts/clusters/components/application_row.vue10
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue64
-rw-r--r--app/assets/javascripts/clusters/constants.js1
-rw-r--r--app/assets/javascripts/clusters/services/clusters_service.js5
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js16
-rw-r--r--app/assets/stylesheets/pages/clusters.scss2
-rw-r--r--app/controllers/projects/clusters/applications_controller.rb23
-rw-r--r--app/models/clusters/applications/jupyter.rb92
-rw-r--r--app/models/clusters/cluster.rb8
-rw-r--r--app/serializers/cluster_application_entity.rb1
-rw-r--r--app/services/clusters/applications/install_service.rb4
-rw-r--r--app/views/projects/clusters/show.html.haml1
-rw-r--r--changelogs/unreleased/46487-add-support-for-jupyter-in-gitlab-via-kubernetes.yml5
-rw-r--r--db/migrate/20180511131058_create_clusters_applications_jupyter.rb23
-rw-r--r--db/schema.rb13
-rw-r--r--doc/user/project/clusters/index.md1
-rw-r--r--spec/factories/clusters/applications/helm.rb3
-rw-r--r--spec/fixtures/api/schemas/cluster_status.json3
-rw-r--r--spec/javascripts/clusters/clusters_bundle_spec.js32
-rw-r--r--spec/javascripts/clusters/components/application_row_spec.js22
-rw-r--r--spec/javascripts/clusters/components/applications_spec.js94
-rw-r--r--spec/javascripts/clusters/services/mock_data.js35
-rw-r--r--spec/javascripts/clusters/stores/clusters_store_spec.js18
-rw-r--r--spec/models/clusters/applications/jupyter_spec.rb59
-rw-r--r--spec/models/clusters/cluster_spec.rb3
-rw-r--r--vendor/jupyter/values.yaml19
27 files changed, 535 insertions, 29 deletions
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index 01aec4f36af..e42a3632e79 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);
@@ -209,11 +211,12 @@ export default class Clusters {
}
}
- installApplication(appId) {
+ installApplication(data) {
+ const appId = data.id;
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_LOADING);
this.store.updateAppProperty(appId, 'requestReason', null);
- this.service.installApplication(appId)
+ this.service.installApplication(appId, data.params)
.then(() => {
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUCCESS);
})
diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue
index fae580c091b..30567993322 100644
--- a/app/assets/javascripts/clusters/components/application_row.vue
+++ b/app/assets/javascripts/clusters/components/application_row.vue
@@ -52,6 +52,11 @@
type: String,
required: false,
},
+ installApplicationRequestParams: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
},
computed: {
rowJsClass() {
@@ -109,7 +114,10 @@
},
methods: {
installClicked() {
- eventHub.$emit('installApplication', this.id);
+ eventHub.$emit('installApplication', {
+ id: this.id,
+ params: this.installApplicationRequestParams,
+ });
},
},
};
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index bb5fcea648d..9d6be555a2c 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -121,6 +121,12 @@ export default {
false,
);
},
+ jupyterInstalled() {
+ return this.applications.jupyter.status === APPLICATION_INSTALLED;
+ },
+ jupyterHostname() {
+ return this.applications.jupyter.hostname;
+ },
},
};
</script>
@@ -278,11 +284,67 @@ 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"
+ :install-application-request-params="{ hostname: applications.jupyter.hostname }"
+ >
+ <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="ingressExternalIp">
+ <div class="form-group">
+ <label for="jupyter-hostname">
+ {{ s__('ClusterIntegration|Jupyter Hostname') }}
+ </label>
+
+ <div class="input-group">
+ <input
+ type="text"
+ class="form-control js-hostname"
+ v-model="applications.jupyter.hostname"
+ :readonly="jupyterInstalled"
+ />
+ <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 v-if="ingressInstalled">
+ {{ 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 -->
</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..a7d82292ba9 100644
--- a/app/assets/javascripts/clusters/services/clusters_service.js
+++ b/app/assets/javascripts/clusters/services/clusters_service.js
@@ -8,6 +8,7 @@ export default class ClusterService {
ingress: this.options.installIngressEndpoint,
runner: this.options.installRunnerEndpoint,
prometheus: this.options.installPrometheusEndpoint,
+ jupyter: this.options.installJupyterEndpoint,
};
}
@@ -15,8 +16,8 @@ export default class ClusterService {
return axios.get(this.options.endpoint);
}
- installApplication(appId) {
- return axios.post(this.appInstallEndpointMap[appId]);
+ installApplication(appId, params) {
+ return axios.post(this.appInstallEndpointMap[appId], params);
}
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..3a4ac09f67c 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,12 @@ 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 ||
+ (this.state.applications.ingress.externalIp
+ ? `jupyter.${this.state.applications.ingress.externalIp}.xip.io`
+ : '');
}
});
}
diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss
index 2b6d92016d5..3e4d123242c 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: 628px;
}
.clusters-dropdown-menu {
diff --git a/app/controllers/projects/clusters/applications_controller.rb b/app/controllers/projects/clusters/applications_controller.rb
index 35885543622..4d758402850 100644
--- a/app/controllers/projects/clusters/applications_controller.rb
+++ b/app/controllers/projects/clusters/applications_controller.rb
@@ -5,7 +5,17 @@ class Projects::Clusters::ApplicationsController < Projects::ApplicationControll
before_action :authorize_create_cluster!, only: [:create]
def create
- application = @application_class.find_or_create_by!(cluster: @cluster)
+ application = @application_class.find_or_initialize_by(cluster: @cluster)
+
+ if application.has_attribute?(:hostname)
+ application.hostname = params[:hostname]
+ end
+
+ if application.respond_to?(:oauth_application)
+ application.oauth_application = create_oauth_application(application)
+ end
+
+ application.save!
Clusters::Applications::ScheduleInstallationService.new(project, current_user).execute(application)
@@ -23,4 +33,15 @@ class Projects::Clusters::ApplicationsController < Projects::ApplicationControll
def application_class
@application_class ||= Clusters::Cluster::APPLICATIONS[params[:application]] || render_404
end
+
+ def create_oauth_application(application)
+ oauth_application_params = {
+ name: params[:application],
+ redirect_uri: application.callback_url,
+ scopes: 'api read_user openid',
+ owner: current_user
+ }
+
+ Applications::CreateService.new(current_user, oauth_application_params).execute
+ end
end
diff --git a/app/models/clusters/applications/jupyter.rb b/app/models/clusters/applications/jupyter.rb
new file mode 100644
index 00000000000..975d434e1a4
--- /dev/null
+++ b/app/models/clusters/applications/jupyter.rb
@@ -0,0 +1,92 @@
+module Clusters
+ module Applications
+ class Jupyter < ActiveRecord::Base
+ VERSION = '0.0.1'.freeze
+
+ self.table_name = 'clusters_applications_jupyter'
+
+ include ::Clusters::Concerns::ApplicationCore
+ include ::Clusters::Concerns::ApplicationStatus
+ include ::Clusters::Concerns::ApplicationData
+
+ belongs_to :oauth_application, class_name: 'Doorkeeper::Application'
+
+ default_value_for :version, VERSION
+
+ def set_initial_status
+ return unless not_installable?
+
+ if cluster&.application_ingress_installed? && cluster.application_ingress.external_ip
+ self.status = 'installable'
+ end
+ end
+
+ def chart
+ "#{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,
+ repository: repository
+ )
+ end
+
+ def callback_url
+ "http://#{hostname}/hub/oauth_callback"
+ end
+
+ private
+
+ def specification
+ {
+ "ingress" => {
+ "hosts" => [hostname]
+ },
+ "hub" => {
+ "extraEnv" => {
+ "GITLAB_HOST" => gitlab_url
+ },
+ "cookieSecret" => cookie_secret
+ },
+ "proxy" => {
+ "secretToken" => secret_token
+ },
+ "auth" => {
+ "gitlab" => {
+ "clientId" => oauth_application.uid,
+ "clientSecret" => oauth_application.secret,
+ "callbackUrl" => callback_url
+ }
+ }
+ }
+ end
+
+ def gitlab_url
+ Gitlab.config.gitlab.url
+ end
+
+ def content_values
+ YAML.load_file(chart_values_file).deep_merge!(specification)
+ end
+
+ def secret_token
+ @secret_token ||= SecureRandom.hex(32)
+ end
+
+ def cookie_secret
+ @cookie_secret ||= SecureRandom.hex(32)
+ end
+ end
+ end
+end
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 77947d515c1..b426b1bf8a1 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -8,7 +8,8 @@ module Clusters
Applications::Helm.application_name => Applications::Helm,
Applications::Ingress.application_name => Applications::Ingress,
Applications::Prometheus.application_name => Applications::Prometheus,
- Applications::Runner.application_name => Applications::Runner
+ Applications::Runner.application_name => Applications::Runner,
+ Applications::Jupyter.application_name => Applications::Jupyter
}.freeze
DEFAULT_ENVIRONMENT = '*'.freeze
@@ -26,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
@@ -39,6 +41,7 @@ module Clusters
delegate :active?, to: :platform_kubernetes, prefix: true, allow_nil: true
delegate :installed?, to: :application_helm, prefix: true, allow_nil: true
+ delegate :installed?, to: :application_ingress, prefix: true, allow_nil: true
enum platform_type: {
kubernetes: 1
@@ -74,7 +77,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,
diff --git a/changelogs/unreleased/46487-add-support-for-jupyter-in-gitlab-via-kubernetes.yml b/changelogs/unreleased/46487-add-support-for-jupyter-in-gitlab-via-kubernetes.yml
new file mode 100644
index 00000000000..782ffd9a928
--- /dev/null
+++ b/changelogs/unreleased/46487-add-support-for-jupyter-in-gitlab-via-kubernetes.yml
@@ -0,0 +1,5 @@
+---
+title: Adds JupyterHub to cluster applications
+merge_request: 19019
+author:
+type: added
diff --git a/db/migrate/20180511131058_create_clusters_applications_jupyter.rb b/db/migrate/20180511131058_create_clusters_applications_jupyter.rb
new file mode 100644
index 00000000000..f3923884e37
--- /dev/null
+++ b/db/migrate/20180511131058_create_clusters_applications_jupyter.rb
@@ -0,0 +1,23 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class CreateClustersApplicationsJupyter < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :clusters_applications_jupyter do |t|
+ t.references :cluster, null: false, unique: true, foreign_key: { on_delete: :cascade }
+ t.references :oauth_application, foreign_key: { on_delete: :nullify }
+
+ t.integer :status, null: false
+ t.string :version, null: false
+ t.string :hostname
+
+ t.timestamps_with_timezone null: false
+
+ t.text :status_reason
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index f8663574580..97247387bc7 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -635,6 +635,17 @@ ActiveRecord::Schema.define(version: 20180529093006) do
t.string "external_ip"
end
+ create_table "clusters_applications_jupyter", force: :cascade do |t|
+ t.integer "cluster_id", null: false
+ t.integer "oauth_application_id"
+ t.integer "status", null: false
+ t.string "version", null: false
+ t.string "hostname"
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.text "status_reason"
+ end
+
create_table "clusters_applications_prometheus", force: :cascade do |t|
t.integer "cluster_id", null: false
t.integer "status", null: false
@@ -2196,6 +2207,8 @@ ActiveRecord::Schema.define(version: 20180529093006) do
add_foreign_key "clusters", "users", on_delete: :nullify
add_foreign_key "clusters_applications_helm", "clusters", on_delete: :cascade
add_foreign_key "clusters_applications_ingress", "clusters", name: "fk_753a7b41c1", on_delete: :cascade
+ add_foreign_key "clusters_applications_jupyter", "clusters", on_delete: :cascade
+ add_foreign_key "clusters_applications_jupyter", "oauth_applications", on_delete: :nullify
add_foreign_key "clusters_applications_prometheus", "clusters", name: "fk_557e773639", on_delete: :cascade
add_foreign_key "clusters_applications_runners", "ci_runners", column: "runner_id", name: "fk_02de2ded36", on_delete: :nullify
add_foreign_key "clusters_applications_runners", "clusters", on_delete: :cascade
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index edb875bc7e6..65cdece8d3d 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -156,6 +156,7 @@ added directly to your configured cluster. Those applications are needed for
| [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) | 10.2+ | Ingress can provide load balancing, SSL termination, and name-based virtual hosting. It acts as a web proxy for your applications and is useful if you want to use [Auto DevOps] or deploy your own web apps. |
| [Prometheus](https://prometheus.io/docs/introduction/overview/) | 10.4+ | Prometheus is an open-source monitoring and alerting system useful to supervise your deployed applications |
| [GitLab Runner](https://docs.gitlab.com/runner/) | 10.6+ | GitLab Runner is the open source project that is used to run your jobs and send the results back to GitLab. It is used in conjunction with [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/), the open-source continuous integration service included with GitLab that coordinates the jobs. When installing the GitLab Runner via the applications, it will run in **privileged mode** by default. Make sure you read the [security implications](#security-implications) before doing so. |
+| [JupyterHub](http://jupyter.org/) | 11.0+ | The Jupyter Notebook is an open-source web application that allows you to create and share documents that contain live code, equations, visualizations and narrative text. |
## Getting the external IP address
diff --git a/spec/factories/clusters/applications/helm.rb b/spec/factories/clusters/applications/helm.rb
index 3deca103578..3e4277e4ba6 100644
--- a/spec/factories/clusters/applications/helm.rb
+++ b/spec/factories/clusters/applications/helm.rb
@@ -35,5 +35,8 @@ FactoryBot.define do
factory :clusters_applications_ingress, class: Clusters::Applications::Ingress
factory :clusters_applications_prometheus, class: Clusters::Applications::Prometheus
factory :clusters_applications_runner, class: Clusters::Applications::Runner
+ factory :clusters_applications_jupyter, class: Clusters::Applications::Jupyter do
+ oauth_application factory: :oauth_application
+ end
end
end
diff --git a/spec/fixtures/api/schemas/cluster_status.json b/spec/fixtures/api/schemas/cluster_status.json
index d27c12e43f2..ccef17a6615 100644
--- a/spec/fixtures/api/schemas/cluster_status.json
+++ b/spec/fixtures/api/schemas/cluster_status.json
@@ -31,7 +31,8 @@
}
},
"status_reason": { "type": ["string", "null"] },
- "external_ip": { "type": ["string", "null"] }
+ "external_ip": { "type": ["string", "null"] },
+ "hostname": { "type": ["string", "null"] }
},
"required" : [ "name", "status" ]
}
diff --git a/spec/javascripts/clusters/clusters_bundle_spec.js b/spec/javascripts/clusters/clusters_bundle_spec.js
index a5cd247b689..abe2954d506 100644
--- a/spec/javascripts/clusters/clusters_bundle_spec.js
+++ b/spec/javascripts/clusters/clusters_bundle_spec.js
@@ -207,11 +207,11 @@ describe('Clusters', () => {
spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve());
expect(cluster.store.state.applications.helm.requestStatus).toEqual(null);
- cluster.installApplication('helm');
+ cluster.installApplication({ id: 'helm' });
expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_LOADING);
expect(cluster.store.state.applications.helm.requestReason).toEqual(null);
- expect(cluster.service.installApplication).toHaveBeenCalledWith('helm');
+ expect(cluster.service.installApplication).toHaveBeenCalledWith('helm', undefined);
getSetTimeoutPromise()
.then(() => {
@@ -226,11 +226,11 @@ describe('Clusters', () => {
spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve());
expect(cluster.store.state.applications.ingress.requestStatus).toEqual(null);
- cluster.installApplication('ingress');
+ cluster.installApplication({ id: 'ingress' });
expect(cluster.store.state.applications.ingress.requestStatus).toEqual(REQUEST_LOADING);
expect(cluster.store.state.applications.ingress.requestReason).toEqual(null);
- expect(cluster.service.installApplication).toHaveBeenCalledWith('ingress');
+ expect(cluster.service.installApplication).toHaveBeenCalledWith('ingress', undefined);
getSetTimeoutPromise()
.then(() => {
@@ -245,11 +245,11 @@ describe('Clusters', () => {
spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve());
expect(cluster.store.state.applications.runner.requestStatus).toEqual(null);
- cluster.installApplication('runner');
+ cluster.installApplication({ id: 'runner' });
expect(cluster.store.state.applications.runner.requestStatus).toEqual(REQUEST_LOADING);
expect(cluster.store.state.applications.runner.requestReason).toEqual(null);
- expect(cluster.service.installApplication).toHaveBeenCalledWith('runner');
+ expect(cluster.service.installApplication).toHaveBeenCalledWith('runner', undefined);
getSetTimeoutPromise()
.then(() => {
@@ -260,11 +260,29 @@ describe('Clusters', () => {
.catch(done.fail);
});
+ it('tries to install jupyter', (done) => {
+ spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve());
+ expect(cluster.store.state.applications.jupyter.requestStatus).toEqual(null);
+ cluster.installApplication({ id: 'jupyter', params: { hostname: cluster.store.state.applications.jupyter.hostname } });
+
+ expect(cluster.store.state.applications.jupyter.requestStatus).toEqual(REQUEST_LOADING);
+ expect(cluster.store.state.applications.jupyter.requestReason).toEqual(null);
+ expect(cluster.service.installApplication).toHaveBeenCalledWith('jupyter', { hostname: cluster.store.state.applications.jupyter.hostname });
+
+ getSetTimeoutPromise()
+ .then(() => {
+ expect(cluster.store.state.applications.jupyter.requestStatus).toEqual(REQUEST_SUCCESS);
+ expect(cluster.store.state.applications.jupyter.requestReason).toEqual(null);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
it('sets error request status when the request fails', (done) => {
spyOn(cluster.service, 'installApplication').and.returnValue(Promise.reject(new Error('STUBBED ERROR')));
expect(cluster.store.state.applications.helm.requestStatus).toEqual(null);
- cluster.installApplication('helm');
+ cluster.installApplication({ id: 'helm' });
expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_LOADING);
expect(cluster.store.state.applications.helm.requestReason).toEqual(null);
diff --git a/spec/javascripts/clusters/components/application_row_spec.js b/spec/javascripts/clusters/components/application_row_spec.js
index 2c4707bb856..c83cbe90a57 100644
--- a/spec/javascripts/clusters/components/application_row_spec.js
+++ b/spec/javascripts/clusters/components/application_row_spec.js
@@ -174,7 +174,27 @@ describe('Application Row', () => {
installButton.click();
- expect(eventHub.$emit).toHaveBeenCalledWith('installApplication', DEFAULT_APPLICATION_STATE.id);
+ expect(eventHub.$emit).toHaveBeenCalledWith('installApplication', {
+ id: DEFAULT_APPLICATION_STATE.id,
+ params: {},
+ });
+ });
+
+ it('clicking install button when installApplicationRequestParams are provided emits event', () => {
+ spyOn(eventHub, '$emit');
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: APPLICATION_INSTALLABLE,
+ installApplicationRequestParams: { hostname: 'jupyter' },
+ });
+ const installButton = vm.$el.querySelector('.js-cluster-application-install-button');
+
+ installButton.click();
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('installApplication', {
+ id: DEFAULT_APPLICATION_STATE.id,
+ params: { hostname: 'jupyter' },
+ });
});
it('clicking disabled install button emits nothing', () => {
diff --git a/spec/javascripts/clusters/components/applications_spec.js b/spec/javascripts/clusters/components/applications_spec.js
index d546543d273..a70138c7eee 100644
--- a/spec/javascripts/clusters/components/applications_spec.js
+++ b/spec/javascripts/clusters/components/applications_spec.js
@@ -22,6 +22,7 @@ describe('Applications', () => {
ingress: { title: 'Ingress' },
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
+ jupyter: { title: 'JupyterHub' },
},
});
});
@@ -41,6 +42,10 @@ describe('Applications', () => {
it('renders a row for GitLab Runner', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-runner')).toBeDefined();
});
+
+ it('renders a row for Jupyter', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-jupyter')).not.toBe(null);
+ });
});
describe('Ingress application', () => {
@@ -57,12 +62,11 @@ describe('Applications', () => {
helm: { title: 'Helm Tiller' },
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
+ jupyter: { title: 'JupyterHub', hostname: '' },
},
});
- expect(
- vm.$el.querySelector('.js-ip-address').value,
- ).toEqual('0.0.0.0');
+ expect(vm.$el.querySelector('.js-ip-address').value).toEqual('0.0.0.0');
expect(
vm.$el.querySelector('.js-clipboard-btn').getAttribute('data-clipboard-text'),
@@ -81,12 +85,11 @@ describe('Applications', () => {
helm: { title: 'Helm Tiller' },
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
+ jupyter: { title: 'JupyterHub', hostname: '' },
},
});
- expect(
- vm.$el.querySelector('.js-ip-address').value,
- ).toEqual('?');
+ expect(vm.$el.querySelector('.js-ip-address').value).toEqual('?');
expect(vm.$el.querySelector('.js-no-ip-message')).not.toBe(null);
});
@@ -101,6 +104,7 @@ describe('Applications', () => {
ingress: { title: 'Ingress' },
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
+ jupyter: { title: 'JupyterHub', hostname: '' },
},
});
@@ -108,5 +112,83 @@ describe('Applications', () => {
expect(vm.$el.querySelector('.js-ip-address')).toBe(null);
});
});
+
+ describe('Jupyter application', () => {
+ describe('with ingress installed with ip & jupyter installable', () => {
+ it('renders hostname active input', () => {
+ vm = mountComponent(Applications, {
+ applications: {
+ helm: { title: 'Helm Tiller', status: 'installed' },
+ ingress: { title: 'Ingress', status: 'installed', externalIp: '1.1.1.1' },
+ runner: { title: 'GitLab Runner' },
+ prometheus: { title: 'Prometheus' },
+ jupyter: { title: 'JupyterHub', hostname: '', status: 'installable' },
+ },
+ });
+
+ expect(vm.$el.querySelector('.js-hostname').getAttribute('readonly')).toEqual(null);
+ });
+ });
+
+ describe('with ingress installed without external ip', () => {
+ it('does not render hostname input', () => {
+ vm = mountComponent(Applications, {
+ applications: {
+ helm: { title: 'Helm Tiller', status: 'installed' },
+ ingress: { title: 'Ingress', status: 'installed' },
+ runner: { title: 'GitLab Runner' },
+ prometheus: { title: 'Prometheus' },
+ jupyter: { title: 'JupyterHub', hostname: '', status: 'installable' },
+ },
+ });
+
+ expect(vm.$el.querySelector('.js-hostname')).toBe(null);
+ });
+ });
+
+ describe('with ingress & jupyter installed', () => {
+ it('renders readonly input', () => {
+ vm = mountComponent(Applications, {
+ applications: {
+ helm: { title: 'Helm Tiller', status: 'installed' },
+ ingress: { title: 'Ingress', status: 'installed', externalIp: '1.1.1.1' },
+ runner: { title: 'GitLab Runner' },
+ prometheus: { title: 'Prometheus' },
+ jupyter: { title: 'JupyterHub', status: 'installed', hostname: '' },
+ },
+ });
+
+ expect(vm.$el.querySelector('.js-hostname').getAttribute('readonly')).toEqual('readonly');
+ });
+ });
+
+ describe('without ingress installed', () => {
+ beforeEach(() => {
+ vm = mountComponent(Applications, {
+ applications: {
+ helm: { title: 'Helm Tiller' },
+ ingress: { title: 'Ingress' },
+ runner: { title: 'GitLab Runner' },
+ prometheus: { title: 'Prometheus' },
+ jupyter: { title: 'JupyterHub', status: 'not_installable' },
+ },
+ });
+ });
+
+ it('does not render input', () => {
+ expect(vm.$el.querySelector('.js-hostname')).toBe(null);
+ });
+
+ it('renders disabled install button', () => {
+ expect(
+ vm.$el
+ .querySelector(
+ '.js-cluster-application-row-jupyter .js-cluster-application-install-button',
+ )
+ .getAttribute('disabled'),
+ ).toEqual('disabled');
+ });
+ });
+ });
});
});
diff --git a/spec/javascripts/clusters/services/mock_data.js b/spec/javascripts/clusters/services/mock_data.js
index 6ae7a792329..b2b0ebf840b 100644
--- a/spec/javascripts/clusters/services/mock_data.js
+++ b/spec/javascripts/clusters/services/mock_data.js
@@ -1,4 +1,5 @@
import {
+ APPLICATION_INSTALLED,
APPLICATION_INSTALLABLE,
APPLICATION_INSTALLING,
APPLICATION_ERROR,
@@ -28,6 +29,39 @@ const CLUSTERS_MOCK_DATA = {
name: 'prometheus',
status: APPLICATION_ERROR,
status_reason: 'Cannot connect',
+ }, {
+ name: 'jupyter',
+ status: APPLICATION_INSTALLING,
+ status_reason: 'Cannot connect',
+ }],
+ },
+ },
+ '/gitlab-org/gitlab-shell/clusters/2/status.json': {
+ data: {
+ status: 'errored',
+ status_reason: 'Failed to request to CloudPlatform.',
+ applications: [{
+ name: 'helm',
+ status: APPLICATION_INSTALLED,
+ status_reason: null,
+ }, {
+ name: 'ingress',
+ status: APPLICATION_INSTALLED,
+ status_reason: 'Cannot connect',
+ external_ip: '1.1.1.1',
+ }, {
+ name: 'runner',
+ status: APPLICATION_INSTALLING,
+ status_reason: null,
+ },
+ {
+ name: 'prometheus',
+ status: APPLICATION_ERROR,
+ status_reason: 'Cannot connect',
+ }, {
+ name: 'jupyter',
+ status: APPLICATION_INSTALLABLE,
+ status_reason: 'Cannot connect',
}],
},
},
@@ -37,6 +71,7 @@ const CLUSTERS_MOCK_DATA = {
'/gitlab-org/gitlab-shell/clusters/1/applications/ingress': { },
'/gitlab-org/gitlab-shell/clusters/1/applications/runner': { },
'/gitlab-org/gitlab-shell/clusters/1/applications/prometheus': { },
+ '/gitlab-org/gitlab-shell/clusters/1/applications/jupyter': { },
},
};
diff --git a/spec/javascripts/clusters/stores/clusters_store_spec.js b/spec/javascripts/clusters/stores/clusters_store_spec.js
index 8028faf2f02..6854b016852 100644
--- a/spec/javascripts/clusters/stores/clusters_store_spec.js
+++ b/spec/javascripts/clusters/stores/clusters_store_spec.js
@@ -91,8 +91,26 @@ describe('Clusters Store', () => {
requestStatus: null,
requestReason: null,
},
+ jupyter: {
+ title: 'JupyterHub',
+ status: mockResponseData.applications[4].status,
+ statusReason: mockResponseData.applications[4].status_reason,
+ requestStatus: null,
+ requestReason: null,
+ hostname: '',
+ },
},
});
});
+
+ it('sets default hostname for jupyter when ingress has a ip address', () => {
+ const mockResponseData = CLUSTERS_MOCK_DATA.GET['/gitlab-org/gitlab-shell/clusters/2/status.json'].data;
+
+ store.updateStateFromServer(mockResponseData);
+
+ expect(
+ store.state.applications.jupyter.hostname,
+ ).toEqual(`jupyter.${store.state.applications.ingress.externalIp}.xip.io`);
+ });
});
});
diff --git a/spec/models/clusters/applications/jupyter_spec.rb b/spec/models/clusters/applications/jupyter_spec.rb
new file mode 100644
index 00000000000..ca48a1d8072
--- /dev/null
+++ b/spec/models/clusters/applications/jupyter_spec.rb
@@ -0,0 +1,59 @@
+require 'rails_helper'
+
+describe Clusters::Applications::Jupyter do
+ include_examples 'cluster application core specs', :clusters_applications_jupyter
+
+ it { is_expected.to belong_to(:oauth_application) }
+
+ describe '#set_initial_status' do
+ before do
+ jupyter.set_initial_status
+ end
+
+ context 'when ingress is not installed' do
+ let(:cluster) { create(:cluster, :provided_by_gcp) }
+ let(:jupyter) { create(:clusters_applications_jupyter, cluster: cluster) }
+
+ it { expect(jupyter).to be_not_installable }
+ end
+
+ context 'when ingress is installed and external_ip is assigned' do
+ let(:ingress) { create(:clusters_applications_ingress, :installed, external_ip: '127.0.0.1') }
+ let(:jupyter) { create(:clusters_applications_jupyter, cluster: ingress.cluster) }
+
+ it { expect(jupyter).to be_installable }
+ end
+ end
+
+ describe '#install_command' do
+ let!(:ingress) { create(:clusters_applications_ingress, :installed, external_ip: '127.0.0.1') }
+ let!(:jupyter) { create(:clusters_applications_jupyter, cluster: ingress.cluster) }
+
+ subject { jupyter.install_command }
+
+ it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) }
+
+ it 'should be initialized with 4 arguments' do
+ expect(subject.name).to eq('jupyter')
+ expect(subject.chart).to eq('jupyter/jupyterhub')
+ expect(subject.repository).to eq('https://jupyterhub.github.io/helm-chart/')
+ expect(subject.values).to eq(jupyter.values)
+ end
+ end
+
+ describe '#values' do
+ let(:jupyter) { create(:clusters_applications_jupyter) }
+
+ subject { jupyter.values }
+
+ it 'should include valid values' do
+ is_expected.to include('ingress')
+ is_expected.to include('hub')
+ is_expected.to include('rbac')
+ is_expected.to include('proxy')
+ is_expected.to include('auth')
+ is_expected.to include("clientId: #{jupyter.oauth_application.uid}")
+ is_expected.to include("callbackUrl: #{jupyter.callback_url}")
+ end
+ end
+end
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index b942554d67b..6f66515b45f 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -234,9 +234,10 @@ describe Clusters::Cluster do
let!(:ingress) { create(:clusters_applications_ingress, cluster: cluster) }
let!(:prometheus) { create(:clusters_applications_prometheus, cluster: cluster) }
let!(:runner) { create(:clusters_applications_runner, cluster: cluster) }
+ let!(:jupyter) { create(:clusters_applications_jupyter, cluster: cluster) }
it 'returns a list of created applications' do
- is_expected.to contain_exactly(helm, ingress, prometheus, runner)
+ is_expected.to contain_exactly(helm, ingress, prometheus, runner, jupyter)
end
end
end
diff --git a/vendor/jupyter/values.yaml b/vendor/jupyter/values.yaml
new file mode 100644
index 00000000000..90817de0f1b
--- /dev/null
+++ b/vendor/jupyter/values.yaml
@@ -0,0 +1,19 @@
+rbac:
+ enabled: false
+
+hub:
+ extraEnv:
+ JUPYTER_ENABLE_LAB: 1
+ extraConfig: |
+ c.KubeSpawner.cmd = ['jupyter-labhub']
+
+auth:
+ type: gitlab
+
+singleuser:
+ defaultUrl: "/lab"
+
+ingress:
+ enabled: true
+ annotations:
+ kubernetes.io/ingress.class: "nginx"