diff options
35 files changed, 958 insertions, 300 deletions
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index c59fac42038..388f674f643 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -36,6 +36,7 @@ export default class Clusters { installRunnerPath, installJupyterPath, installKnativePath, + updateKnativePath, installPrometheusPath, managePrometheusPath, hasRbac, @@ -62,6 +63,7 @@ export default class Clusters { installPrometheusEndpoint: installPrometheusPath, installJupyterEndpoint: installJupyterPath, installKnativeEndpoint: installKnativePath, + updateKnativeEndpoint: updateKnativePath, }); this.installApplication = this.installApplication.bind(this); @@ -128,6 +130,8 @@ export default class Clusters { eventHub.$on('upgradeApplication', data => this.upgradeApplication(data)); eventHub.$on('upgradeFailed', appId => this.upgradeFailed(appId)); eventHub.$on('dismissUpgradeSuccess', appId => this.dismissUpgradeSuccess(appId)); + eventHub.$on('saveKnativeDomain', data => this.saveKnativeDomain(data)); + eventHub.$on('setKnativeHostname', data => this.setKnativeHostname(data)); } removeListeners() { @@ -136,6 +140,8 @@ export default class Clusters { eventHub.$off('upgradeApplication', this.upgradeApplication); eventHub.$off('upgradeFailed', this.upgradeFailed); eventHub.$off('dismissUpgradeSuccess', this.dismissUpgradeSuccess); + eventHub.$off('saveKnativeDomain'); + eventHub.$off('setKnativeHostname'); } initPolling() { @@ -271,6 +277,18 @@ export default class Clusters { this.store.updateAppProperty(appId, 'requestStatus', null); } + saveKnativeDomain(data) { + const appId = data.id; + this.store.updateAppProperty(appId, 'status', APPLICATION_STATUS.UPDATING); + this.service.updateApplication(appId, data.params); + } + + setKnativeHostname(data) { + const appId = data.id; + this.store.updateAppProperty(appId, 'isEditingHostName', true); + this.store.updateAppProperty(appId, 'hostname', data.hostname); + } + destroy() { this.destroyed = true; diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue index 5952e93b9a7..19e5ac1567d 100644 --- a/app/assets/javascripts/clusters/components/application_row.vue +++ b/app/assets/javascripts/clusters/components/application_row.vue @@ -191,14 +191,7 @@ export default { return this.status === APPLICATION_STATUS.UPDATE_ERRORED; }, upgradeFailureDescription() { - return sprintf( - s__( - 'ClusterIntegration|Something went wrong when upgrading %{title}. Please check the logs and try again.', - ), - { - title: this.title, - }, - ); + return s__('ClusterIntegration|Update failed. Please check the logs and try again.'); }, upgradeSuccessDescription() { return sprintf(s__('ClusterIntegration|%{title} upgraded successfully.'), { @@ -210,9 +203,9 @@ export default { if (this.upgradeAvailable && !this.upgradeFailed && !this.isUpgrading) { label = s__('ClusterIntegration|Upgrade'); } else if (this.isUpgrading) { - label = s__('ClusterIntegration|Upgrading'); + label = s__('ClusterIntegration|Updating'); } else if (this.upgradeFailed) { - label = s__('ClusterIntegration|Retry upgrade'); + label = s__('ClusterIntegration|Retry update'); } return label; @@ -224,6 +217,14 @@ export default { (this.upgradeRequested && !this.upgradeSuccessful) ); }, + shouldShowUpgradeDetails() { + // This method only returns true when; + // Upgrade was successful OR Upgrade failed + // AND new upgrade is unavailable AND version information is present. + return ( + (this.upgradeSuccessful || this.upgradeFailed) && !this.upgradeAvailable && this.version + ); + }, }, watch: { status() { @@ -303,7 +304,7 @@ export default { </div> <div - v-if="(upgradeSuccessful || upgradeFailed) && !upgradeAvailable" + v-if="shouldShowUpgradeDetails" class="form-text text-muted label p-0 js-cluster-application-upgrade-details" > {{ versionLabel }} diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index 0cf187d4189..f74cd71de04 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -15,11 +15,14 @@ import { s__, sprintf } from '../../locale'; import applicationRow from './application_row.vue'; import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; import { CLUSTER_TYPE, APPLICATION_STATUS, INGRESS } from '../constants'; +import LoadingButton from '~/vue_shared/components/loading_button.vue'; +import eventHub from '~/clusters/event_hub'; export default { components: { applicationRow, clipboardButton, + LoadingButton, }, props: { type: { @@ -173,16 +176,55 @@ export default { jupyterHostname() { return this.applications.jupyter.hostname; }, + knative() { + return this.applications.knative; + }, knativeInstalled() { - return this.applications.knative.status === APPLICATION_STATUS.INSTALLED; + return ( + this.knative.status === APPLICATION_STATUS.INSTALLED || + this.knativeUpgrading || + this.knativeUpgradeFailed || + this.knative.status === APPLICATION_STATUS.UPDATED + ); + }, + knativeUpgrading() { + return ( + this.knative.status === APPLICATION_STATUS.UPDATING || + this.knative.status === APPLICATION_STATUS.SCHEDULED + ); + }, + knativeUpgradeFailed() { + return this.knative.status === APPLICATION_STATUS.UPDATE_ERRORED; }, knativeExternalIp() { - return this.applications.knative.externalIp; + return this.knative.externalIp; + }, + canUpdateKnativeEndpoint() { + return this.knativeExternalIp && !this.knativeUpgradeFailed && !this.knativeUpgrading; + }, + knativeHostname: { + get() { + return this.knative.hostname; + }, + set(hostname) { + eventHub.$emit('setKnativeHostname', { + id: 'knative', + hostname, + }); + }, }, }, created() { this.helmInstallIllustration = helmInstallIllustration; }, + methods: { + saveKnativeDomain() { + eventHub.$emit('saveKnativeDomain', { + id: 'knative', + params: { hostname: this.knative.hostname }, + }); + }, + }, }; </script> @@ -471,76 +513,88 @@ export default { }} </p> - <template v-if="knativeInstalled"> - <div class="form-group"> - <label for="knative-domainname"> - {{ s__('ClusterIntegration|Knative Domain Name:') }} - </label> - <input - id="knative-domainname" - v-model="applications.knative.hostname" - type="text" - class="form-control js-domainname" - readonly - /> - </div> - </template> - <template v-else-if="helmInstalled && rbac"> - <div class="form-group"> - <label for="knative-domainname"> - {{ s__('ClusterIntegration|Knative Domain Name:') }} - </label> - <input - id="knative-domainname" - v-model="applications.knative.hostname" - type="text" - class="form-control js-domainname" - /> - </div> - </template> - <template v-if="knativeInstalled"> - <div class="form-group"> - <label for="knative-ip-address"> - {{ s__('ClusterIntegration|Knative IP Address:') }} - </label> - <div v-if="knativeExternalIp" class="input-group"> + <div class="row"> + <template v-if="knativeInstalled || (helmInstalled && rbac)"> + <div + :class="{ 'col-md-6': knativeInstalled, 'col-12': helmInstalled && rbac }" + class="form-group col-sm-12 mb-0" + > + <label for="knative-domainname"> + <strong> + {{ s__('ClusterIntegration|Knative Domain Name:') }} + </strong> + </label> <input - id="knative-ip-address" - :value="knativeExternalIp" + id="knative-domainname" + v-model="knativeHostname" type="text" - class="form-control js-ip-address" - readonly + class="form-control js-knative-domainname" /> - <span class="input-group-append"> - <clipboard-button - :text="knativeExternalIp" - :title="s__('ClusterIntegration|Copy Knative IP Address to clipboard')" - class="input-group-text js-clipboard-btn" + </div> + </template> + <template v-if="knativeInstalled"> + <div class="form-group col-sm-12 col-md-6 pl-md-0 mb-0 mt-3 mt-md-0"> + <label for="knative-ip-address"> + <strong> + {{ s__('ClusterIntegration|Knative Endpoint:') }} + </strong> + </label> + <div v-if="knativeExternalIp" class="input-group"> + <input + id="knative-ip-address" + :value="knativeExternalIp" + type="text" + class="form-control js-knative-ip-address" + readonly /> - </span> + <span class="input-group-append"> + <clipboard-button + :text="knativeExternalIp" + :title="s__('ClusterIntegration|Copy Knative Endpoint to clipboard')" + class="input-group-text js-knative-ip-clipboard-btn" + /> + </span> + </div> + <input + v-else + type="text" + class="form-control js-knative-ip-address" + readonly + value="?" + /> </div> - <input v-else type="text" class="form-control js-ip-address" readonly value="?" /> - </div> - <p v-if="!knativeExternalIp" class="settings-message js-no-ip-message"> - {{ - s__(`ClusterIntegration|The IP address is in - the process of being assigned. Please check your Kubernetes - cluster or Quotas on Google Kubernetes Engine if it takes a long time.`) - }} - </p> + <p class="form-text text-muted col-12"> + {{ + s__( + `ClusterIntegration|To access your application after deployment, point a wildcard DNS to the Knative Endpoint.`, + ) + }} + <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer"> + {{ __('More information') }} + </a> + </p> - <p> - {{ - s__(`ClusterIntegration|Point a wildcard DNS to this - generated IP address in order to access - your application after it has been deployed.`) - }} - <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer"> - {{ __('More information') }} - </a> - </p> - </template> + <p + v-if="!knativeExternalIp" + class="settings-message js-no-knative-ip-message mt-2 mr-3 mb-0 ml-3 " + > + {{ + s__(`ClusterIntegration|The IP address is in + the process of being assigned. Please check your Kubernetes + cluster or Quotas on Google Kubernetes Engine if it takes a long time.`) + }} + </p> + + <button + v-if="canUpdateKnativeEndpoint" + class="btn btn-success js-knative-save-domain-button mt-3 ml-3" + @click="saveKnativeDomain" + > + {{ s__('ClusterIntegration|Save changes') }} + </button> + </template> + </div> </div> </application-row> </div> diff --git a/app/assets/javascripts/clusters/services/clusters_service.js b/app/assets/javascripts/clusters/services/clusters_service.js index 89dda4b7902..dea33ac44c5 100644 --- a/app/assets/javascripts/clusters/services/clusters_service.js +++ b/app/assets/javascripts/clusters/services/clusters_service.js @@ -12,6 +12,9 @@ export default class ClusterService { jupyter: this.options.installJupyterEndpoint, knative: this.options.installKnativeEndpoint, }; + this.appUpdateEndpointMap = { + knative: this.options.updateKnativeEndpoint, + }; } fetchData() { @@ -22,6 +25,10 @@ export default class ClusterService { return axios.post(this.appInstallEndpointMap[appId], params); } + updateApplication(appId, params) { + return axios.patch(this.appUpdateEndpointMap[appId], params); + } + static updateCluster(endpoint, data) { return axios.put(endpoint, data); } diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js index d309678be27..3f03a8512fc 100644 --- a/app/assets/javascripts/clusters/stores/clusters_store.js +++ b/app/assets/javascripts/clusters/stores/clusters_store.js @@ -66,6 +66,7 @@ export default class ClusterStore { requestStatus: null, requestReason: null, hostname: null, + isEditingHostName: false, externalIp: null, }, }, @@ -129,8 +130,10 @@ export default class ClusterStore { ? `jupyter.${this.state.applications.ingress.externalIp}.nip.io` : ''); } else if (appId === KNATIVE) { - this.state.applications.knative.hostname = - serverAppEntry.hostname || this.state.applications.knative.hostname; + if (!this.state.applications.knative.isEditingHostName) { + this.state.applications.knative.hostname = + serverAppEntry.hostname || this.state.applications.knative.hostname; + } this.state.applications.knative.externalIp = serverAppEntry.external_ip || this.state.applications.knative.externalIp; } else if (appId === RUNNER) { diff --git a/app/controllers/clusters/applications_controller.rb b/app/controllers/clusters/applications_controller.rb index c4e7fc950f9..73c744efeba 100644 --- a/app/controllers/clusters/applications_controller.rb +++ b/app/controllers/clusters/applications_controller.rb @@ -3,26 +3,41 @@ class Clusters::ApplicationsController < Clusters::BaseController before_action :cluster before_action :authorize_create_cluster!, only: [:create] + before_action :authorize_update_cluster!, only: [:update] def create - Clusters::Applications::CreateService - .new(@cluster, current_user, create_cluster_application_params) - .execute(request) + request_handler do + Clusters::Applications::CreateService + .new(@cluster, current_user, cluster_application_params) + .execute(request) + end + end + + def update + request_handler do + Clusters::Applications::UpdateService + .new(@cluster, current_user, cluster_application_params) + .execute(request) + end + end + + private + + def request_handler + yield head :no_content - rescue Clusters::Applications::CreateService::InvalidApplicationError + rescue Clusters::Applications::BaseService::InvalidApplicationError render_404 rescue StandardError head :bad_request end - private - def cluster @cluster ||= clusterable.clusters.find(params[:id]) || render_404 end - def create_cluster_application_params + def cluster_application_params params.permit(:application, :hostname, :email) end end diff --git a/app/models/clusters/concerns/application_core.rb b/app/models/clusters/concerns/application_core.rb index 683b45331f6..ee964fb7c93 100644 --- a/app/models/clusters/concerns/application_core.rb +++ b/app/models/clusters/concerns/application_core.rb @@ -30,6 +30,12 @@ module Clusters # Override if you need extra data synchronized # from K8s after installation end + + def update_command + install_command.tap do |command| + command.version = version + end + end end end end diff --git a/app/presenters/clusterable_presenter.rb b/app/presenters/clusterable_presenter.rb index d94d9118eee..34bdf156623 100644 --- a/app/presenters/clusterable_presenter.rb +++ b/app/presenters/clusterable_presenter.rb @@ -44,6 +44,10 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated raise NotImplementedError end + def update_applications_cluster_path(cluster, application) + raise NotImplementedError + end + def cluster_path(cluster, params = {}) raise NotImplementedError end diff --git a/app/presenters/group_clusterable_presenter.rb b/app/presenters/group_clusterable_presenter.rb index ef6bbc0d109..f5b0bb64487 100644 --- a/app/presenters/group_clusterable_presenter.rb +++ b/app/presenters/group_clusterable_presenter.rb @@ -14,6 +14,11 @@ class GroupClusterablePresenter < ClusterablePresenter install_applications_group_cluster_path(clusterable, cluster, application) end + override :update_applications_cluster_path + def update_applications_cluster_path(cluster, application) + update_applications_group_cluster_path(clusterable, cluster, application) + end + override :cluster_path def cluster_path(cluster, params = {}) group_cluster_path(clusterable, cluster, params) diff --git a/app/presenters/project_clusterable_presenter.rb b/app/presenters/project_clusterable_presenter.rb index 63e69b91b11..8661ee02b68 100644 --- a/app/presenters/project_clusterable_presenter.rb +++ b/app/presenters/project_clusterable_presenter.rb @@ -14,6 +14,11 @@ class ProjectClusterablePresenter < ClusterablePresenter install_applications_project_cluster_path(clusterable, cluster, application) end + override :update_applications_cluster_path + def update_applications_cluster_path(cluster, application) + update_applications_project_cluster_path(clusterable, cluster, application) + end + override :cluster_path def cluster_path(cluster, params = {}) project_cluster_path(clusterable, cluster, params) diff --git a/app/services/clusters/applications/base_helm_service.rb b/app/services/clusters/applications/base_helm_service.rb index 8a71730d5ec..c38b2656260 100644 --- a/app/services/clusters/applications/base_helm_service.rb +++ b/app/services/clusters/applications/base_helm_service.rb @@ -46,6 +46,10 @@ module Clusters @install_command ||= app.install_command end + def update_command + @update_command ||= app.update_command + end + def upgrade_command(new_values = "") app.upgrade_command(new_values) end diff --git a/app/services/clusters/applications/base_service.rb b/app/services/clusters/applications/base_service.rb new file mode 100644 index 00000000000..cbd1cf03ae1 --- /dev/null +++ b/app/services/clusters/applications/base_service.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +module Clusters + module Applications + class BaseService + InvalidApplicationError = Class.new(StandardError) + + attr_reader :cluster, :current_user, :params + + def initialize(cluster, user, params = {}) + @cluster = cluster + @current_user = user + @params = params.dup + end + + def execute(request) + instantiate_application.tap do |application| + if application.has_attribute?(:hostname) + application.hostname = params[:hostname] + end + + if application.has_attribute?(:email) + application.email = params[:email] + end + + if application.respond_to?(:oauth_application) + application.oauth_application = create_oauth_application(application, request) + end + + worker = worker_class(application) + + application.make_scheduled! + + worker.perform_async(application.name, application.id) + end + end + + protected + + def worker_class(application) + raise NotImplementedError + end + + def builders + raise NotImplementedError + end + + def project_builders + raise NotImplementedError + end + + def instantiate_application + builder.call(@cluster) || raise(InvalidApplicationError, "invalid application: #{application_name}") + end + + def builder + builders[application_name] || raise(InvalidApplicationError, "invalid application: #{application_name}") + end + + def application_name + params[:application] + end + + def create_oauth_application(application, request) + 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(request) + end + end + end +end diff --git a/app/services/clusters/applications/create_service.rb b/app/services/clusters/applications/create_service.rb index 12f8c849d41..bd7c31bb981 100644 --- a/app/services/clusters/applications/create_service.rb +++ b/app/services/clusters/applications/create_service.rb @@ -2,47 +2,11 @@ module Clusters module Applications - class CreateService - InvalidApplicationError = Class.new(StandardError) - - attr_reader :cluster, :current_user, :params - - def initialize(cluster, user, params = {}) - @cluster = cluster - @current_user = user - @params = params.dup - end - - def execute(request) - create_application.tap do |application| - if application.has_attribute?(:hostname) - application.hostname = params[:hostname] - end - - if application.has_attribute?(:email) - application.email = params[:email] - end - - if application.respond_to?(:oauth_application) - application.oauth_application = create_oauth_application(application, request) - end - - worker = application.updateable? ? ClusterUpgradeAppWorker : ClusterInstallAppWorker - - application.make_scheduled! - - worker.perform_async(application.name, application.id) - end - end - + class CreateService < Clusters::Applications::BaseService private - def create_application - builder.call(@cluster) - end - - def builder - builders[application_name] || raise(InvalidApplicationError, "invalid application: #{application_name}") + def worker_class(application) + application.updateable? ? ClusterUpgradeAppWorker : ClusterInstallAppWorker end def builders @@ -65,21 +29,6 @@ module Clusters "knative" => -> (cluster) { cluster.application_knative || cluster.build_application_knative } } end - - def application_name - params[:application] - end - - def create_oauth_application(application, request) - 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(request) - end end end end diff --git a/app/services/clusters/applications/install_service.rb b/app/services/clusters/applications/install_service.rb index 5a65dc4ef59..5bd3623a558 100644 --- a/app/services/clusters/applications/install_service.rb +++ b/app/services/clusters/applications/install_service.rb @@ -6,19 +6,17 @@ module Clusters def execute return unless app.scheduled? - begin - app.make_installing! - helm_api.install(install_command) + app.make_installing! + helm_api.install(install_command) - ClusterWaitForAppInstallationWorker.perform_in( - ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id) - rescue Kubeclient::HttpError => e - log_error(e) - app.make_errored!("Kubernetes error: #{e.error_code}") - rescue StandardError => e - log_error(e) - app.make_errored!("Can't start installation process.") - end + ClusterWaitForAppInstallationWorker.perform_in( + ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id) + rescue Kubeclient::HttpError => e + log_error(e) + app.make_errored!("Kubernetes error: #{e.error_code}") + rescue StandardError => e + log_error(e) + app.make_errored!("Can't start installation process.") end end end diff --git a/app/services/clusters/applications/patch_service.rb b/app/services/clusters/applications/patch_service.rb new file mode 100644 index 00000000000..20c739af7a2 --- /dev/null +++ b/app/services/clusters/applications/patch_service.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Clusters + module Applications + class PatchService < BaseHelmService + def execute + return unless app.scheduled? + + app.make_updating! + + helm_api.update(update_command) + + ClusterWaitForAppInstallationWorker.perform_in( + ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id) + rescue Kubeclient::HttpError => e + log_error(e) + app.make_update_errored!("Kubernetes error: #{e.error_code}") + rescue StandardError => e + log_error(e) + app.make_update_errored!("Can't start update process.") + end + end + end +end diff --git a/app/services/clusters/applications/update_service.rb b/app/services/clusters/applications/update_service.rb new file mode 100644 index 00000000000..a9d4e609992 --- /dev/null +++ b/app/services/clusters/applications/update_service.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Clusters + module Applications + class UpdateService < Clusters::Applications::BaseService + private + + def worker_class(application) + ClusterPatchAppWorker + end + + def builders + { + "helm" => -> (cluster) { cluster.application_helm }, + "ingress" => -> (cluster) { cluster.application_ingress }, + "cert_manager" => -> (cluster) { cluster.application_cert_manager } + }.tap do |hash| + hash.merge!(project_builders) if cluster.project_type? + end + end + + # These applications will need extra configuration to enable them to work + # with groups of projects + def project_builders + { + "prometheus" => -> (cluster) { cluster.application_prometheus }, + "runner" => -> (cluster) { cluster.application_runner }, + "jupyter" => -> (cluster) { cluster.application_jupyter }, + "knative" => -> (cluster) { cluster.application_knative } + } + end + end + end +end diff --git a/app/views/clusters/clusters/show.html.haml b/app/views/clusters/clusters/show.html.haml index 1ef76ef801e..7d381c6d4a6 100644 --- a/app/views/clusters/clusters/show.html.haml +++ b/app/views/clusters/clusters/show.html.haml @@ -15,6 +15,7 @@ install_runner_path: clusterable.install_applications_cluster_path(@cluster, :runner), install_jupyter_path: clusterable.install_applications_cluster_path(@cluster, :jupyter), install_knative_path: clusterable.install_applications_cluster_path(@cluster, :knative), + update_knative_path: clusterable.update_applications_cluster_path(@cluster, :knative), toggle_status: @cluster.enabled? ? 'true': 'false', has_rbac: @cluster.platform_kubernetes_rbac? ? 'true': 'false', cluster_type: @cluster.cluster_type, diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index d86f654dd44..b2d88567e0e 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -23,6 +23,7 @@ - cronjob:prune_web_hook_logs - gcp_cluster:cluster_install_app +- gcp_cluster:cluster_patch_app - gcp_cluster:cluster_upgrade_app - gcp_cluster:cluster_provision - gcp_cluster:cluster_wait_for_app_installation diff --git a/app/workers/cluster_patch_app_worker.rb b/app/workers/cluster_patch_app_worker.rb new file mode 100644 index 00000000000..0549e81ed05 --- /dev/null +++ b/app/workers/cluster_patch_app_worker.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class ClusterPatchAppWorker + include ApplicationWorker + include ClusterQueue + include ClusterApplications + + def perform(app_name, app_id) + find_application(app_name, app_id) do |app| + Clusters::Applications::PatchService.new(app).execute + end + end +end diff --git a/changelogs/unreleased/56937-edit-knative-domain.yml b/changelogs/unreleased/56937-edit-knative-domain.yml new file mode 100644 index 00000000000..7147a4e06b1 --- /dev/null +++ b/changelogs/unreleased/56937-edit-knative-domain.yml @@ -0,0 +1,5 @@ +--- +title: Edit Knative domain after it has been deployed +merge_request: 25386 +author: +type: added diff --git a/config/routes.rb b/config/routes.rb index 484e05114be..53c6225eff1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -101,6 +101,7 @@ Rails.application.routes.draw do member do scope :applications do post '/:application', to: 'clusters/applications#create', as: :install_applications + patch '/:application', to: 'clusters/applications#update', as: :update_applications end get :cluster_status, format: :json diff --git a/lib/gitlab/kubernetes/helm/install_command.rb b/lib/gitlab/kubernetes/helm/install_command.rb index f931248b747..e33ba9305ce 100644 --- a/lib/gitlab/kubernetes/helm/install_command.rb +++ b/lib/gitlab/kubernetes/helm/install_command.rb @@ -7,7 +7,8 @@ module Gitlab include BaseCommand include ClientCommand - attr_reader :name, :files, :chart, :version, :repository, :preinstall, :postinstall + attr_reader :name, :files, :chart, :repository, :preinstall, :postinstall + attr_accessor :version def initialize(name:, chart:, files:, rbac:, version: nil, repository: nil, preinstall: nil, postinstall: nil) @name = name diff --git a/locale/gitlab.pot b/locale/gitlab.pot index d20ad666ba7..cfab870ce24 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1698,7 +1698,7 @@ msgstr "" msgid "ClusterIntegration|Copy Jupyter Hostname to clipboard" msgstr "" -msgid "ClusterIntegration|Copy Knative IP Address to clipboard" +msgid "ClusterIntegration|Copy Knative Endpoint to clipboard" msgstr "" msgid "ClusterIntegration|Copy Kubernetes cluster name" @@ -1821,7 +1821,7 @@ msgstr "" msgid "ClusterIntegration|Knative Domain Name:" msgstr "" -msgid "ClusterIntegration|Knative IP Address:" +msgid "ClusterIntegration|Knative Endpoint:" msgstr "" msgid "ClusterIntegration|Knative extends Kubernetes to provide a set of middleware components that are essential to build modern, source-centric, and container-based applications that can run anywhere: on premises, in the cloud, or even in a third-party data center." @@ -1941,7 +1941,7 @@ msgstr "" msgid "ClusterIntegration|Request to begin installing failed" msgstr "" -msgid "ClusterIntegration|Retry upgrade" +msgid "ClusterIntegration|Retry update" msgstr "" msgid "ClusterIntegration|Save changes" @@ -1986,9 +1986,6 @@ msgstr "" msgid "ClusterIntegration|Something went wrong on our end." msgstr "" -msgid "ClusterIntegration|Something went wrong when upgrading %{title}. Please check the logs and try again." -msgstr "" - msgid "ClusterIntegration|Something went wrong while creating your Kubernetes cluster on Google Kubernetes Engine" msgstr "" @@ -2007,12 +2004,21 @@ msgstr "" msgid "ClusterIntegration|This option will allow you to install applications on RBAC clusters." msgstr "" +msgid "ClusterIntegration|To access your application after deployment, point a wildcard DNS to the Knative Endpoint." +msgstr "" + msgid "ClusterIntegration|Toggle Kubernetes cluster" msgstr "" msgid "ClusterIntegration|Token" msgstr "" +msgid "ClusterIntegration|Update failed. Please check the logs and try again." +msgstr "" + +msgid "ClusterIntegration|Updating" +msgstr "" + msgid "ClusterIntegration|Upgrade" msgstr "" diff --git a/spec/controllers/groups/clusters/applications_controller_spec.rb b/spec/controllers/groups/clusters/applications_controller_spec.rb index dd5263b077c..16a63536ea6 100644 --- a/spec/controllers/groups/clusters/applications_controller_spec.rb +++ b/spec/controllers/groups/clusters/applications_controller_spec.rb @@ -9,9 +9,25 @@ describe Groups::Clusters::ApplicationsController do Clusters::Cluster::APPLICATIONS[application] end + shared_examples 'a secure endpoint' do + it { expect { subject }.to be_allowed_for(:admin) } + it { expect { subject }.to be_allowed_for(:owner).of(group) } + it { expect { subject }.to be_allowed_for(:maintainer).of(group) } + it { expect { subject }.to be_denied_for(:developer).of(group) } + it { expect { subject }.to be_denied_for(:reporter).of(group) } + it { expect { subject }.to be_denied_for(:guest).of(group) } + it { expect { subject }.to be_denied_for(:user) } + it { expect { subject }.to be_denied_for(:external) } + end + + let(:cluster) { create(:cluster, :group, :provided_by_gcp) } + let(:group) { cluster.group } + describe 'POST create' do - let(:cluster) { create(:cluster, :group, :provided_by_gcp) } - let(:group) { cluster.group } + subject do + post :create, params: params.merge(group_id: group) + end + let(:application) { 'helm' } let(:params) { { application: application, id: cluster.id } } @@ -26,7 +42,7 @@ describe Groups::Clusters::ApplicationsController do it 'schedule an application installation' do expect(ClusterInstallAppWorker).to receive(:perform_async).with(application, anything).once - expect { go }.to change { current_application.count } + expect { subject }.to change { current_application.count } expect(response).to have_http_status(:no_content) expect(cluster.application_helm).to be_scheduled end @@ -37,7 +53,7 @@ describe Groups::Clusters::ApplicationsController do end it 'return 404' do - expect { go }.not_to change { current_application.count } + expect { subject }.not_to change { current_application.count } expect(response).to have_http_status(:not_found) end end @@ -46,9 +62,7 @@ describe Groups::Clusters::ApplicationsController do let(:application) { 'unkwnown-app' } it 'return 404' do - go - - expect(response).to have_http_status(:not_found) + is_expected.to have_http_status(:not_found) end end @@ -58,9 +72,7 @@ describe Groups::Clusters::ApplicationsController do end it 'returns 400' do - go - - expect(response).to have_http_status(:bad_request) + is_expected.to have_http_status(:bad_request) end end end @@ -70,18 +82,66 @@ describe Groups::Clusters::ApplicationsController do allow(ClusterInstallAppWorker).to receive(:perform_async) end - it { expect { go }.to be_allowed_for(:admin) } - it { expect { go }.to be_allowed_for(:owner).of(group) } - it { expect { go }.to be_allowed_for(:maintainer).of(group) } - it { expect { go }.to be_denied_for(:developer).of(group) } - it { expect { go }.to be_denied_for(:reporter).of(group) } - it { expect { go }.to be_denied_for(:guest).of(group) } - it { expect { go }.to be_denied_for(:user) } - it { expect { go }.to be_denied_for(:external) } + it_behaves_like 'a secure endpoint' end + end - def go - post :create, params: params.merge(group_id: group) + describe 'PATCH update' do + subject do + patch :update, params: params.merge(group_id: group) + end + + let!(:application) { create(:clusters_applications_cert_managers, :installed, cluster: cluster) } + let(:application_name) { application.name } + let(:params) { { application: application_name, id: cluster.id, email: "new-email@example.com" } } + + describe 'functionality' do + let(:user) { create(:user) } + + before do + group.add_maintainer(user) + sign_in(user) + end + + context "when cluster and app exists" do + it "schedules an application update" do + expect(ClusterPatchAppWorker).to receive(:perform_async).with(application.name, anything).once + + is_expected.to have_http_status(:no_content) + + expect(cluster.application_cert_manager).to be_scheduled + end + end + + context 'when cluster do not exists' do + before do + cluster.destroy! + end + + it { is_expected.to have_http_status(:not_found) } + end + + context 'when application is unknown' do + let(:application_name) { 'unkwnown-app' } + + it { is_expected.to have_http_status(:not_found) } + end + + context 'when application is already scheduled' do + before do + application.make_scheduled! + end + + it { is_expected.to have_http_status(:bad_request) } + end + end + + describe 'security' do + before do + allow(ClusterPatchAppWorker).to receive(:perform_async) + end + + it_behaves_like 'a secure endpoint' end end end diff --git a/spec/controllers/projects/clusters/applications_controller_spec.rb b/spec/controllers/projects/clusters/applications_controller_spec.rb index cb558259225..cd1a01f8acc 100644 --- a/spec/controllers/projects/clusters/applications_controller_spec.rb +++ b/spec/controllers/projects/clusters/applications_controller_spec.rb @@ -9,7 +9,22 @@ describe Projects::Clusters::ApplicationsController do Clusters::Cluster::APPLICATIONS[application] end + shared_examples 'a secure endpoint' do + it { expect { subject }.to be_allowed_for(:admin) } + it { expect { subject }.to be_allowed_for(:owner).of(project) } + it { expect { subject }.to be_allowed_for(:maintainer).of(project) } + it { expect { subject }.to be_denied_for(:developer).of(project) } + it { expect { subject }.to be_denied_for(:reporter).of(project) } + it { expect { subject }.to be_denied_for(:guest).of(project) } + it { expect { subject }.to be_denied_for(:user) } + it { expect { subject }.to be_denied_for(:external) } + end + describe 'POST create' do + subject do + post :create, params: params.merge(namespace_id: project.namespace, project_id: project) + end + let(:cluster) { create(:cluster, :project, :provided_by_gcp) } let(:project) { cluster.project } let(:application) { 'helm' } @@ -26,7 +41,7 @@ describe Projects::Clusters::ApplicationsController do it 'schedule an application installation' do expect(ClusterInstallAppWorker).to receive(:perform_async).with(application, anything).once - expect { go }.to change { current_application.count } + expect { subject }.to change { current_application.count } expect(response).to have_http_status(:no_content) expect(cluster.application_helm).to be_scheduled end @@ -37,7 +52,7 @@ describe Projects::Clusters::ApplicationsController do end it 'return 404' do - expect { go }.not_to change { current_application.count } + expect { subject }.not_to change { current_application.count } expect(response).to have_http_status(:not_found) end end @@ -46,9 +61,7 @@ describe Projects::Clusters::ApplicationsController do let(:application) { 'unkwnown-app' } it 'return 404' do - go - - expect(response).to have_http_status(:not_found) + is_expected.to have_http_status(:not_found) end end @@ -58,9 +71,7 @@ describe Projects::Clusters::ApplicationsController do end it 'returns 400' do - go - - expect(response).to have_http_status(:bad_request) + is_expected.to have_http_status(:bad_request) end end end @@ -70,18 +81,68 @@ describe Projects::Clusters::ApplicationsController do allow(ClusterInstallAppWorker).to receive(:perform_async) end - it { expect { go }.to be_allowed_for(:admin) } - it { expect { go }.to be_allowed_for(:owner).of(project) } - it { expect { go }.to be_allowed_for(:maintainer).of(project) } - it { expect { go }.to be_denied_for(:developer).of(project) } - it { expect { go }.to be_denied_for(:reporter).of(project) } - it { expect { go }.to be_denied_for(:guest).of(project) } - it { expect { go }.to be_denied_for(:user) } - it { expect { go }.to be_denied_for(:external) } + it_behaves_like 'a secure endpoint' end + end - def go - post :create, params: params.merge(namespace_id: project.namespace, project_id: project) + describe 'PATCH update' do + subject do + patch :update, params: params.merge(namespace_id: project.namespace, project_id: project) + end + + let(:cluster) { create(:cluster, :project, :provided_by_gcp) } + let(:project) { cluster.project } + let!(:application) { create(:clusters_applications_knative, :installed, cluster: cluster) } + let(:application_name) { application.name } + let(:params) { { application: application_name, id: cluster.id, hostname: "new.example.com" } } + + describe 'functionality' do + let(:user) { create(:user) } + + before do + project.add_maintainer(user) + sign_in(user) + end + + context "when cluster and app exists" do + it "schedules an application update" do + expect(ClusterPatchAppWorker).to receive(:perform_async).with(application.name, anything).once + + is_expected.to have_http_status(:no_content) + + expect(cluster.application_knative).to be_scheduled + end + end + + context 'when cluster do not exists' do + before do + cluster.destroy! + end + + it { is_expected.to have_http_status(:not_found) } + end + + context 'when application is unknown' do + let(:application_name) { 'unkwnown-app' } + + it { is_expected.to have_http_status(:not_found) } + end + + context 'when application is already scheduled' do + before do + application.make_scheduled! + end + + it { is_expected.to have_http_status(:bad_request) } + end + end + + describe 'security' do + before do + allow(ClusterPatchAppWorker).to receive(:perform_async) + end + + it_behaves_like 'a secure endpoint' end end end diff --git a/spec/features/projects/clusters/applications_spec.rb b/spec/features/projects/clusters/applications_spec.rb index 2c8d014c36d..713e25cdcb2 100644 --- a/spec/features/projects/clusters/applications_spec.rb +++ b/spec/features/projects/clusters/applications_spec.rb @@ -17,7 +17,7 @@ describe 'Clusters Applications', :js do end context 'when cluster is being created' do - let(:cluster) { create(:cluster, :providing_by_gcp, projects: [project])} + let(:cluster) { create(:cluster, :providing_by_gcp, projects: [project]) } it 'user is unable to install applications' do page.within('.js-cluster-application-row-helm') do @@ -28,9 +28,11 @@ describe 'Clusters Applications', :js do end context 'when cluster is created' do - let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project])} + let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) } it 'user can install applications' do + wait_for_requests + page.within('.js-cluster-application-row-helm') do expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to be_nil expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install') @@ -44,6 +46,8 @@ describe 'Clusters Applications', :js do page.within('.js-cluster-application-row-helm') do page.find(:css, '.js-cluster-application-install-button').click end + + wait_for_requests end it 'they see status transition' do @@ -52,8 +56,6 @@ describe 'Clusters Applications', :js do expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true') expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing') - wait_until_helm_created! - Clusters::Cluster.last.application_helm.make_installing! # FE starts polling and update the buttons to "Installing" @@ -76,7 +78,7 @@ describe 'Clusters Applications', :js do end context 'on an abac cluster' do - let(:cluster) { create(:cluster, :provided_by_gcp, :rbac_disabled, projects: [project])} + let(:cluster) { create(:cluster, :provided_by_gcp, :rbac_disabled, projects: [project]) } it 'should show info block and not be installable' do page.within('.js-cluster-application-row-knative') do @@ -87,7 +89,7 @@ describe 'Clusters Applications', :js do end context 'on an rbac cluster' do - let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project])} + let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) } it 'should not show callout block and be installable' do page.within('.js-cluster-application-row-knative') do @@ -95,6 +97,60 @@ describe 'Clusters Applications', :js do expect(page).to have_css('.js-cluster-application-install-button:not([disabled])') end end + + describe 'when user clicks install button' do + def domainname_form_value + page.find('.js-knative-domainname').value + end + + before do + allow(ClusterInstallAppWorker).to receive(:perform_async) + allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in) + allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async) + + page.within('.js-cluster-application-row-knative') do + expect(page).to have_css('.js-cluster-application-install-button:not([disabled])') + + page.find('.js-knative-domainname').set("domain.example.org") + + click_button 'Install' + + wait_for_requests + + expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing') + + Clusters::Cluster.last.application_knative.make_installing! + Clusters::Cluster.last.application_knative.make_installed! + Clusters::Cluster.last.application_knative.update_attribute(:external_ip, '127.0.0.1') + end + end + + it 'shows status transition' do + page.within('.js-cluster-application-row-knative') do + expect(domainname_form_value).to eq('domain.example.org') + expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installed') + end + + expect(page).to have_content('Knative was successfully installed on your Kubernetes cluster') + expect(page).to have_css('.js-knative-save-domain-button'), exact_text: 'Save changes' + end + + it 'can then update the domain' do + page.within('.js-cluster-application-row-knative') do + expect(ClusterPatchAppWorker).to receive(:perform_async) + + expect(domainname_form_value).to eq('domain.example.org') + + page.find('.js-knative-domainname').set("new.domain.example.org") + + click_button 'Save changes' + + wait_for_requests + + expect(domainname_form_value).to eq('new.domain.example.org') + end + end + end end end @@ -148,6 +204,8 @@ describe 'Clusters Applications', :js do page.within('.js-cluster-application-row-ingress') do expect(page).to have_css('.js-cluster-application-install-button:not([disabled])') page.find(:css, '.js-cluster-application-install-button').click + + wait_for_requests end end @@ -184,14 +242,4 @@ describe 'Clusters Applications', :js do end end end - - def wait_until_helm_created! - retries = 0 - - while Clusters::Cluster.last.application_helm.nil? - raise "Timed out waiting for helm application to be created in DB" if (retries += 1) > 3 - - sleep(1) - end - end end diff --git a/spec/javascripts/clusters/components/application_row_spec.js b/spec/javascripts/clusters/components/application_row_spec.js index 8cb9713964e..a2dd4e93daf 100644 --- a/spec/javascripts/clusters/components/application_row_spec.js +++ b/spec/javascripts/clusters/components/application_row_spec.js @@ -230,7 +230,7 @@ describe('Application Row', () => { expect(upgradeBtn.innerHTML).toContain('Upgrade'); }); - it('has enabled "Retry upgrade" when APPLICATION_STATUS.UPDATE_ERRORED', () => { + it('has enabled "Retry update" when APPLICATION_STATUS.UPDATE_ERRORED', () => { vm = mountComponent(ApplicationRow, { ...DEFAULT_APPLICATION_STATE, status: APPLICATION_STATUS.UPDATE_ERRORED, @@ -239,10 +239,10 @@ describe('Application Row', () => { expect(upgradeBtn).not.toBe(null); expect(vm.upgradeFailed).toBe(true); - expect(upgradeBtn.innerHTML).toContain('Retry upgrade'); + expect(upgradeBtn.innerHTML).toContain('Retry update'); }); - it('has disabled "Retry upgrade" when APPLICATION_STATUS.UPDATING', () => { + it('has disabled "Updating" when APPLICATION_STATUS.UPDATING', () => { vm = mountComponent(ApplicationRow, { ...DEFAULT_APPLICATION_STATE, status: APPLICATION_STATUS.UPDATING, @@ -251,7 +251,7 @@ describe('Application Row', () => { expect(upgradeBtn).not.toBe(null); expect(vm.isUpgrading).toBe(true); - expect(upgradeBtn.innerHTML).toContain('Upgrading'); + expect(upgradeBtn.innerHTML).toContain('Updating'); }); it('clicking upgrade button emits event', () => { @@ -295,7 +295,7 @@ describe('Application Row', () => { expect(failureMessage).not.toBe(null); expect(failureMessage.innerHTML).toContain( - 'Something went wrong when upgrading GitLab Runner. Please check the logs and try again.', + 'Update failed. Please check the logs and try again.', ); }); }); diff --git a/spec/javascripts/clusters/components/applications_spec.js b/spec/javascripts/clusters/components/applications_spec.js index 14ef1193984..8daf0282184 100644 --- a/spec/javascripts/clusters/components/applications_spec.js +++ b/spec/javascripts/clusters/components/applications_spec.js @@ -1,7 +1,9 @@ import Vue from 'vue'; import applications from '~/clusters/components/applications.vue'; import { CLUSTER_TYPE } from '~/clusters/constants'; +import eventHub from '~/clusters/event_hub'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import { APPLICATIONS_MOCK_STATE } from '../services/mock_data'; describe('Applications', () => { let vm; @@ -18,16 +20,8 @@ describe('Applications', () => { describe('Project cluster applications', () => { beforeEach(() => { vm = mountComponent(Applications, { + applications: APPLICATIONS_MOCK_STATE, type: CLUSTER_TYPE.PROJECT, - applications: { - helm: { title: 'Helm Tiller' }, - ingress: { title: 'Ingress' }, - cert_manager: { title: 'Cert-Manager' }, - runner: { title: 'GitLab Runner' }, - prometheus: { title: 'Prometheus' }, - jupyter: { title: 'JupyterHub' }, - knative: { title: 'Knative' }, - }, }); }); @@ -64,15 +58,7 @@ describe('Applications', () => { beforeEach(() => { vm = mountComponent(Applications, { type: CLUSTER_TYPE.GROUP, - applications: { - helm: { title: 'Helm Tiller' }, - ingress: { title: 'Ingress' }, - cert_manager: { title: 'Cert-Manager' }, - runner: { title: 'GitLab Runner' }, - prometheus: { title: 'Prometheus' }, - jupyter: { title: 'JupyterHub' }, - knative: { title: 'Knative' }, - }, + applications: APPLICATIONS_MOCK_STATE, }); }); @@ -111,17 +97,12 @@ describe('Applications', () => { it('renders ip address with a clipboard button', () => { vm = mountComponent(Applications, { applications: { + ...APPLICATIONS_MOCK_STATE, ingress: { title: 'Ingress', status: 'installed', externalIp: '0.0.0.0', }, - helm: { title: 'Helm Tiller' }, - cert_manager: { title: 'Cert-Manager' }, - runner: { title: 'GitLab Runner' }, - prometheus: { title: 'Prometheus' }, - jupyter: { title: 'JupyterHub', hostname: '' }, - knative: { title: 'Knative', hostname: '' }, }, }); @@ -137,16 +118,11 @@ describe('Applications', () => { it('renders an input text with a question mark and an alert text', () => { vm = mountComponent(Applications, { applications: { + ...APPLICATIONS_MOCK_STATE, ingress: { title: 'Ingress', status: 'installed', }, - helm: { title: 'Helm Tiller' }, - cert_manager: { title: 'Cert-Manager' }, - runner: { title: 'GitLab Runner' }, - prometheus: { title: 'Prometheus' }, - jupyter: { title: 'JupyterHub', hostname: '' }, - knative: { title: 'Knative', hostname: '' }, }, }); @@ -160,15 +136,7 @@ describe('Applications', () => { describe('before installing', () => { it('does not render the IP address', () => { vm = mountComponent(Applications, { - applications: { - helm: { title: 'Helm Tiller' }, - ingress: { title: 'Ingress' }, - cert_manager: { title: 'Cert-Manager' }, - runner: { title: 'GitLab Runner' }, - prometheus: { title: 'Prometheus' }, - jupyter: { title: 'JupyterHub', hostname: '' }, - knative: { title: 'Knative', hostname: '' }, - }, + applications: APPLICATIONS_MOCK_STATE, }); expect(vm.$el.textContent).not.toContain('Ingress IP Address'); @@ -181,17 +149,12 @@ describe('Applications', () => { it('renders email & allows editing', () => { vm = mountComponent(Applications, { applications: { - helm: { title: 'Helm Tiller', status: 'installed' }, - ingress: { title: 'Ingress', status: 'installed', externalIp: '1.1.1.1' }, + ...APPLICATIONS_MOCK_STATE, cert_manager: { title: 'Cert-Manager', email: 'before@example.com', status: 'installable', }, - runner: { title: 'GitLab Runner' }, - prometheus: { title: 'Prometheus' }, - jupyter: { title: 'JupyterHub', hostname: '', status: 'installable' }, - knative: { title: 'Knative', hostname: '', status: 'installable' }, }, }); @@ -204,17 +167,12 @@ describe('Applications', () => { it('renders email in readonly', () => { vm = mountComponent(Applications, { applications: { - helm: { title: 'Helm Tiller', status: 'installed' }, - ingress: { title: 'Ingress', status: 'installed', externalIp: '1.1.1.1' }, + ...APPLICATIONS_MOCK_STATE, cert_manager: { title: 'Cert-Manager', email: 'after@example.com', status: 'installed', }, - runner: { title: 'GitLab Runner' }, - prometheus: { title: 'Prometheus' }, - jupyter: { title: 'JupyterHub', hostname: '', status: 'installable' }, - knative: { title: 'Knative', hostname: '', status: 'installable' }, }, }); @@ -229,13 +187,12 @@ describe('Applications', () => { 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' }, - cert_manager: { title: 'Cert-Manager' }, - runner: { title: 'GitLab Runner' }, - prometheus: { title: 'Prometheus' }, - jupyter: { title: 'JupyterHub', hostname: '', status: 'installable' }, - knative: { title: 'Knative', hostname: '', status: 'installable' }, + ...APPLICATIONS_MOCK_STATE, + ingress: { + title: 'Ingress', + status: 'installed', + externalIp: '1.1.1.1', + }, }, }); @@ -247,13 +204,8 @@ describe('Applications', () => { it('does not render hostname input', () => { vm = mountComponent(Applications, { applications: { - helm: { title: 'Helm Tiller', status: 'installed' }, + ...APPLICATIONS_MOCK_STATE, ingress: { title: 'Ingress', status: 'installed' }, - cert_manager: { title: 'Cert-Manager' }, - runner: { title: 'GitLab Runner' }, - prometheus: { title: 'Prometheus' }, - jupyter: { title: 'JupyterHub', hostname: '', status: 'installable' }, - knative: { title: 'Knative', hostname: '', status: 'installable' }, }, }); @@ -265,13 +217,9 @@ describe('Applications', () => { it('renders readonly input', () => { vm = mountComponent(Applications, { applications: { - helm: { title: 'Helm Tiller', status: 'installed' }, + ...APPLICATIONS_MOCK_STATE, ingress: { title: 'Ingress', status: 'installed', externalIp: '1.1.1.1' }, - cert_manager: { title: 'Cert-Manager' }, - runner: { title: 'GitLab Runner' }, - prometheus: { title: 'Prometheus' }, jupyter: { title: 'JupyterHub', status: 'installed', hostname: '' }, - knative: { title: 'Knative', status: 'installed', hostname: '' }, }, }); @@ -282,15 +230,7 @@ describe('Applications', () => { describe('without ingress installed', () => { beforeEach(() => { vm = mountComponent(Applications, { - applications: { - helm: { title: 'Helm Tiller' }, - ingress: { title: 'Ingress' }, - cert_manager: { title: 'Cert-Manager' }, - runner: { title: 'GitLab Runner' }, - prometheus: { title: 'Prometheus' }, - jupyter: { title: 'JupyterHub', status: 'not_installable' }, - knative: { title: 'Knative' }, - }, + applications: APPLICATIONS_MOCK_STATE, }); }); @@ -310,4 +250,77 @@ describe('Applications', () => { }); }); }); + + describe('Knative application', () => { + describe('when installed', () => { + describe('with ip address', () => { + const props = { + applications: { + ...APPLICATIONS_MOCK_STATE, + knative: { + title: 'Knative', + hostname: 'example.com', + status: 'installed', + externalIp: '1.1.1.1', + }, + }, + }; + it('renders ip address with a clipboard button', () => { + vm = mountComponent(Applications, props); + + expect(vm.$el.querySelector('.js-knative-ip-address').value).toEqual('1.1.1.1'); + + expect( + vm.$el + .querySelector('.js-knative-ip-clipboard-btn') + .getAttribute('data-clipboard-text'), + ).toEqual('1.1.1.1'); + }); + + it('renders domain & allows editing', () => { + expect(vm.$el.querySelector('.js-knative-domainname').value).toEqual('example.com'); + expect(vm.$el.querySelector('.js-knative-domainname').getAttribute('readonly')).toBe( + null, + ); + }); + + it('renders an update/save Knative domain button', () => { + expect(vm.$el.querySelector('.js-knative-save-domain-button')).not.toBe(null); + }); + + it('emits event when clicking Save changes button', () => { + spyOn(eventHub, '$emit'); + vm = mountComponent(Applications, props); + + const saveButton = vm.$el.querySelector('.js-knative-save-domain-button'); + + saveButton.click(); + + expect(eventHub.$emit).toHaveBeenCalledWith('saveKnativeDomain', { + id: 'knative', + params: { hostname: 'example.com' }, + }); + }); + }); + + describe('without ip address', () => { + it('renders an input text with a question mark and an alert text', () => { + vm = mountComponent(Applications, { + applications: { + ...APPLICATIONS_MOCK_STATE, + knative: { + title: 'Knative', + hostname: 'example.com', + status: 'installed', + }, + }, + }); + + expect(vm.$el.querySelector('.js-knative-ip-address').value).toEqual('?'); + + expect(vm.$el.querySelector('.js-no-knative-ip-message')).not.toBe(null); + }); + }); + }); + }); }); diff --git a/spec/javascripts/clusters/services/mock_data.js b/spec/javascripts/clusters/services/mock_data.js index 3c3d9977ffb..3ace19c6401 100644 --- a/spec/javascripts/clusters/services/mock_data.js +++ b/spec/javascripts/clusters/services/mock_data.js @@ -115,4 +115,14 @@ const DEFAULT_APPLICATION_STATE = { requestReason: null, }; -export { CLUSTERS_MOCK_DATA, DEFAULT_APPLICATION_STATE }; +const APPLICATIONS_MOCK_STATE = { + helm: { title: 'Helm Tiller', status: 'installable' }, + ingress: { title: 'Ingress', status: 'installable' }, + cert_manager: { title: 'Cert-Manager', status: 'installable' }, + runner: { title: 'GitLab Runner' }, + prometheus: { title: 'Prometheus' }, + jupyter: { title: 'JupyterHub', status: 'installable', hostname: '' }, + knative: { title: 'Knative ', status: 'installable', hostname: '' }, +}; + +export { CLUSTERS_MOCK_DATA, DEFAULT_APPLICATION_STATE, APPLICATIONS_MOCK_STATE }; diff --git a/spec/javascripts/clusters/stores/clusters_store_spec.js b/spec/javascripts/clusters/stores/clusters_store_spec.js index 37a4d6614f6..09bcdf91d91 100644 --- a/spec/javascripts/clusters/stores/clusters_store_spec.js +++ b/spec/javascripts/clusters/stores/clusters_store_spec.js @@ -111,6 +111,7 @@ describe('Clusters Store', () => { requestStatus: null, requestReason: null, hostname: null, + isEditingHostName: false, externalIp: null, }, cert_manager: { diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb index 006b922ab27..4884a5927fb 100644 --- a/spec/models/clusters/applications/knative_spec.rb +++ b/spec/models/clusters/applications/knative_spec.rb @@ -66,9 +66,7 @@ describe Clusters::Applications::Knative do end end - describe '#install_command' do - subject { knative.install_command } - + shared_examples 'a command' do it 'should be an instance of Helm::InstallCommand' do expect(subject).to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) end @@ -76,7 +74,6 @@ describe Clusters::Applications::Knative do it 'should be initialized with knative arguments' do expect(subject.name).to eq('knative') expect(subject.chart).to eq('knative/knative') - expect(subject.version).to eq('0.2.2') expect(subject.files).to eq(knative.files) end @@ -98,6 +95,27 @@ describe Clusters::Applications::Knative do end end + describe '#install_command' do + subject { knative.install_command } + + it 'should be initialized with latest version' do + expect(subject.version).to eq('0.2.2') + end + + it_behaves_like 'a command' + end + + describe '#update_command' do + let!(:current_installed_version) { knative.version = '0.1.0' } + subject { knative.update_command } + + it 'should be initialized with current version' do + expect(subject.version).to eq(current_installed_version) + end + + it_behaves_like 'a command' + end + describe '#files' do let(:application) { knative } let(:values) { subject[:'values.yaml'] } diff --git a/spec/presenters/group_clusterable_presenter_spec.rb b/spec/presenters/group_clusterable_presenter_spec.rb index 205160742bf..fa77273f6aa 100644 --- a/spec/presenters/group_clusterable_presenter_spec.rb +++ b/spec/presenters/group_clusterable_presenter_spec.rb @@ -69,6 +69,14 @@ describe GroupClusterablePresenter do it { is_expected.to eq(install_applications_group_cluster_path(group, cluster, application)) } end + describe '#update_applications_cluster_path' do + let(:application) { :helm } + + subject { presenter.update_applications_cluster_path(cluster, application) } + + it { is_expected.to eq(update_applications_group_cluster_path(group, cluster, application)) } + end + describe '#cluster_path' do subject { presenter.cluster_path(cluster) } diff --git a/spec/presenters/project_clusterable_presenter_spec.rb b/spec/presenters/project_clusterable_presenter_spec.rb index c50d90ae1e8..6786a84243f 100644 --- a/spec/presenters/project_clusterable_presenter_spec.rb +++ b/spec/presenters/project_clusterable_presenter_spec.rb @@ -69,6 +69,14 @@ describe ProjectClusterablePresenter do it { is_expected.to eq(install_applications_project_cluster_path(project, cluster, application)) } end + describe '#update_applications_cluster_path' do + let(:application) { :helm } + + subject { presenter.update_applications_cluster_path(cluster, application) } + + it { is_expected.to eq(update_applications_project_cluster_path(project, cluster, application)) } + end + describe '#cluster_path' do subject { presenter.cluster_path(cluster) } diff --git a/spec/services/clusters/applications/patch_service_spec.rb b/spec/services/clusters/applications/patch_service_spec.rb new file mode 100644 index 00000000000..d4ee3243b84 --- /dev/null +++ b/spec/services/clusters/applications/patch_service_spec.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Clusters::Applications::PatchService do + describe '#execute' do + let(:application) { create(:clusters_applications_knative, :scheduled) } + let!(:update_command) { application.update_command } + let(:service) { described_class.new(application) } + let(:helm_client) { instance_double(Gitlab::Kubernetes::Helm::Api) } + + before do + allow(service).to receive(:update_command).and_return(update_command) + allow(service).to receive(:helm_api).and_return(helm_client) + end + + context 'when there are no errors' do + before do + expect(helm_client).to receive(:update).with(update_command) + allow(ClusterWaitForAppInstallationWorker).to receive(:perform_in).and_return(nil) + end + + it 'make the application updating' do + expect(application.cluster).not_to be_nil + service.execute + + expect(application).to be_updating + end + + it 'schedule async installation status check' do + expect(ClusterWaitForAppInstallationWorker).to receive(:perform_in).once + + service.execute + end + end + + context 'when kubernetes cluster communication fails' do + let(:error) { Kubeclient::HttpError.new(500, 'system failure', nil) } + + before do + expect(helm_client).to receive(:update).with(update_command).and_raise(error) + end + + it 'make the application errored' do + service.execute + + expect(application).to be_update_errored + expect(application.status_reason).to match('Kubernetes error: 500') + end + + it 'logs errors' do + expect(service.send(:logger)).to receive(:error).with( + { + exception: 'Kubeclient::HttpError', + message: 'system failure', + service: 'Clusters::Applications::PatchService', + app_id: application.id, + project_ids: application.cluster.project_ids, + group_ids: [], + error_code: 500 + } + ) + + expect(Gitlab::Sentry).to receive(:track_acceptable_exception).with( + error, + extra: { + exception: 'Kubeclient::HttpError', + message: 'system failure', + service: 'Clusters::Applications::PatchService', + app_id: application.id, + project_ids: application.cluster.project_ids, + group_ids: [], + error_code: 500 + } + ) + + service.execute + end + end + + context 'a non kubernetes error happens' do + let(:application) { create(:clusters_applications_knative, :scheduled) } + let(:error) { StandardError.new('something bad happened') } + + before do + expect(application).to receive(:make_updating!).once.and_raise(error) + end + + it 'make the application errored' do + expect(helm_client).not_to receive(:update) + + service.execute + + expect(application).to be_update_errored + expect(application.status_reason).to eq("Can't start update process.") + end + + it 'logs errors' do + expect(service.send(:logger)).to receive(:error).with( + { + exception: 'StandardError', + error_code: nil, + message: 'something bad happened', + service: 'Clusters::Applications::PatchService', + app_id: application.id, + project_ids: application.cluster.projects.pluck(:id), + group_ids: [] + } + ) + + expect(Gitlab::Sentry).to receive(:track_acceptable_exception).with( + error, + extra: { + exception: 'StandardError', + error_code: nil, + message: 'something bad happened', + service: 'Clusters::Applications::PatchService', + app_id: application.id, + project_ids: application.cluster.projects.pluck(:id), + group_ids: [] + } + ) + + service.execute + end + end + end +end diff --git a/spec/services/clusters/applications/update_service_spec.rb b/spec/services/clusters/applications/update_service_spec.rb new file mode 100644 index 00000000000..2d299882af0 --- /dev/null +++ b/spec/services/clusters/applications/update_service_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Clusters::Applications::UpdateService do + include TestRequestHelpers + + let(:cluster) { create(:cluster, :project, :provided_by_gcp) } + let(:user) { create(:user) } + let(:params) { { application: 'knative', hostname: 'udpate.example.com' } } + let(:service) { described_class.new(cluster, user, params) } + + subject { service.execute(test_request) } + + describe '#execute' do + before do + allow(ClusterPatchAppWorker).to receive(:perform_async) + end + + context 'application is not installed' do + it 'raises Clusters::Applications::BaseService::InvalidApplicationError' do + expect(ClusterPatchAppWorker).not_to receive(:perform_async) + + expect { subject } + .to raise_exception { Clusters::Applications::BaseService::InvalidApplicationError } + .and not_change { Clusters::Applications::Knative.count } + .and not_change { Clusters::Applications::Knative.with_status(:scheduled).count } + end + end + + context 'application is installed' do + context 'application is schedulable' do + let!(:application) do + create(:clusters_applications_knative, status: 3, cluster: cluster) + end + + it 'updates the application data' do + expect do + subject + end.to change { application.reload.hostname }.to(params[:hostname]) + end + + it 'makes application scheduled!' do + subject + + expect(application.reload).to be_scheduled + end + + it 'schedules ClusterPatchAppWorker' do + expect(ClusterPatchAppWorker).to receive(:perform_async) + + subject + end + end + + context 'application is not schedulable' do + let!(:application) do + create(:clusters_applications_knative, status: 4, cluster: cluster) + end + + it 'raises StateMachines::InvalidTransition' do + expect(ClusterPatchAppWorker).not_to receive(:perform_async) + + expect { subject } + .to raise_exception { StateMachines::InvalidTransition } + .and not_change { application.reload.hostname } + .and not_change { Clusters::Applications::Knative.with_status(:scheduled).count } + end + end + end + end +end |