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 {
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;
......
......@@ -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 }}
......
......@@ -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>
......
......@@ -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);
}
......
......@@ -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) {
......
......@@ -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
......@@ -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
......
......@@ -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
......
......@@ -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)
......
......@@ -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)
......
......@@ -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
......
# 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 @@
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
......@@ -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
......
# 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