541 lines
17 KiB
JavaScript
541 lines
17 KiB
JavaScript
import Visibility from 'visibilityjs';
|
|
import Vue from 'vue';
|
|
import { GlToast } from '@gitlab/ui';
|
|
import AccessorUtilities from '~/lib/utils/accessor';
|
|
import PersistentUserCallout from '../persistent_user_callout';
|
|
import { s__, sprintf } from '../locale';
|
|
import { deprecatedCreateFlash as Flash } from '../flash';
|
|
import Poll from '../lib/utils/poll';
|
|
import initSettingsPanels from '../settings_panels';
|
|
import eventHub from './event_hub';
|
|
import { APPLICATION_STATUS, CROSSPLANE, KNATIVE, FLUENTD } from './constants';
|
|
import ClustersService from './services/clusters_service';
|
|
import ClustersStore from './stores/clusters_store';
|
|
import Applications from './components/applications.vue';
|
|
import RemoveClusterConfirmation from './components/remove_cluster_confirmation.vue';
|
|
import initProjectSelectDropdown from '~/project_select';
|
|
import initServerlessSurveyBanner from '~/serverless/survey_banner';
|
|
|
|
const Environments = () => import('ee_component/clusters/components/environments.vue');
|
|
|
|
Vue.use(GlToast);
|
|
|
|
/**
|
|
* Cluster page has 2 separate parts:
|
|
* Toggle button and applications section
|
|
*
|
|
* - Polling status while creating or scheduled
|
|
* - Update status area with the response result
|
|
*/
|
|
|
|
export default class Clusters {
|
|
constructor() {
|
|
const {
|
|
statusPath,
|
|
installHelmPath,
|
|
installIngressPath,
|
|
installCertManagerPath,
|
|
installRunnerPath,
|
|
installJupyterPath,
|
|
installKnativePath,
|
|
updateKnativePath,
|
|
installElasticStackPath,
|
|
installCrossplanePath,
|
|
installPrometheusPath,
|
|
installFluentdPath,
|
|
managePrometheusPath,
|
|
clusterEnvironmentsPath,
|
|
hasRbac,
|
|
providerType,
|
|
preInstalledKnative,
|
|
clusterType,
|
|
clusterStatus,
|
|
clusterStatusReason,
|
|
helpPath,
|
|
ingressHelpPath,
|
|
ingressDnsHelpPath,
|
|
ingressModSecurityHelpPath,
|
|
environmentsHelpPath,
|
|
clustersHelpPath,
|
|
deployBoardsHelpPath,
|
|
cloudRunHelpPath,
|
|
clusterId,
|
|
ciliumHelpPath,
|
|
} = document.querySelector('.js-edit-cluster-form').dataset;
|
|
|
|
this.clusterId = clusterId;
|
|
this.clusterNewlyCreatedKey = `cluster_${this.clusterId}_newly_created`;
|
|
this.clusterBannerDismissedKey = `cluster_${this.clusterId}_banner_dismissed`;
|
|
|
|
this.store = new ClustersStore();
|
|
this.store.setHelpPaths(
|
|
helpPath,
|
|
ingressHelpPath,
|
|
ingressDnsHelpPath,
|
|
ingressModSecurityHelpPath,
|
|
environmentsHelpPath,
|
|
clustersHelpPath,
|
|
deployBoardsHelpPath,
|
|
cloudRunHelpPath,
|
|
ciliumHelpPath,
|
|
);
|
|
this.store.setManagePrometheusPath(managePrometheusPath);
|
|
this.store.updateStatus(clusterStatus);
|
|
this.store.updateStatusReason(clusterStatusReason);
|
|
this.store.updateProviderType(providerType);
|
|
this.store.updatePreInstalledKnative(preInstalledKnative);
|
|
this.store.updateRbac(hasRbac);
|
|
this.service = new ClustersService({
|
|
endpoint: statusPath,
|
|
installHelmEndpoint: installHelmPath,
|
|
installIngressEndpoint: installIngressPath,
|
|
installCertManagerEndpoint: installCertManagerPath,
|
|
installCrossplaneEndpoint: installCrossplanePath,
|
|
installRunnerEndpoint: installRunnerPath,
|
|
installPrometheusEndpoint: installPrometheusPath,
|
|
installJupyterEndpoint: installJupyterPath,
|
|
installKnativeEndpoint: installKnativePath,
|
|
updateKnativeEndpoint: updateKnativePath,
|
|
installElasticStackEndpoint: installElasticStackPath,
|
|
clusterEnvironmentsEndpoint: clusterEnvironmentsPath,
|
|
installFluentdEndpoint: installFluentdPath,
|
|
});
|
|
|
|
this.installApplication = this.installApplication.bind(this);
|
|
|
|
this.errorContainer = document.querySelector('.js-cluster-error');
|
|
this.successContainer = document.querySelector('.js-cluster-success');
|
|
this.creatingContainer = document.querySelector('.js-cluster-creating');
|
|
this.unreachableContainer = document.querySelector('.js-cluster-api-unreachable');
|
|
this.authenticationFailureContainer = document.querySelector(
|
|
'.js-cluster-authentication-failure',
|
|
);
|
|
this.errorReasonContainer = this.errorContainer.querySelector('.js-error-reason');
|
|
this.successApplicationContainer = document.querySelector('.js-cluster-application-notice');
|
|
this.tokenField = document.querySelector('.js-cluster-token');
|
|
|
|
initProjectSelectDropdown();
|
|
Clusters.initDismissableCallout();
|
|
initSettingsPanels();
|
|
|
|
this.initApplications(clusterType);
|
|
this.initEnvironments();
|
|
|
|
if (clusterEnvironmentsPath && this.environments) {
|
|
this.store.toggleFetchEnvironments(true);
|
|
|
|
this.initPolling(
|
|
'fetchClusterEnvironments',
|
|
data => this.handleClusterEnvironmentsSuccess(data),
|
|
() => this.handleEnvironmentsPollError(),
|
|
);
|
|
}
|
|
|
|
this.updateContainer(null, this.store.state.status, this.store.state.statusReason);
|
|
|
|
this.addListeners();
|
|
if (statusPath && !this.environments) {
|
|
this.initPolling(
|
|
'fetchClusterStatus',
|
|
data => this.handleClusterStatusSuccess(data),
|
|
() => this.handlePollError(),
|
|
);
|
|
}
|
|
|
|
this.initRemoveClusterActions();
|
|
}
|
|
|
|
initApplications(type) {
|
|
const { store } = this;
|
|
const el = document.querySelector('#js-cluster-applications');
|
|
|
|
this.applications = new Vue({
|
|
el,
|
|
data() {
|
|
return {
|
|
state: store.state,
|
|
};
|
|
},
|
|
render(createElement) {
|
|
return createElement(Applications, {
|
|
props: {
|
|
type,
|
|
applications: this.state.applications,
|
|
helpPath: this.state.helpPath,
|
|
ingressHelpPath: this.state.ingressHelpPath,
|
|
managePrometheusPath: this.state.managePrometheusPath,
|
|
ingressDnsHelpPath: this.state.ingressDnsHelpPath,
|
|
ingressModSecurityHelpPath: this.state.ingressModSecurityHelpPath,
|
|
cloudRunHelpPath: this.state.cloudRunHelpPath,
|
|
providerType: this.state.providerType,
|
|
preInstalledKnative: this.state.preInstalledKnative,
|
|
rbac: this.state.rbac,
|
|
ciliumHelpPath: this.state.ciliumHelpPath,
|
|
},
|
|
});
|
|
},
|
|
});
|
|
}
|
|
|
|
initEnvironments() {
|
|
const { store } = this;
|
|
const el = document.querySelector('#js-cluster-environments');
|
|
|
|
if (!el) {
|
|
return;
|
|
}
|
|
|
|
this.environments = new Vue({
|
|
el,
|
|
data() {
|
|
return {
|
|
state: store.state,
|
|
};
|
|
},
|
|
render(createElement) {
|
|
return createElement(Environments, {
|
|
props: {
|
|
isFetching: this.state.fetchingEnvironments,
|
|
environments: this.state.environments,
|
|
environmentsHelpPath: this.state.environmentsHelpPath,
|
|
clustersHelpPath: this.state.clustersHelpPath,
|
|
deployBoardsHelpPath: this.state.deployBoardsHelpPath,
|
|
},
|
|
});
|
|
},
|
|
});
|
|
}
|
|
|
|
initRemoveClusterActions() {
|
|
const el = document.querySelector('#js-cluster-remove-actions');
|
|
if (el && el.dataset) {
|
|
const { clusterName, clusterPath, hasManagementProject } = el.dataset;
|
|
|
|
this.removeClusterAction = new Vue({
|
|
el,
|
|
render(createElement) {
|
|
return createElement(RemoveClusterConfirmation, {
|
|
props: {
|
|
clusterName,
|
|
clusterPath,
|
|
hasManagementProject,
|
|
},
|
|
});
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
handleClusterEnvironmentsSuccess(data) {
|
|
this.store.toggleFetchEnvironments(false);
|
|
this.store.updateEnvironments(data.data);
|
|
}
|
|
|
|
static initDismissableCallout() {
|
|
const callout = document.querySelector('.js-cluster-security-warning');
|
|
PersistentUserCallout.factory(callout);
|
|
}
|
|
|
|
addBannerCloseHandler(el, status) {
|
|
el.querySelector('.js-close-banner').addEventListener('click', () => {
|
|
el.classList.add('hidden');
|
|
this.setBannerDismissedState(status, true);
|
|
});
|
|
}
|
|
|
|
addListeners() {
|
|
eventHub.$on('installApplication', this.installApplication);
|
|
eventHub.$on('updateApplication', data => this.updateApplication(data));
|
|
eventHub.$on('saveKnativeDomain', data => this.saveKnativeDomain(data));
|
|
eventHub.$on('setKnativeDomain', data => this.setKnativeDomain(data));
|
|
eventHub.$on('uninstallApplication', data => this.uninstallApplication(data));
|
|
eventHub.$on('setCrossplaneProviderStack', data => this.setCrossplaneProviderStack(data));
|
|
eventHub.$on('setIngressModSecurityEnabled', data => this.setIngressModSecurityEnabled(data));
|
|
eventHub.$on('setIngressModSecurityMode', data => this.setIngressModSecurityMode(data));
|
|
eventHub.$on('resetIngressModSecurityChanges', id => this.resetIngressModSecurityChanges(id));
|
|
eventHub.$on('setFluentdSettings', data => this.setFluentdSettings(data));
|
|
// Add event listener to all the banner close buttons
|
|
this.addBannerCloseHandler(this.unreachableContainer, 'unreachable');
|
|
this.addBannerCloseHandler(this.authenticationFailureContainer, 'authentication_failure');
|
|
}
|
|
|
|
removeListeners() {
|
|
eventHub.$off('installApplication', this.installApplication);
|
|
eventHub.$off('updateApplication', this.updateApplication);
|
|
eventHub.$off('saveKnativeDomain');
|
|
eventHub.$off('setKnativeDomain');
|
|
eventHub.$off('setCrossplaneProviderStack');
|
|
eventHub.$off('uninstallApplication');
|
|
eventHub.$off('setIngressModSecurityEnabled');
|
|
eventHub.$off('setIngressModSecurityMode');
|
|
eventHub.$off('resetIngressModSecurityChanges');
|
|
eventHub.$off('setFluentdSettings');
|
|
}
|
|
|
|
initPolling(method, successCallback, errorCallback) {
|
|
this.poll = new Poll({
|
|
resource: this.service,
|
|
method,
|
|
successCallback,
|
|
errorCallback,
|
|
});
|
|
|
|
if (!Visibility.hidden()) {
|
|
this.poll.makeRequest();
|
|
}
|
|
|
|
Visibility.change(() => {
|
|
if (!Visibility.hidden() && !this.destroyed) {
|
|
this.poll.restart();
|
|
} else {
|
|
this.poll.stop();
|
|
}
|
|
});
|
|
}
|
|
|
|
handlePollError() {
|
|
this.constructor.handleError();
|
|
}
|
|
|
|
handleEnvironmentsPollError() {
|
|
this.store.toggleFetchEnvironments(false);
|
|
|
|
this.handlePollError();
|
|
}
|
|
|
|
static handleError() {
|
|
Flash(s__('ClusterIntegration|Something went wrong on our end.'));
|
|
}
|
|
|
|
handleClusterStatusSuccess(data) {
|
|
const prevStatus = this.store.state.status;
|
|
const prevApplicationMap = { ...this.store.state.applications };
|
|
|
|
this.store.updateStateFromServer(data.data);
|
|
|
|
this.checkForNewInstalls(prevApplicationMap, this.store.state.applications);
|
|
this.updateContainer(prevStatus, this.store.state.status, this.store.state.statusReason);
|
|
|
|
if (this.store.state.applications[KNATIVE]?.status === APPLICATION_STATUS.INSTALLED) {
|
|
initServerlessSurveyBanner();
|
|
}
|
|
}
|
|
|
|
hideAll() {
|
|
this.errorContainer.classList.add('hidden');
|
|
this.successContainer.classList.add('hidden');
|
|
this.creatingContainer.classList.add('hidden');
|
|
this.unreachableContainer.classList.add('hidden');
|
|
this.authenticationFailureContainer.classList.add('hidden');
|
|
}
|
|
|
|
checkForNewInstalls(prevApplicationMap, newApplicationMap) {
|
|
const appTitles = Object.keys(newApplicationMap)
|
|
.filter(
|
|
appId =>
|
|
newApplicationMap[appId].status === APPLICATION_STATUS.INSTALLED &&
|
|
prevApplicationMap[appId].status !== APPLICATION_STATUS.INSTALLED &&
|
|
prevApplicationMap[appId].status !== null,
|
|
)
|
|
.map(appId => newApplicationMap[appId].title);
|
|
|
|
if (appTitles.length > 0) {
|
|
const text = sprintf(
|
|
s__('ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster'),
|
|
{
|
|
appList: appTitles.join(', '),
|
|
},
|
|
);
|
|
Flash(text, 'notice', this.successApplicationContainer);
|
|
}
|
|
}
|
|
|
|
setBannerDismissedState(status, isDismissed) {
|
|
if (AccessorUtilities.isLocalStorageAccessSafe()) {
|
|
window.localStorage.setItem(this.clusterBannerDismissedKey, `${status}_${isDismissed}`);
|
|
}
|
|
}
|
|
|
|
isBannerDismissed(status) {
|
|
let bannerState;
|
|
if (AccessorUtilities.isLocalStorageAccessSafe()) {
|
|
bannerState = window.localStorage.getItem(this.clusterBannerDismissedKey);
|
|
}
|
|
|
|
return bannerState === `${status}_true`;
|
|
}
|
|
|
|
setClusterNewlyCreated(state) {
|
|
if (AccessorUtilities.isLocalStorageAccessSafe()) {
|
|
window.localStorage.setItem(this.clusterNewlyCreatedKey, Boolean(state));
|
|
}
|
|
}
|
|
|
|
isClusterNewlyCreated() {
|
|
// once this is true, it will always be true for a given page load
|
|
if (!this.isNewlyCreated) {
|
|
let newlyCreated;
|
|
if (AccessorUtilities.isLocalStorageAccessSafe()) {
|
|
newlyCreated = window.localStorage.getItem(this.clusterNewlyCreatedKey);
|
|
}
|
|
|
|
this.isNewlyCreated = newlyCreated === 'true';
|
|
}
|
|
return this.isNewlyCreated;
|
|
}
|
|
|
|
updateContainer(prevStatus, status, error) {
|
|
if (status !== 'created' && this.isBannerDismissed(status)) {
|
|
return;
|
|
}
|
|
this.setBannerDismissedState(status, false);
|
|
|
|
if (prevStatus !== status) {
|
|
this.hideAll();
|
|
|
|
switch (status) {
|
|
case 'created':
|
|
if (this.isClusterNewlyCreated()) {
|
|
this.setClusterNewlyCreated(false);
|
|
this.successContainer.classList.remove('hidden');
|
|
} else if (prevStatus) {
|
|
this.setClusterNewlyCreated(true);
|
|
window.location.reload();
|
|
}
|
|
break;
|
|
case 'errored':
|
|
this.errorContainer.classList.remove('hidden');
|
|
this.errorReasonContainer.textContent = error;
|
|
break;
|
|
case 'unreachable':
|
|
this.unreachableContainer.classList.remove('hidden');
|
|
break;
|
|
case 'authentication_failure':
|
|
this.authenticationFailureContainer.classList.remove('hidden');
|
|
break;
|
|
case 'scheduled':
|
|
case 'creating':
|
|
this.creatingContainer.classList.remove('hidden');
|
|
break;
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
installApplication({ id: appId, params }) {
|
|
return Clusters.validateInstallation(appId, params)
|
|
.then(() => {
|
|
this.store.updateAppProperty(appId, 'requestReason', null);
|
|
this.store.updateAppProperty(appId, 'statusReason', null);
|
|
this.store.installApplication(appId);
|
|
|
|
// eslint-disable-next-line promise/no-nesting
|
|
this.service.installApplication(appId, params).catch(() => {
|
|
this.store.notifyInstallFailure(appId);
|
|
this.store.updateAppProperty(
|
|
appId,
|
|
'requestReason',
|
|
s__('ClusterIntegration|Request to begin installing failed'),
|
|
);
|
|
});
|
|
})
|
|
.catch(error => this.store.updateAppProperty(appId, 'validationError', error));
|
|
}
|
|
|
|
static validateInstallation(appId, params) {
|
|
return new Promise((resolve, reject) => {
|
|
if (appId === CROSSPLANE && !params.stack) {
|
|
reject(s__('ClusterIntegration|Select a stack to install Crossplane.'));
|
|
return;
|
|
}
|
|
|
|
if (appId === KNATIVE && !params.hostname && !params.pages_domain_id) {
|
|
reject(s__('ClusterIntegration|You must specify a domain before you can install Knative.'));
|
|
return;
|
|
}
|
|
|
|
resolve();
|
|
});
|
|
}
|
|
|
|
uninstallApplication({ id: appId }) {
|
|
this.store.updateAppProperty(appId, 'requestReason', null);
|
|
this.store.updateAppProperty(appId, 'statusReason', null);
|
|
|
|
this.store.uninstallApplication(appId);
|
|
|
|
return this.service.uninstallApplication(appId).catch(() => {
|
|
this.store.notifyUninstallFailure(appId);
|
|
this.store.updateAppProperty(
|
|
appId,
|
|
'requestReason',
|
|
s__('ClusterIntegration|Request to begin uninstalling failed'),
|
|
);
|
|
});
|
|
}
|
|
|
|
updateApplication({ id: appId, params }) {
|
|
this.store.updateApplication(appId);
|
|
this.service.installApplication(appId, params).catch(() => {
|
|
this.store.notifyUpdateFailure(appId);
|
|
});
|
|
}
|
|
|
|
setFluentdSettings(settings = {}) {
|
|
Object.entries(settings).forEach(([key, value]) => {
|
|
this.store.updateAppProperty(FLUENTD, key, value);
|
|
});
|
|
}
|
|
|
|
saveKnativeDomain(data) {
|
|
const appId = data.id;
|
|
this.store.updateApplication(appId);
|
|
this.service.updateApplication(appId, data.params).catch(() => {
|
|
this.store.notifyUpdateFailure(appId);
|
|
});
|
|
}
|
|
|
|
setKnativeDomain({ id: appId, domain, domainId }) {
|
|
this.store.updateAppProperty(appId, 'isEditingDomain', true);
|
|
this.store.updateAppProperty(appId, 'hostname', domain);
|
|
this.store.updateAppProperty(appId, 'pagesDomain', domainId ? { id: domainId, domain } : null);
|
|
this.store.updateAppProperty(appId, 'validationError', null);
|
|
}
|
|
|
|
setCrossplaneProviderStack(data) {
|
|
const appId = data.id;
|
|
this.store.updateAppProperty(appId, 'stack', data.stack.code);
|
|
this.store.updateAppProperty(appId, 'validationError', null);
|
|
}
|
|
|
|
setIngressModSecurityEnabled({ id, modSecurityEnabled }) {
|
|
this.store.updateAppProperty(id, 'isEditingModSecurityEnabled', true);
|
|
this.store.updateAppProperty(id, 'modsecurity_enabled', modSecurityEnabled);
|
|
}
|
|
|
|
setIngressModSecurityMode({ id, modSecurityMode }) {
|
|
this.store.updateAppProperty(id, 'isEditingModSecurityMode', true);
|
|
this.store.updateAppProperty(id, 'modsecurity_mode', modSecurityMode);
|
|
}
|
|
|
|
resetIngressModSecurityChanges(id) {
|
|
this.store.updateAppProperty(id, 'isEditingModSecurityEnabled', false);
|
|
this.store.updateAppProperty(id, 'isEditingModSecurityMode', false);
|
|
}
|
|
|
|
destroy() {
|
|
this.destroyed = true;
|
|
|
|
this.removeListeners();
|
|
|
|
if (this.poll) {
|
|
this.poll.stop();
|
|
}
|
|
|
|
if (this.environments) {
|
|
this.environments.$destroy();
|
|
}
|
|
|
|
this.applications.$destroy();
|
|
}
|
|
}
|