Commit 5cea1e8b authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch '56937-edit-knative-domain' into 'master'

Edit Knative domain after it has been deployed

Closes #56937

See merge request gitlab-org/gitlab-ce!25386
parents f8dc5f8d c08beb50
...@@ -36,6 +36,7 @@ export default class Clusters { ...@@ -36,6 +36,7 @@ export default class Clusters {
installRunnerPath, installRunnerPath,
installJupyterPath, installJupyterPath,
installKnativePath, installKnativePath,
updateKnativePath,
installPrometheusPath, installPrometheusPath,
managePrometheusPath, managePrometheusPath,
hasRbac, hasRbac,
...@@ -62,6 +63,7 @@ export default class Clusters { ...@@ -62,6 +63,7 @@ export default class Clusters {
installPrometheusEndpoint: installPrometheusPath, installPrometheusEndpoint: installPrometheusPath,
installJupyterEndpoint: installJupyterPath, installJupyterEndpoint: installJupyterPath,
installKnativeEndpoint: installKnativePath, installKnativeEndpoint: installKnativePath,
updateKnativeEndpoint: updateKnativePath,
}); });
this.installApplication = this.installApplication.bind(this); this.installApplication = this.installApplication.bind(this);
...@@ -128,6 +130,8 @@ export default class Clusters { ...@@ -128,6 +130,8 @@ export default class Clusters {
eventHub.$on('upgradeApplication', data => this.upgradeApplication(data)); eventHub.$on('upgradeApplication', data => this.upgradeApplication(data));
eventHub.$on('upgradeFailed', appId => this.upgradeFailed(appId)); eventHub.$on('upgradeFailed', appId => this.upgradeFailed(appId));
eventHub.$on('dismissUpgradeSuccess', appId => this.dismissUpgradeSuccess(appId)); eventHub.$on('dismissUpgradeSuccess', appId => this.dismissUpgradeSuccess(appId));
eventHub.$on('saveKnativeDomain', data => this.saveKnativeDomain(data));
eventHub.$on('setKnativeHostname', data => this.setKnativeHostname(data));
} }
removeListeners() { removeListeners() {
...@@ -136,6 +140,8 @@ export default class Clusters { ...@@ -136,6 +140,8 @@ export default class Clusters {
eventHub.$off('upgradeApplication', this.upgradeApplication); eventHub.$off('upgradeApplication', this.upgradeApplication);
eventHub.$off('upgradeFailed', this.upgradeFailed); eventHub.$off('upgradeFailed', this.upgradeFailed);
eventHub.$off('dismissUpgradeSuccess', this.dismissUpgradeSuccess); eventHub.$off('dismissUpgradeSuccess', this.dismissUpgradeSuccess);
eventHub.$off('saveKnativeDomain');
eventHub.$off('setKnativeHostname');
} }
initPolling() { initPolling() {
...@@ -271,6 +277,18 @@ export default class Clusters { ...@@ -271,6 +277,18 @@ export default class Clusters {
this.store.updateAppProperty(appId, 'requestStatus', null); 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() { destroy() {
this.destroyed = true; this.destroyed = true;
......
...@@ -191,14 +191,7 @@ export default { ...@@ -191,14 +191,7 @@ export default {
return this.status === APPLICATION_STATUS.UPDATE_ERRORED; return this.status === APPLICATION_STATUS.UPDATE_ERRORED;
}, },
upgradeFailureDescription() { upgradeFailureDescription() {
return sprintf( return s__('ClusterIntegration|Update failed. Please check the logs and try again.');
s__(
'ClusterIntegration|Something went wrong when upgrading %{title}. Please check the logs and try again.',
),
{
title: this.title,
},
);
}, },
upgradeSuccessDescription() { upgradeSuccessDescription() {
return sprintf(s__('ClusterIntegration|%{title} upgraded successfully.'), { return sprintf(s__('ClusterIntegration|%{title} upgraded successfully.'), {
...@@ -210,9 +203,9 @@ export default { ...@@ -210,9 +203,9 @@ export default {
if (this.upgradeAvailable && !this.upgradeFailed && !this.isUpgrading) { if (this.upgradeAvailable && !this.upgradeFailed && !this.isUpgrading) {
label = s__('ClusterIntegration|Upgrade'); label = s__('ClusterIntegration|Upgrade');
} else if (this.isUpgrading) { } else if (this.isUpgrading) {
label = s__('ClusterIntegration|Upgrading'); label = s__('ClusterIntegration|Updating');
} else if (this.upgradeFailed) { } else if (this.upgradeFailed) {
label = s__('ClusterIntegration|Retry upgrade'); label = s__('ClusterIntegration|Retry update');
} }
return label; return label;
...@@ -224,6 +217,14 @@ export default { ...@@ -224,6 +217,14 @@ export default {
(this.upgradeRequested && !this.upgradeSuccessful) (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: { watch: {
status() { status() {
...@@ -303,7 +304,7 @@ export default { ...@@ -303,7 +304,7 @@ export default {
</div> </div>
<div <div
v-if="(upgradeSuccessful || upgradeFailed) && !upgradeAvailable" v-if="shouldShowUpgradeDetails"
class="form-text text-muted label p-0 js-cluster-application-upgrade-details" class="form-text text-muted label p-0 js-cluster-application-upgrade-details"
> >
{{ versionLabel }} {{ versionLabel }}
......
...@@ -15,11 +15,14 @@ import { s__, sprintf } from '../../locale'; ...@@ -15,11 +15,14 @@ import { s__, sprintf } from '../../locale';
import applicationRow from './application_row.vue'; import applicationRow from './application_row.vue';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
import { CLUSTER_TYPE, APPLICATION_STATUS, INGRESS } from '../constants'; 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 { export default {
components: { components: {
applicationRow, applicationRow,
clipboardButton, clipboardButton,
LoadingButton,
}, },
props: { props: {
type: { type: {
...@@ -173,16 +176,55 @@ export default { ...@@ -173,16 +176,55 @@ export default {
jupyterHostname() { jupyterHostname() {
return this.applications.jupyter.hostname; return this.applications.jupyter.hostname;
}, },
knative() {
return this.applications.knative;
},
knativeInstalled() { 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() { 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() { created() {
this.helmInstallIllustration = helmInstallIllustration; this.helmInstallIllustration = helmInstallIllustration;
}, },
methods: {
saveKnativeDomain() {
eventHub.$emit('saveKnativeDomain', {
id: 'knative',
params: { hostname: this.knative.hostname },
});
},
},
}; };
</script> </script>
...@@ -471,77 +513,89 @@ export default { ...@@ -471,77 +513,89 @@ export default {
}} }}
</p> </p>
<template v-if="knativeInstalled"> <div class="row">
<div class="form-group"> <template v-if="knativeInstalled || (helmInstalled && rbac)">
<label for="knative-domainname"> <div
{{ s__('ClusterIntegration|Knative Domain Name:') }} :class="{ 'col-md-6': knativeInstalled, 'col-12': helmInstalled && rbac }"
</label> class="form-group col-sm-12 mb-0"
<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"> <label for="knative-domainname">
<strong>
{{ s__('ClusterIntegration|Knative Domain Name:') }} {{ s__('ClusterIntegration|Knative Domain Name:') }}
</strong>
</label> </label>
<input <input
id="knative-domainname" id="knative-domainname"
v-model="applications.knative.hostname" v-model="knativeHostname"
type="text" type="text"
class="form-control js-domainname" class="form-control js-knative-domainname"
/> />
</div> </div>
</template> </template>
<template v-if="knativeInstalled"> <template v-if="knativeInstalled">
<div class="form-group"> <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"> <label for="knative-ip-address">
{{ s__('ClusterIntegration|Knative IP Address:') }} <strong>
{{ s__('ClusterIntegration|Knative Endpoint:') }}
</strong>
</label> </label>
<div v-if="knativeExternalIp" class="input-group"> <div v-if="knativeExternalIp" class="input-group">
<input <input
id="knative-ip-address" id="knative-ip-address"
:value="knativeExternalIp" :value="knativeExternalIp"
type="text" type="text"
class="form-control js-ip-address" class="form-control js-knative-ip-address"
readonly readonly
/> />
<span class="input-group-append"> <span class="input-group-append">
<clipboard-button <clipboard-button
:text="knativeExternalIp" :text="knativeExternalIp"
:title="s__('ClusterIntegration|Copy Knative IP Address to clipboard')" :title="s__('ClusterIntegration|Copy Knative Endpoint to clipboard')"
class="input-group-text js-clipboard-btn" class="input-group-text js-knative-ip-clipboard-btn"
/> />
</span> </span>
</div> </div>
<input v-else type="text" class="form-control js-ip-address" readonly value="?" /> <input
v-else
type="text"
class="form-control js-knative-ip-address"
readonly
value="?"
/>
</div> </div>
<p v-if="!knativeExternalIp" class="settings-message js-no-ip-message"> <p class="form-text text-muted col-12">
{{ {{
s__(`ClusterIntegration|The IP address is in s__(
the process of being assigned. Please check your Kubernetes `ClusterIntegration|To access your application after deployment, point a wildcard DNS to the Knative Endpoint.`,
cluster or Quotas on Google Kubernetes Engine if it takes a long time.`) )
}} }}
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
{{ __('More information') }}
</a>
</p> </p>
<p> <p
v-if="!knativeExternalIp"
class="settings-message js-no-knative-ip-message mt-2 mr-3 mb-0 ml-3 "
>
{{ {{
s__(`ClusterIntegration|Point a wildcard DNS to this s__(`ClusterIntegration|The IP address is in
generated IP address in order to access the process of being assigned. Please check your Kubernetes
your application after it has been deployed.`) cluster or Quotas on Google Kubernetes Engine if it takes a long time.`)
}} }}
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
{{ __('More information') }}
</a>
</p> </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> </template>
</div> </div>
</div>
</application-row> </application-row>
</div> </div>
</section> </section>
......
...@@ -12,6 +12,9 @@ export default class ClusterService { ...@@ -12,6 +12,9 @@ export default class ClusterService {
jupyter: this.options.installJupyterEndpoint, jupyter: this.options.installJupyterEndpoint,
knative: this.options.installKnativeEndpoint, knative: this.options.installKnativeEndpoint,
}; };
this.appUpdateEndpointMap = {
knative: this.options.updateKnativeEndpoint,
};
} }
fetchData() { fetchData() {
...@@ -22,6 +25,10 @@ export default class ClusterService { ...@@ -22,6 +25,10 @@ export default class ClusterService {
return axios.post(this.appInstallEndpointMap[appId], params); return axios.post(this.appInstallEndpointMap[appId], params);
} }
updateApplication(appId, params) {
return axios.patch(this.appUpdateEndpointMap[appId], params);
}
static updateCluster(endpoint, data) { static updateCluster(endpoint, data) {
return axios.put(endpoint, data); return axios.put(endpoint, data);
} }
......
...@@ -66,6 +66,7 @@ export default class ClusterStore { ...@@ -66,6 +66,7 @@ export default class ClusterStore {
requestStatus: null, requestStatus: null,
requestReason: null, requestReason: null,
hostname: null, hostname: null,
isEditingHostName: false,
externalIp: null, externalIp: null,
}, },
}, },
...@@ -129,8 +130,10 @@ export default class ClusterStore { ...@@ -129,8 +130,10 @@ export default class ClusterStore {
? `jupyter.${this.state.applications.ingress.externalIp}.nip.io` ? `jupyter.${this.state.applications.ingress.externalIp}.nip.io`
: ''); : '');
} else if (appId === KNATIVE) { } else if (appId === KNATIVE) {
if (!this.state.applications.knative.isEditingHostName) {
this.state.applications.knative.hostname = this.state.applications.knative.hostname =
serverAppEntry.hostname || this.state.applications.knative.hostname; serverAppEntry.hostname || this.state.applications.knative.hostname;
}
this.state.applications.knative.externalIp = this.state.applications.knative.externalIp =
serverAppEntry.external_ip || this.state.applications.knative.externalIp; serverAppEntry.external_ip || this.state.applications.knative.externalIp;
} else if (appId === RUNNER) { } else if (appId === RUNNER) {
......
...@@ -3,26 +3,41 @@ ...@@ -3,26 +3,41 @@
class Clusters::ApplicationsController < Clusters::BaseController class Clusters::ApplicationsController < Clusters::BaseController
before_action :cluster before_action :cluster
before_action :authorize_create_cluster!, only: [:create] before_action :authorize_create_cluster!, only: [:create]
before_action :authorize_update_cluster!, only: [:update]
def create def create
request_handler do
Clusters::Applications::CreateService Clusters::Applications::CreateService
.new(@cluster, current_user, create_cluster_application_params) .new(@cluster, current_user, cluster_application_params)
.execute(request) .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 head :no_content
rescue Clusters::Applications::CreateService::InvalidApplicationError rescue Clusters::Applications::BaseService::InvalidApplicationError
render_404 render_404
rescue StandardError rescue StandardError
head :bad_request head :bad_request
end end
private
def cluster def cluster
@cluster ||= clusterable.clusters.find(params[:id]) || render_404 @cluster ||= clusterable.clusters.find(params[:id]) || render_404
end end
def create_cluster_application_params def cluster_application_params
params.permit(:application, :hostname, :email) params.permit(:application, :hostname, :email)
end end
end end
...@@ -30,6 +30,12 @@ module Clusters ...@@ -30,6 +30,12 @@ module Clusters
# Override if you need extra data synchronized # Override if you need extra data synchronized
# from K8s after installation # from K8s after installation
end end
def update_command
install_command.tap do |command|
command.version = version
end
end
end end
end end
end end
......
...@@ -44,6 +44,10 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated ...@@ -44,6 +44,10 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated
raise NotImplementedError raise NotImplementedError
end end
def update_applications_cluster_path(cluster, application)
raise NotImplementedError
end
def cluster_path(cluster, params = {}) def cluster_path(cluster, params = {})
raise NotImplementedError raise NotImplementedError
end end
......
...@@ -14,6 +14,11 @@ class GroupClusterablePresenter < ClusterablePresenter ...@@ -14,6 +14,11 @@ class GroupClusterablePresenter < ClusterablePresenter
install_applications_group_cluster_path(clusterable, cluster, application) install_applications_group_cluster_path(clusterable, cluster, application)
end 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 override :cluster_path
def cluster_path(cluster, params = {}) def cluster_path(cluster, params = {})
group_cluster_path(clusterable, cluster, params) group_cluster_path(clusterable, cluster, params)
......
...@@ -14,6 +14,11 @@ class ProjectClusterablePresenter < ClusterablePresenter ...@@ -14,6 +14,11 @@ class ProjectClusterablePresenter < ClusterablePresenter
install_applications_project_cluster_path(clusterable, cluster, application) install_applications_project_cluster_path(clusterable, cluster, application)
end 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 override :cluster_path
def cluster_path(cluster, params = {}) def cluster_path(cluster, params = {})
project_cluster_path(clusterable, cluster, params) project_cluster_path(clusterable, cluster, params)
......
...@@ -46,6 +46,10 @@ module Clusters ...@@ -46,6 +46,10 @@ module Clusters
@install_command ||= app.install_command @install_command ||= app.install_command
end end
def update_command
@update_command ||= app.update_command
end
def upgrade_command(new_values = "") def upgrade_command(new_values = "")
app.upgrade_command(new_values) app.upgrade_command(new_values)
end end
......
# 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
...@@ -2,47 +2,11 @@ ...@@ -2,47 +2,11 @@
module Clusters module Clusters
module Applications module Applications
class CreateService class CreateService < Clusters::Applications::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)
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
private private
def create_application def worker_class(application)
builder.call(@cluster) application.updateable? ? ClusterUpgradeAppWorker : ClusterInstallAppWorker
end
def builder
builders[application_name] || raise(InvalidApplicationError, "invalid application: #{application_name}")
end end
def builders def builders
...@@ -65,21 +29,6 @@ module Clusters ...@@ -65,21 +29,6 @@ module Clusters
"knative" => -> (cluster) { cluster.application_knative || cluster.build_application_knative } "knative" => -> (cluster) { cluster.application_knative || cluster.build_application_knative }
} }
end 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 end
end end
...@@ -6,7 +6,6 @@ module Clusters ...@@ -6,7 +6,6 @@ module Clusters
def execute def execute
return unless app.scheduled? return unless app.scheduled?
begin
app.make_installing! app.make_installing!
helm_api.install(install_command) helm_api.install(install_command)
...@@ -21,5 +20,4 @@ module Clusters ...@@ -21,5 +20,4 @@ module Clusters
end end
end end
end end
end
end end
# 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
# 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
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
install_runner_path: clusterable.install_applications_cluster_path(@cluster, :runner), install_runner_path: clusterable.install_applications_cluster_path(@cluster, :runner),
install_jupyter_path: clusterable.install_applications_cluster_path(@cluster, :jupyter), install_jupyter_path: clusterable.install_applications_cluster_path(@cluster, :jupyter),
install_knative_path: clusterable.install_applications_cluster_path(@cluster, :knative), 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', toggle_status: @cluster.enabled? ? 'true': 'false',
has_rbac: @cluster.platform_kubernetes_rbac? ? 'true': 'false', has_rbac: @cluster.platform_kubernetes_rbac? ? 'true': 'false',
cluster_type: @cluster.cluster_type, cluster_type: @cluster.cluster_type,
......
...@@ -23,6 +23,7 @@ ...@@ -23,6 +23,7 @@
- cronjob:prune_web_hook_logs - cronjob:prune_web_hook_logs
- gcp_cluster:cluster_install_app - gcp_cluster:cluster_install_app
- gcp_cluster:cluster_patch_app
- gcp_cluster:cluster_upgrade_app - gcp_cluster:cluster_upgrade_app
- gcp_cluster:cluster_provision - gcp_cluster:cluster_provision
- gcp_cluster:cluster_wait_for_app_installation - gcp_cluster:cluster_wait_for_app_installation
......
# 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
---
title: Edit Knative domain after it has been deployed
merge_request: 25386
author:
type: added
...@@ -101,6 +101,7 @@ Rails.application.routes.draw do ...@@ -101,6 +101,7 @@ Rails.application.routes.draw do
member do member do
scope :applications do scope :applications do
post '/:application', to: 'clusters/applications#create', as: :install_applications post '/:application', to: 'clusters/applications#create', as: :install_applications
patch '/:application', to: 'clusters/applications#update', as: :update_applications
end end
get :cluster_status, format: :json get :cluster_status, format: :json
......
...@@ -7,7 +7,8 @@ module Gitlab ...@@ -7,7 +7,8 @@ module Gitlab
include BaseCommand include BaseCommand
include ClientCommand 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) def initialize(name:, chart:, files:, rbac:, version: nil, repository: nil, preinstall: nil, postinstall: nil)
@name = name @name = name
......
...@@ -1698,7 +1698,7 @@ msgstr "" ...@@ -1698,7 +1698,7 @@ msgstr ""
msgid "ClusterIntegration|Copy Jupyter Hostname to clipboard" msgid "ClusterIntegration|Copy Jupyter Hostname to clipboard"
msgstr "" msgstr ""
msgid "ClusterIntegration|Copy Knative IP Address to clipboard" msgid "ClusterIntegration|Copy Knative Endpoint to clipboard"
msgstr "" msgstr ""
msgid "ClusterIntegration|Copy Kubernetes cluster name" msgid "ClusterIntegration|Copy Kubernetes cluster name"
...@@ -1821,7 +1821,7 @@ msgstr "" ...@@ -1821,7 +1821,7 @@ msgstr ""
msgid "ClusterIntegration|Knative Domain Name:" msgid "ClusterIntegration|Knative Domain Name:"
msgstr "" msgstr ""
msgid "ClusterIntegration|Knative IP Address:" msgid "ClusterIntegration|Knative Endpoint:"
msgstr "" 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." 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 "" ...@@ -1941,7 +1941,7 @@ msgstr ""
msgid "ClusterIntegration|Request to begin installing failed" msgid "ClusterIntegration|Request to begin installing failed"
msgstr "" msgstr ""
msgid "ClusterIntegration|Retry upgrade" msgid "ClusterIntegration|Retry update"
msgstr "" msgstr ""
msgid "ClusterIntegration|Save changes" msgid "ClusterIntegration|Save changes"
...@@ -1986,9 +1986,6 @@ msgstr "" ...@@ -1986,9 +1986,6 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong on our end." msgid "ClusterIntegration|Something went wrong on our end."
msgstr "" 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" msgid "ClusterIntegration|Something went wrong while creating your Kubernetes cluster on Google Kubernetes Engine"
msgstr "" msgstr ""
...@@ -2007,12 +2004,21 @@ msgstr "" ...@@ -2007,12 +2004,21 @@ msgstr ""
msgid "ClusterIntegration|This option will allow you to install applications on RBAC clusters." msgid "ClusterIntegration|This option will allow you to install applications on RBAC clusters."
msgstr "" msgstr ""
msgid "ClusterIntegration|To access your application after deployment, point a wildcard DNS to the Knative Endpoint."
msgstr ""
msgid "ClusterIntegration|Toggle Kubernetes cluster" msgid "ClusterIntegration|Toggle Kubernetes cluster"
msgstr "" msgstr ""
msgid "ClusterIntegration|Token" msgid "ClusterIntegration|Token"
msgstr "" msgstr ""
msgid "ClusterIntegration|Update failed. Please check the logs and try again."
msgstr ""
msgid "ClusterIntegration|Updating"
msgstr ""
msgid "ClusterIntegration|Upgrade" msgid "ClusterIntegration|Upgrade"
msgstr "" msgstr ""
......
...@@ -9,9 +9,25 @@ describe Groups::Clusters::ApplicationsController do ...@@ -9,9 +9,25 @@ describe Groups::Clusters::ApplicationsController do
Clusters::Cluster::APPLICATIONS[application] Clusters::Cluster::APPLICATIONS[application]
end end
describe 'POST create' do 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(:cluster) { create(:cluster, :group, :provided_by_gcp) }
let(:group) { cluster.group } let(:group) { cluster.group }
describe 'POST create' do
subject do
post :create, params: params.merge(group_id: group)
end
let(:application) { 'helm' } let(:application) { 'helm' }
let(:params) { { application: application, id: cluster.id } } let(:params) { { application: application, id: cluster.id } }
...@@ -26,7 +42,7 @@ describe Groups::Clusters::ApplicationsController do ...@@ -26,7 +42,7 @@ describe Groups::Clusters::ApplicationsController do
it 'schedule an application installation' do it 'schedule an application installation' do
expect(ClusterInstallAppWorker).to receive(:perform_async).with(application, anything).once 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(response).to have_http_status(:no_content)
expect(cluster.application_helm).to be_scheduled expect(cluster.application_helm).to be_scheduled
end end
...@@ -37,7 +53,7 @@ describe Groups::Clusters::ApplicationsController do ...@@ -37,7 +53,7 @@ describe Groups::Clusters::ApplicationsController do
end end
it 'return 404' do 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) expect(response).to have_http_status(:not_found)
end end
end end
...@@ -46,9 +62,7 @@ describe Groups::Clusters::ApplicationsController do ...@@ -46,9 +62,7 @@ describe Groups::Clusters::ApplicationsController do
let(:application) { 'unkwnown-app' } let(:application) { 'unkwnown-app' }
it 'return 404' do it 'return 404' do
go is_expected.to have_http_status(:not_found)
expect(response).to have_http_status(:not_found)
end end
end end
...@@ -58,9 +72,7 @@ describe Groups::Clusters::ApplicationsController do ...@@ -58,9 +72,7 @@ describe Groups::Clusters::ApplicationsController do
end end
it 'returns 400' do it 'returns 400' do
go is_expected.to have_http_status(:bad_request)
expect(response).to have_http_status(:bad_request)
end end
end end
end end
...@@ -70,18 +82,66 @@ describe Groups::Clusters::ApplicationsController do ...@@ -70,18 +82,66 @@ describe Groups::Clusters::ApplicationsController do
allow(ClusterInstallAppWorker).to receive(:perform_async) allow(ClusterInstallAppWorker).to receive(:perform_async)
end end
it { expect { go }.to be_allowed_for(:admin) } it_behaves_like 'a secure endpoint'
it { expect { go }.to be_allowed_for(:owner).of(group) } end
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) }
end end
def go describe 'PATCH update' do
post :create, params: params.merge(group_id: group) 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 end
end end
...@@ -9,7 +9,22 @@ describe Projects::Clusters::ApplicationsController do ...@@ -9,7 +9,22 @@ describe Projects::Clusters::ApplicationsController do
Clusters::Cluster::APPLICATIONS[application] Clusters::Cluster::APPLICATIONS[application]
end 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 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(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:project) { cluster.project } let(:project) { cluster.project }
let(:application) { 'helm' } let(:application) { 'helm' }
...@@ -26,7 +41,7 @@ describe Projects::Clusters::ApplicationsController do ...@@ -26,7 +41,7 @@ describe Projects::Clusters::ApplicationsController do
it 'schedule an application installation' do it 'schedule an application installation' do
expect(ClusterInstallAppWorker).to receive(:perform_async).with(application, anything).once 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(response).to have_http_status(:no_content)
expect(cluster.application_helm).to be_scheduled expect(cluster.application_helm).to be_scheduled
end end
...@@ -37,7 +52,7 @@ describe Projects::Clusters::ApplicationsController do ...@@ -37,7 +52,7 @@ describe Projects::Clusters::ApplicationsController do
end end
it 'return 404' do 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) expect(response).to have_http_status(:not_found)
end end
end end
...@@ -46,9 +61,7 @@ describe Projects::Clusters::ApplicationsController do ...@@ -46,9 +61,7 @@ describe Projects::Clusters::ApplicationsController do
let(:application) { 'unkwnown-app' } let(:application) { 'unkwnown-app' }
it 'return 404' do it 'return 404' do
go is_expected.to have_http_status(:not_found)
expect(response).to have_http_status(:not_found)
end end
end end
...@@ -58,9 +71,7 @@ describe Projects::Clusters::ApplicationsController do ...@@ -58,9 +71,7 @@ describe Projects::Clusters::ApplicationsController do
end end
it 'returns 400' do it 'returns 400' do
go is_expected.to have_http_status(:bad_request)
expect(response).to have_http_status(:bad_request)
end end
end end
end end
...@@ -70,18 +81,68 @@ describe Projects::Clusters::ApplicationsController do ...@@ -70,18 +81,68 @@ describe Projects::Clusters::ApplicationsController do
allow(ClusterInstallAppWorker).to receive(:perform_async) allow(ClusterInstallAppWorker).to receive(:perform_async)
end end
it { expect { go }.to be_allowed_for(:admin) } it_behaves_like 'a secure endpoint'
it { expect { go }.to be_allowed_for(:owner).of(project) } end
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) }
end end
def go describe 'PATCH update' do
post :create, params: params.merge(namespace_id: project.namespace, project_id: project) 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 end
end end
...@@ -17,7 +17,7 @@ describe 'Clusters Applications', :js do ...@@ -17,7 +17,7 @@ describe 'Clusters Applications', :js do
end end
context 'when cluster is being created' do 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 it 'user is unable to install applications' do
page.within('.js-cluster-application-row-helm') do page.within('.js-cluster-application-row-helm') do
...@@ -28,9 +28,11 @@ describe 'Clusters Applications', :js do ...@@ -28,9 +28,11 @@ describe 'Clusters Applications', :js do
end end
context 'when cluster is created' do 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 it 'user can install applications' do
wait_for_requests
page.within('.js-cluster-application-row-helm') do page.within('.js-cluster-application-row-helm') do
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to be_nil 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') expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install')
...@@ -44,6 +46,8 @@ describe 'Clusters Applications', :js do ...@@ -44,6 +46,8 @@ describe 'Clusters Applications', :js do
page.within('.js-cluster-application-row-helm') do page.within('.js-cluster-application-row-helm') do
page.find(:css, '.js-cluster-application-install-button').click page.find(:css, '.js-cluster-application-install-button').click
end end
wait_for_requests
end end
it 'they see status transition' do it 'they see status transition' do
...@@ -52,8 +56,6 @@ describe 'Clusters Applications', :js 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.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing') expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing')
wait_until_helm_created!
Clusters::Cluster.last.application_helm.make_installing! Clusters::Cluster.last.application_helm.make_installing!
# FE starts polling and update the buttons to "Installing" # FE starts polling and update the buttons to "Installing"
...@@ -76,7 +78,7 @@ describe 'Clusters Applications', :js do ...@@ -76,7 +78,7 @@ describe 'Clusters Applications', :js do
end end
context 'on an abac cluster' do 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 it 'should show info block and not be installable' do
page.within('.js-cluster-application-row-knative') do page.within('.js-cluster-application-row-knative') do
...@@ -87,7 +89,7 @@ describe 'Clusters Applications', :js do ...@@ -87,7 +89,7 @@ describe 'Clusters Applications', :js do
end end
context 'on an rbac cluster' do 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 it 'should not show callout block and be installable' do
page.within('.js-cluster-application-row-knative') do page.within('.js-cluster-application-row-knative') do
...@@ -95,6 +97,60 @@ describe 'Clusters Applications', :js do ...@@ -95,6 +97,60 @@ describe 'Clusters Applications', :js do
expect(page).to have_css('.js-cluster-application-install-button:not([disabled])') expect(page).to have_css('.js-cluster-application-install-button:not([disabled])')
end end
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
end end
...@@ -148,6 +204,8 @@ describe 'Clusters Applications', :js do ...@@ -148,6 +204,8 @@ describe 'Clusters Applications', :js do
page.within('.js-cluster-application-row-ingress') do page.within('.js-cluster-application-row-ingress') do
expect(page).to have_css('.js-cluster-application-install-button:not([disabled])') expect(page).to have_css('.js-cluster-application-install-button:not([disabled])')
page.find(:css, '.js-cluster-application-install-button').click page.find(:css, '.js-cluster-application-install-button').click
wait_for_requests
end end
end end
...@@ -184,14 +242,4 @@ describe 'Clusters Applications', :js do ...@@ -184,14 +242,4 @@ describe 'Clusters Applications', :js do
end end
end 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 end
...@@ -230,7 +230,7 @@ describe('Application Row', () => { ...@@ -230,7 +230,7 @@ describe('Application Row', () => {
expect(upgradeBtn.innerHTML).toContain('Upgrade'); 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, { vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE, ...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.UPDATE_ERRORED, status: APPLICATION_STATUS.UPDATE_ERRORED,
...@@ -239,10 +239,10 @@ describe('Application Row', () => { ...@@ -239,10 +239,10 @@ describe('Application Row', () => {
expect(upgradeBtn).not.toBe(null); expect(upgradeBtn).not.toBe(null);
expect(vm.upgradeFailed).toBe(true); 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, { vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE, ...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.UPDATING, status: APPLICATION_STATUS.UPDATING,
...@@ -251,7 +251,7 @@ describe('Application Row', () => { ...@@ -251,7 +251,7 @@ describe('Application Row', () => {
expect(upgradeBtn).not.toBe(null); expect(upgradeBtn).not.toBe(null);
expect(vm.isUpgrading).toBe(true); expect(vm.isUpgrading).toBe(true);
expect(upgradeBtn.innerHTML).toContain('Upgrading'); expect(upgradeBtn.innerHTML).toContain('Updating');
}); });
it('clicking upgrade button emits event', () => { it('clicking upgrade button emits event', () => {
...@@ -295,7 +295,7 @@ describe('Application Row', () => { ...@@ -295,7 +295,7 @@ describe('Application Row', () => {
expect(failureMessage).not.toBe(null); expect(failureMessage).not.toBe(null);
expect(failureMessage.innerHTML).toContain( 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.',
); );
}); });
}); });
......
import Vue from 'vue'; import Vue from 'vue';
import applications from '~/clusters/components/applications.vue'; import applications from '~/clusters/components/applications.vue';
import { CLUSTER_TYPE } from '~/clusters/constants'; import { CLUSTER_TYPE } from '~/clusters/constants';
import eventHub from '~/clusters/event_hub';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { APPLICATIONS_MOCK_STATE } from '../services/mock_data';
describe('Applications', () => { describe('Applications', () => {
let vm; let vm;
...@@ -18,16 +20,8 @@ describe('Applications', () => { ...@@ -18,16 +20,8 @@ describe('Applications', () => {
describe('Project cluster applications', () => { describe('Project cluster applications', () => {
beforeEach(() => { beforeEach(() => {
vm = mountComponent(Applications, { vm = mountComponent(Applications, {
applications: APPLICATIONS_MOCK_STATE,
type: CLUSTER_TYPE.PROJECT, 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', () => { ...@@ -64,15 +58,7 @@ describe('Applications', () => {
beforeEach(() => { beforeEach(() => {
vm = mountComponent(Applications, { vm = mountComponent(Applications, {
type: CLUSTER_TYPE.GROUP, type: CLUSTER_TYPE.GROUP,
applications: { applications: APPLICATIONS_MOCK_STATE,
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' },
},
}); });
}); });
...@@ -111,17 +97,12 @@ describe('Applications', () => { ...@@ -111,17 +97,12 @@ describe('Applications', () => {
it('renders ip address with a clipboard button', () => { it('renders ip address with a clipboard button', () => {
vm = mountComponent(Applications, { vm = mountComponent(Applications, {
applications: { applications: {
...APPLICATIONS_MOCK_STATE,
ingress: { ingress: {
title: 'Ingress', title: 'Ingress',
status: 'installed', status: 'installed',
externalIp: '0.0.0.0', 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', () => { ...@@ -137,16 +118,11 @@ describe('Applications', () => {
it('renders an input text with a question mark and an alert text', () => { it('renders an input text with a question mark and an alert text', () => {
vm = mountComponent(Applications, { vm = mountComponent(Applications, {
applications: { applications: {
...APPLICATIONS_MOCK_STATE,
ingress: { ingress: {
title: 'Ingress', title: 'Ingress',
status: 'installed', 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', () => { ...@@ -160,15 +136,7 @@ describe('Applications', () => {
describe('before installing', () => { describe('before installing', () => {
it('does not render the IP address', () => { it('does not render the IP address', () => {
vm = mountComponent(Applications, { vm = mountComponent(Applications, {
applications: { applications: APPLICATIONS_MOCK_STATE,
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: '' },
},
}); });
expect(vm.$el.textContent).not.toContain('Ingress IP Address'); expect(vm.$el.textContent).not.toContain('Ingress IP Address');
...@@ -181,17 +149,12 @@ describe('Applications', () => { ...@@ -181,17 +149,12 @@ describe('Applications', () => {
it('renders email & allows editing', () => { it('renders email & allows editing', () => {
vm = mountComponent(Applications, { vm = mountComponent(Applications, {
applications: { applications: {
helm: { title: 'Helm Tiller', status: 'installed' }, ...APPLICATIONS_MOCK_STATE,
ingress: { title: 'Ingress', status: 'installed', externalIp: '1.1.1.1' },
cert_manager: { cert_manager: {
title: 'Cert-Manager', title: 'Cert-Manager',
email: 'before@example.com', email: 'before@example.com',
status: 'installable', 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', () => { ...@@ -204,17 +167,12 @@ describe('Applications', () => {
it('renders email in readonly', () => { it('renders email in readonly', () => {
vm = mountComponent(Applications, { vm = mountComponent(Applications, {
applications: { applications: {
helm: { title: 'Helm Tiller', status: 'installed' }, ...APPLICATIONS_MOCK_STATE,
ingress: { title: 'Ingress', status: 'installed', externalIp: '1.1.1.1' },
cert_manager: { cert_manager: {
title: 'Cert-Manager', title: 'Cert-Manager',
email: 'after@example.com', email: 'after@example.com',
status: 'installed', 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', () => { ...@@ -229,13 +187,12 @@ describe('Applications', () => {
it('renders hostname active input', () => { it('renders hostname active input', () => {
vm = mountComponent(Applications, { vm = mountComponent(Applications, {
applications: { applications: {
helm: { title: 'Helm Tiller', status: 'installed' }, ...APPLICATIONS_MOCK_STATE,
ingress: { title: 'Ingress', status: 'installed', externalIp: '1.1.1.1' }, ingress: {
cert_manager: { title: 'Cert-Manager' }, title: 'Ingress',
runner: { title: 'GitLab Runner' }, status: 'installed',
prometheus: { title: 'Prometheus' }, externalIp: '1.1.1.1',
jupyter: { title: 'JupyterHub', hostname: '', status: 'installable' }, },
knative: { title: 'Knative', hostname: '', status: 'installable' },
}, },
}); });
...@@ -247,13 +204,8 @@ describe('Applications', () => { ...@@ -247,13 +204,8 @@ describe('Applications', () => {
it('does not render hostname input', () => { it('does not render hostname input', () => {
vm = mountComponent(Applications, { vm = mountComponent(Applications, {
applications: { applications: {
helm: { title: 'Helm Tiller', status: 'installed' }, ...APPLICATIONS_MOCK_STATE,
ingress: { title: 'Ingress', status: 'installed' }, 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', () => { ...@@ -265,13 +217,9 @@ describe('Applications', () => {
it('renders readonly input', () => { it('renders readonly input', () => {
vm = mountComponent(Applications, { vm = mountComponent(Applications, {
applications: { applications: {
helm: { title: 'Helm Tiller', status: 'installed' }, ...APPLICATIONS_MOCK_STATE,
ingress: { title: 'Ingress', status: 'installed', externalIp: '1.1.1.1' }, 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: '' }, jupyter: { title: 'JupyterHub', status: 'installed', hostname: '' },
knative: { title: 'Knative', status: 'installed', hostname: '' },
}, },
}); });
...@@ -282,15 +230,7 @@ describe('Applications', () => { ...@@ -282,15 +230,7 @@ describe('Applications', () => {
describe('without ingress installed', () => { describe('without ingress installed', () => {
beforeEach(() => { beforeEach(() => {
vm = mountComponent(Applications, { vm = mountComponent(Applications, {
applications: { applications: APPLICATIONS_MOCK_STATE,
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' },
},
}); });
}); });
...@@ -310,4 +250,77 @@ describe('Applications', () => { ...@@ -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);
});
});
});
});
}); });
...@@ -115,4 +115,14 @@ const DEFAULT_APPLICATION_STATE = { ...@@ -115,4 +115,14 @@ const DEFAULT_APPLICATION_STATE = {
requestReason: null, 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 };
...@@ -111,6 +111,7 @@ describe('Clusters Store', () => { ...@@ -111,6 +111,7 @@ describe('Clusters Store', () => {
requestStatus: null, requestStatus: null,
requestReason: null, requestReason: null,
hostname: null, hostname: null,
isEditingHostName: false,
externalIp: null, externalIp: null,
}, },
cert_manager: { cert_manager: {
......
...@@ -66,9 +66,7 @@ describe Clusters::Applications::Knative do ...@@ -66,9 +66,7 @@ describe Clusters::Applications::Knative do
end end
end end
describe '#install_command' do shared_examples 'a command' do
subject { knative.install_command }
it 'should be an instance of Helm::InstallCommand' do it 'should be an instance of Helm::InstallCommand' do
expect(subject).to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) expect(subject).to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand)
end end
...@@ -76,7 +74,6 @@ describe Clusters::Applications::Knative do ...@@ -76,7 +74,6 @@ describe Clusters::Applications::Knative do
it 'should be initialized with knative arguments' do it 'should be initialized with knative arguments' do
expect(subject.name).to eq('knative') expect(subject.name).to eq('knative')
expect(subject.chart).to eq('knative/knative') expect(subject.chart).to eq('knative/knative')
expect(subject.version).to eq('0.2.2')
expect(subject.files).to eq(knative.files) expect(subject.files).to eq(knative.files)
end end
...@@ -98,6 +95,27 @@ describe Clusters::Applications::Knative do ...@@ -98,6 +95,27 @@ describe Clusters::Applications::Knative do
end end
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 describe '#files' do
let(:application) { knative } let(:application) { knative }
let(:values) { subject[:'values.yaml'] } let(:values) { subject[:'values.yaml'] }
......
...@@ -69,6 +69,14 @@ describe GroupClusterablePresenter do ...@@ -69,6 +69,14 @@ describe GroupClusterablePresenter do
it { is_expected.to eq(install_applications_group_cluster_path(group, cluster, application)) } it { is_expected.to eq(install_applications_group_cluster_path(group, cluster, application)) }
end 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 describe '#cluster_path' do
subject { presenter.cluster_path(cluster) } subject { presenter.cluster_path(cluster) }
......
...@@ -69,6 +69,14 @@ describe ProjectClusterablePresenter do ...@@ -69,6 +69,14 @@ describe ProjectClusterablePresenter do
it { is_expected.to eq(install_applications_project_cluster_path(project, cluster, application)) } it { is_expected.to eq(install_applications_project_cluster_path(project, cluster, application)) }
end 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 describe '#cluster_path' do
subject { presenter.cluster_path(cluster) } subject { presenter.cluster_path(cluster) }
......
# 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
# 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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment