Commit 2ca377d8 authored by Hordur Freyr Yngvason's avatar Hordur Freyr Yngvason

Remove cluster applications frontend code

The code being removed has been unused since the removal of the
applications tab in
https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63348

Part of https://gitlab.com/groups/gitlab-org/-/epics/4280
parent c044eeae
......@@ -3,16 +3,12 @@ import Visibility from 'visibilityjs';
import Vue from 'vue';
import AccessorUtilities from '~/lib/utils/accessor';
import initProjectSelectDropdown from '~/project_select';
import initServerlessSurveyBanner from '~/serverless/survey_banner';
import createFlash from '../flash';
import Poll from '../lib/utils/poll';
import { s__, sprintf } from '../locale';
import { s__ } from '../locale';
import PersistentUserCallout from '../persistent_user_callout';
import initSettingsPanels from '../settings_panels';
import Applications from './components/applications.vue';
import RemoveClusterConfirmation from './components/remove_cluster_confirmation.vue';
import { APPLICATION_STATUS, CROSSPLANE, KNATIVE } from './constants';
import eventHub from './event_hub';
import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store';
......@@ -20,46 +16,20 @@ const Environments = () => import('ee_component/clusters/components/environments
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,
managePrometheusPath,
clusterEnvironmentsPath,
hasRbac,
providerType,
preInstalledKnative,
clusterType,
clusterStatus,
clusterStatusReason,
helpPath,
helmHelpPath,
ingressHelpPath,
ingressDnsHelpPath,
environmentsHelpPath,
clustersHelpPath,
deployBoardsHelpPath,
cloudRunHelpPath,
clusterId,
ciliumHelpPath,
} = document.querySelector('.js-edit-cluster-form').dataset;
this.clusterId = clusterId;
......@@ -69,38 +39,19 @@ export default class Clusters {
this.store = new ClustersStore();
this.store.setHelpPaths({
helpPath,
helmHelpPath,
ingressHelpPath,
ingressDnsHelpPath,
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,
});
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');
......@@ -109,14 +60,12 @@ export default class Clusters {
'.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) {
......@@ -143,38 +92,6 @@ export default class Clusters {
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,
helmHelpPath: this.state.helmHelpPath,
ingressHelpPath: this.state.ingressHelpPath,
managePrometheusPath: this.state.managePrometheusPath,
ingressDnsHelpPath: this.state.ingressDnsHelpPath,
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');
......@@ -242,30 +159,11 @@ export default class Clusters {
}
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));
// 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);
// eslint-disable-next-line @gitlab/no-global-event-off
eventHub.$off('saveKnativeDomain');
// eslint-disable-next-line @gitlab/no-global-event-off
eventHub.$off('setKnativeDomain');
// eslint-disable-next-line @gitlab/no-global-event-off
eventHub.$off('setCrossplaneProviderStack');
// eslint-disable-next-line @gitlab/no-global-event-off
eventHub.$off('uninstallApplication');
}
initPolling(method, successCallback, errorCallback) {
this.poll = new Poll({
resource: this.service,
......@@ -305,16 +203,10 @@ export default class Clusters {
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() {
......@@ -325,31 +217,6 @@ export default class Clusters {
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(', '),
},
);
createFlash({
message: text,
type: 'notice',
parent: this.successApplicationContainer,
});
}
}
setBannerDismissedState(status, isDismissed) {
if (AccessorUtilities.isLocalStorageAccessSafe()) {
window.localStorage.setItem(this.clusterBannerDismissedKey, `${status}_${isDismissed}`);
......@@ -422,91 +289,9 @@ export default class Clusters {
}
}
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);
});
}
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);
}
destroy() {
this.destroyed = true;
this.removeListeners();
if (this.poll) {
this.poll.stop();
}
......@@ -514,7 +299,5 @@ export default class Clusters {
if (this.environments) {
this.environments.$destroy();
}
this.applications.$destroy();
}
}
<script>
import { GlLink, GlModalDirective, GlSprintf, GlButton, GlAlert } from '@gitlab/ui';
import { s__, __, sprintf } from '~/locale';
import identicon from '../../vue_shared/components/identicon.vue';
import { APPLICATION_STATUS, ELASTIC_STACK } from '../constants';
import eventHub from '../event_hub';
import UninstallApplicationButton from './uninstall_application_button.vue';
import UninstallApplicationConfirmationModal from './uninstall_application_confirmation_modal.vue';
import UpdateApplicationConfirmationModal from './update_application_confirmation_modal.vue';
export default {
components: {
GlButton,
identicon,
GlLink,
GlAlert,
GlSprintf,
UninstallApplicationButton,
UninstallApplicationConfirmationModal,
UpdateApplicationConfirmationModal,
},
directives: {
GlModalDirective,
},
props: {
id: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
titleLink: {
type: String,
required: false,
default: '',
},
manageLink: {
type: String,
required: false,
default: '',
},
logoUrl: {
type: String,
required: false,
default: '',
},
disabled: {
type: Boolean,
required: false,
default: false,
},
installable: {
type: Boolean,
required: false,
default: true,
},
uninstallable: {
type: Boolean,
required: false,
default: false,
},
status: {
type: String,
required: false,
default: '',
},
statusReason: {
type: String,
required: false,
default: '',
},
requestReason: {
type: String,
required: false,
default: '',
},
installed: {
type: Boolean,
required: false,
default: false,
},
installFailed: {
type: Boolean,
required: false,
default: false,
},
version: {
type: String,
required: false,
default: '',
},
chartRepo: {
type: String,
required: false,
default: '',
},
updateAvailable: {
type: Boolean,
required: false,
},
updateable: {
type: Boolean,
default: true,
required: false,
},
updateSuccessful: {
type: Boolean,
required: false,
default: false,
},
updateFailed: {
type: Boolean,
required: false,
default: false,
},
uninstallFailed: {
type: Boolean,
required: false,
default: false,
},
uninstallSuccessful: {
type: Boolean,
required: false,
default: false,
},
installApplicationRequestParams: {
type: Object,
required: false,
default: () => ({}),
},
},
computed: {
isUnknownStatus() {
return !this.isKnownStatus && this.status !== null;
},
isKnownStatus() {
return Object.values(APPLICATION_STATUS).includes(this.status);
},
isInstalling() {
return this.status === APPLICATION_STATUS.INSTALLING;
},
isExternallyInstalled() {
return this.status === APPLICATION_STATUS.EXTERNALLY_INSTALLED;
},
canInstall() {
return (
this.status === APPLICATION_STATUS.NOT_INSTALLABLE ||
this.status === APPLICATION_STATUS.INSTALLABLE ||
this.status === APPLICATION_STATUS.UNINSTALLED ||
this.isUnknownStatus
);
},
hasLogo() {
return Boolean(this.logoUrl);
},
identiconId() {
// generate a deterministic integer id for the identicon background
return this.id.charCodeAt(0);
},
rowJsClass() {
return `js-cluster-application-row-${this.id}`;
},
displayUninstallButton() {
return this.installed && this.uninstallable;
},
displayInstallButton() {
return !this.installed || !this.uninstallable;
},
installButtonLoading() {
return !this.status || this.isInstalling;
},
installButtonDisabled() {
// Applications installed through the management project can
// only be installed through the CI pipeline. Installation should
// be disable in all states.
if (!this.installable) return true;
// Avoid the potential for the real-time data to say APPLICATION_STATUS.INSTALLABLE but
// we already made a request to install and are just waiting for the real-time
// to sync up.
if (this.isInstalling) return true;
if (!this.isKnownStatus) return false;
return (
this.status !== APPLICATION_STATUS.INSTALLABLE && this.status !== APPLICATION_STATUS.ERROR
);
},
installButtonLabel() {
let label;
if (this.canInstall) {
label = __('Install');
} else if (this.isInstalling) {
label = __('Installing');
} else if (this.installed) {
label = __('Installed');
} else if (this.isExternallyInstalled) {
label = __('Externally installed');
}
return label;
},
buttonGridCellClass() {
return this.showManageButton || this.status === APPLICATION_STATUS.EXTERNALLY_INSTALLED
? 'section-25'
: 'section-15';
},
showManageButton() {
return this.manageLink && this.status === APPLICATION_STATUS.INSTALLED;
},
manageButtonLabel() {
return __('Manage');
},
hasError() {
return this.installFailed || this.uninstallFailed;
},
generalErrorDescription() {
let errorDescription;
if (this.installFailed) {
errorDescription = s__('ClusterIntegration|Something went wrong while installing %{title}');
} else if (this.uninstallFailed) {
errorDescription = s__(
'ClusterIntegration|Something went wrong while uninstalling %{title}',
);
}
return sprintf(errorDescription, { title: this.title });
},
updateFailureDescription() {
return s__('ClusterIntegration|Update failed. Please check the logs and try again.');
},
updateSuccessDescription() {
return sprintf(s__('ClusterIntegration|%{title} updated successfully.'), {
title: this.title,
});
},
updateButtonLabel() {
let label;
if (this.updateAvailable && !this.updateFailed && !this.isUpdating) {
label = __('Update');
} else if (this.isUpdating) {
label = __('Updating');
} else if (this.updateFailed) {
label = __('Retry update');
}
return label;
},
updatingNeedsConfirmation() {
if (this.version) {
const majorVersion = parseInt(this.version.split('.')[0], 10);
if (!Number.isNaN(majorVersion)) {
return this.id === ELASTIC_STACK && majorVersion < 3;
}
}
return false;
},
isUpdating() {
// Since upgrading is handled asynchronously on the backend we need this check to prevent any delay on the frontend
return this.status === APPLICATION_STATUS.UPDATING;
},
shouldShowUpdateDetails() {
// This method only returns true when;
// Update was successful OR Update failed
// AND new update is unavailable AND version information is present.
return (this.updateSuccessful || this.updateFailed) && !this.updateAvailable && this.version;
},
uninstallSuccessDescription() {
return sprintf(s__('ClusterIntegration|%{title} uninstalled successfully.'), {
title: this.title,
});
},
updateModalId() {
return `update-${this.id}`;
},
uninstallModalId() {
return `uninstall-${this.id}`;
},
},
watch: {
updateSuccessful(updateSuccessful) {
if (updateSuccessful) {
this.$toast.show(this.updateSuccessDescription);
}
},
uninstallSuccessful(uninstallSuccessful) {
if (uninstallSuccessful) {
this.$toast.show(this.uninstallSuccessDescription);
}
},
},
methods: {
installClicked() {
if (this.disabled || this.installButtonDisabled) return;
eventHub.$emit('installApplication', {
id: this.id,
params: this.installApplicationRequestParams,
});
},
updateConfirmed() {
if (this.isUpdating) return;
eventHub.$emit('updateApplication', {
id: this.id,
params: this.installApplicationRequestParams,
});
},
uninstallConfirmed() {
eventHub.$emit('uninstallApplication', {
id: this.id,
});
},
},
};
</script>
<template>
<div
:class="[
rowJsClass,
installed && 'cluster-application-installed',
disabled && 'cluster-application-disabled',
]"
class="cluster-application-row gl-responsive-table-row gl-responsive-table-row-col-span"
:data-qa-selector="id"
>
<div class="gl-responsive-table-row-layout" role="row">
<div class="table-section gl-mr-3 section-align-top" role="gridcell">
<img
v-if="hasLogo"
:src="logoUrl"
:alt="`${title} logo`"
class="cluster-application-logo avatar s40"
/>
<identicon v-else :entity-id="identiconId" :entity-name="title" size-class="s40" />
</div>
<div class="table-section cluster-application-description section-wrap" role="gridcell">
<strong>
<a
v-if="titleLink"
:href="titleLink"
target="_blank"
rel="noopener noreferrer"
class="js-cluster-application-title"
>{{ title }}</a
>
<span v-else class="js-cluster-application-title">{{ title }}</span>
</strong>
<slot name="installed-via"></slot>
<div>
<slot name="description"></slot>
</div>
<div v-if="hasError" class="cluster-application-error text-danger gl-mt-3">
<p class="js-cluster-application-general-error-message gl-mb-0">
{{ generalErrorDescription }}
</p>
<ul v-if="statusReason || requestReason">
<li v-if="statusReason" class="js-cluster-application-status-error-message">
{{ statusReason }}
</li>
<li v-if="requestReason" class="js-cluster-application-request-error-message">
{{ requestReason }}
</li>
</ul>
</div>
<div v-if="updateable">
<div
v-if="shouldShowUpdateDetails"
class="form-text text-muted label p-0 js-cluster-application-update-details"
>
<template v-if="updateFailed">{{ __('Update failed') }}</template>
<template v-else-if="isUpdating">{{ __('Updating') }}</template>
<template v-else>
<gl-sprintf :message="__('Updated to %{linkStart}chart v%{linkEnd}')">
<template #link="{ content }">
<gl-link
:href="chartRepo"
target="_blank"
class="js-cluster-application-update-version"
>{{ content }}{{ version }}</gl-link
>
</template>
</gl-sprintf>
</template>
</div>
<gl-alert
v-if="updateFailed && !isUpdating"
variant="danger"
:dismissible="false"
class="gl-mt-3 gl-mb-0 js-cluster-application-update-details"
>
{{ updateFailureDescription }}
</gl-alert>
<template v-if="updateAvailable || updateFailed || isUpdating">
<template v-if="updatingNeedsConfirmation">
<gl-button
v-gl-modal-directive="updateModalId"
class="js-cluster-application-update-button mt-2"
variant="info"
category="primary"
:loading="isUpdating"
:disabled="isUpdating"
data-qa-selector="update_button_with_confirmation"
:data-qa-application="id"
>
{{ updateButtonLabel }}
</gl-button>
<update-application-confirmation-modal
:application="id"
:application-title="title"
@confirm="updateConfirmed()"
/>
</template>
<gl-button
v-else
class="js-cluster-application-update-button mt-2"
variant="info"
category="primary"
:loading="isUpdating"
:disabled="isUpdating"
data-qa-selector="update_button"
:data-qa-application="id"
@click="updateConfirmed"
>
{{ updateButtonLabel }}
</gl-button>
</template>
</div>
</div>
<div
:class="[buttonGridCellClass, 'table-section', 'table-button-footer', 'section-align-top']"
role="gridcell"
>
<div v-if="showManageButton" class="btn-group table-action-buttons">
<a :href="manageLink" :class="{ disabled: disabled }" class="btn">{{
manageButtonLabel
}}</a>
</div>
<div class="btn-group table-action-buttons">
<gl-button
v-if="displayInstallButton"
:loading="installButtonLoading"
:disabled="disabled || installButtonDisabled"
class="js-cluster-application-install-button"
variant="default"
data-qa-selector="install_button"
:data-qa-application="id"
@click="installClicked"
>
{{ installButtonLabel }}
</gl-button>
<uninstall-application-button
v-if="displayUninstallButton"
v-gl-modal-directive="uninstallModalId"
:status="status"
data-qa-selector="uninstall_button"
:data-qa-application="id"
class="js-cluster-application-uninstall-button"
/>
<uninstall-application-confirmation-modal
:application="id"
:application-title="title"
@confirm="uninstallConfirmed()"
/>
</div>
</div>
</div>
</div>
</template>
<script>
import { GlLoadingIcon, GlSprintf, GlLink, GlAlert } from '@gitlab/ui';
import certManagerLogo from 'images/cluster_app_logos/cert_manager.png';
import crossplaneLogo from 'images/cluster_app_logos/crossplane.png';
import elasticStackLogo from 'images/cluster_app_logos/elastic_stack.png';
import gitlabLogo from 'images/cluster_app_logos/gitlab.png';
import helmLogo from 'images/cluster_app_logos/helm.png';
import jupyterhubLogo from 'images/cluster_app_logos/jupyterhub.png';
import knativeLogo from 'images/cluster_app_logos/knative.png';
import kubernetesLogo from 'images/cluster_app_logos/kubernetes.png';
import prometheusLogo from 'images/cluster_app_logos/prometheus.png';
import eventHub from '~/clusters/event_hub';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
import { CLUSTER_TYPE, PROVIDER_TYPE, APPLICATION_STATUS, INGRESS } from '../constants';
import applicationRow from './application_row.vue';
import CrossplaneProviderStack from './crossplane_provider_stack.vue';
import KnativeDomainEditor from './knative_domain_editor.vue';
export default {
components: {
applicationRow,
clipboardButton,
GlLoadingIcon,
GlSprintf,
GlLink,
KnativeDomainEditor,
CrossplaneProviderStack,
GlAlert,
},
props: {
type: {
type: String,
required: false,
default: CLUSTER_TYPE.PROJECT,
},
applications: {
type: Object,
required: false,
default: () => ({}),
},
helpPath: {
type: String,
required: false,
default: '',
},
helmHelpPath: {
type: String,
required: false,
default: '',
},
ingressHelpPath: {
type: String,
required: false,
default: '',
},
ingressDnsHelpPath: {
type: String,
required: false,
default: '',
},
cloudRunHelpPath: {
type: String,
required: false,
default: '',
},
managePrometheusPath: {
type: String,
required: false,
default: '',
},
providerType: {
type: String,
required: false,
default: '',
},
preInstalledKnative: {
type: Boolean,
required: false,
default: false,
},
rbac: {
type: Boolean,
required: false,
default: false,
},
ciliumHelpPath: {
type: String,
required: false,
default: '',
},
},
computed: {
ingressId() {
return INGRESS;
},
ingressInstalled() {
return this.applications.ingress.status === APPLICATION_STATUS.INSTALLED;
},
ingressExternalEndpoint() {
return this.applications.ingress.externalIp || this.applications.ingress.externalHostname;
},
certManagerInstalled() {
return this.applications.cert_manager.status === APPLICATION_STATUS.INSTALLED;
},
jupyterInstalled() {
return this.applications.jupyter.status === APPLICATION_STATUS.INSTALLED;
},
jupyterHostname() {
return this.applications.jupyter.hostname;
},
knative() {
return this.applications.knative;
},
crossplane() {
return this.applications.crossplane;
},
cloudRun() {
return this.providerType === PROVIDER_TYPE.GCP && this.preInstalledKnative;
},
ingress() {
return this.applications.ingress;
},
},
methods: {
saveKnativeDomain() {
eventHub.$emit('saveKnativeDomain', {
id: 'knative',
params: {
hostname: this.applications.knative.hostname,
pages_domain_id: this.applications.knative.pagesDomain?.id,
},
});
},
setKnativeDomain({ domainId, domain }) {
eventHub.$emit('setKnativeDomain', {
id: 'knative',
domainId,
domain,
});
},
setCrossplaneProviderStack(stack) {
eventHub.$emit('setCrossplaneProviderStack', {
id: 'crossplane',
stack,
});
},
},
logos: {
gitlabLogo,
helmLogo,
jupyterhubLogo,
kubernetesLogo,
certManagerLogo,
crossplaneLogo,
knativeLogo,
prometheusLogo,
elasticStackLogo,
},
};
</script>
<template>
<section id="cluster-applications">
<p class="gl-mb-0">
{{
s__(`ClusterIntegration|Choose which applications to install on your Kubernetes cluster.`)
}}
<gl-link :href="helpPath">{{ __('More information') }}</gl-link>
</p>
<div class="cluster-application-list gl-mt-3">
<application-row
v-if="applications.helm.installed || applications.helm.uninstalling"
id="helm"
:logo-url="$options.logos.helmLogo"
:title="applications.helm.title"
:status="applications.helm.status"
:status-reason="applications.helm.statusReason"
:request-status="applications.helm.requestStatus"
:request-reason="applications.helm.requestReason"
:installed="applications.helm.installed"
:install-failed="applications.helm.installFailed"
:uninstallable="applications.helm.uninstallable"
:uninstall-successful="applications.helm.uninstallSuccessful"
:uninstall-failed="applications.helm.uninstallFailed"
title-link="https://v2.helm.sh/"
>
<template #description>
<p>
{{
s__(`ClusterIntegration|Can be safely removed. Prior to GitLab
13.2, GitLab used a remote Tiller server to manage the
applications. GitLab no longer uses this server.
Uninstalling this server will not affect your other
applications. This row will disappear afterwards.`)
}}
<gl-link :href="helmHelpPath">{{ __('More information') }}</gl-link>
</p>
</template>
</application-row>
<application-row
:id="ingressId"
:logo-url="$options.logos.kubernetesLogo"
:title="applications.ingress.title"
:status="applications.ingress.status"
:status-reason="applications.ingress.statusReason"
:request-status="applications.ingress.requestStatus"
:request-reason="applications.ingress.requestReason"
:installed="applications.ingress.installed"
:install-failed="applications.ingress.installFailed"
:uninstallable="applications.ingress.uninstallable"
:uninstall-successful="applications.ingress.uninstallSuccessful"
:uninstall-failed="applications.ingress.uninstallFailed"
:updateable="false"
title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/"
>
<template #description>
<p>
{{
s__(`ClusterIntegration|Ingress gives you a way to route
requests to services based on the request host or path,
centralizing a number of services into a single entrypoint.`)
}}
</p>
<template v-if="ingressInstalled">
<div class="form-group">
<label for="ingress-endpoint">{{ s__('ClusterIntegration|Ingress Endpoint') }}</label>
<div class="input-group">
<template v-if="ingressExternalEndpoint">
<input
id="ingress-endpoint"
:value="ingressExternalEndpoint"
type="text"
class="form-control js-endpoint"
readonly
/>
<span class="input-group-append">
<clipboard-button
:text="ingressExternalEndpoint"
:title="s__('ClusterIntegration|Copy Ingress Endpoint')"
class="input-group-text js-clipboard-btn"
/>
</span>
</template>
<template v-else>
<input type="text" class="form-control js-endpoint" readonly />
<gl-loading-icon
class="position-absolute align-self-center ml-2 js-ingress-ip-loading-icon"
/>
</template>
</div>
<p class="form-text text-muted">
{{
s__(`ClusterIntegration|Point a wildcard DNS to this
generated endpoint in order to access
your application after it has been deployed.`)
}}
<gl-link :href="ingressDnsHelpPath" target="_blank">
{{ __('More information') }}
</gl-link>
</p>
</div>
<p v-if="!ingressExternalEndpoint" class="settings-message js-no-endpoint-message">
{{
s__(`ClusterIntegration|The endpoint is in
the process of being assigned. Please check your Kubernetes
cluster or Quotas on Google Kubernetes Engine if it takes a long time.`)
}}
<gl-link :href="ingressDnsHelpPath" target="_blank">
{{ __('More information') }}
</gl-link>
</p>
</template>
<template v-else>
<gl-alert variant="info" :dismissible="false">
<span data-testid="ingressCostWarning">
<gl-sprintf
:message="
s__(
'ClusterIntegration|Installing Ingress may incur additional costs. Learn more about %{linkStart}pricing%{linkEnd}.',
)
"
>
<template #link="{ content }">
<gl-link href="https://cloud.google.com/compute/pricing#lb" target="_blank">{{
content
}}</gl-link>
</template>
</gl-sprintf>
</span>
</gl-alert>
</template>
</template>
</application-row>
<application-row
id="cert_manager"
:logo-url="$options.logos.certManagerLogo"
:title="applications.cert_manager.title"
:status="applications.cert_manager.status"
:status-reason="applications.cert_manager.statusReason"
:request-status="applications.cert_manager.requestStatus"
:request-reason="applications.cert_manager.requestReason"
:installed="applications.cert_manager.installed"
:install-failed="applications.cert_manager.installFailed"
:install-application-request-params="{ email: applications.cert_manager.email }"
:uninstallable="applications.cert_manager.uninstallable"
:uninstall-successful="applications.cert_manager.uninstallSuccessful"
:uninstall-failed="applications.cert_manager.uninstallFailed"
title-link="https://cert-manager.readthedocs.io/en/latest/#"
>
<template #description>
<p data-testid="certManagerDescription">
<gl-sprintf
:message="
s__(`ClusterIntegration|Cert-Manager is a native Kubernetes certificate management controller that helps with issuing certificates.
Installing Cert-Manager on your cluster will issue a certificate by %{linkStart}Let's Encrypt%{linkEnd} and ensure that certificates
are valid and up-to-date.`)
"
>
<template #link="{ content }">
<gl-link href="https://letsencrypt.org/" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
<div class="form-group">
<label for="cert-manager-issuer-email">
{{ s__('ClusterIntegration|Issuer Email') }}
</label>
<div class="input-group">
<!-- eslint-disable vue/no-mutating-props -->
<input
id="cert-manager-issuer-email"
v-model="applications.cert_manager.email"
:readonly="certManagerInstalled"
type="text"
class="form-control js-email"
/>
<!-- eslint-enable vue/no-mutating-props -->
</div>
<p class="form-text text-muted">
{{
s__(`ClusterIntegration|Issuers represent a certificate authority.
You must provide an email address for your Issuer.`)
}}
<gl-link
href="http://docs.cert-manager.io/en/latest/reference/issuers.html?highlight=email"
target="_blank"
>{{ __('More information') }}</gl-link
>
</p>
</div>
</template>
</application-row>
<application-row
id="prometheus"
:logo-url="$options.logos.prometheusLogo"
:title="applications.prometheus.title"
:manage-link="managePrometheusPath"
:status="applications.prometheus.status"
:status-reason="applications.prometheus.statusReason"
:request-status="applications.prometheus.requestStatus"
:request-reason="applications.prometheus.requestReason"
:installed="applications.prometheus.installed"
:install-failed="applications.prometheus.installFailed"
:uninstallable="applications.prometheus.uninstallable"
:uninstall-successful="applications.prometheus.uninstallSuccessful"
:uninstall-failed="applications.prometheus.uninstallFailed"
title-link="https://prometheus.io/docs/introduction/overview/"
>
<template #description>
<span data-testid="prometheusDescription">
<gl-sprintf
:message="
s__(`ClusterIntegration|Prometheus is an open-source monitoring system
with %{linkStart}GitLab Integration%{linkEnd} to monitor deployed applications.`)
"
>
<template #link="{ content }">
<gl-link
href="https://docs.gitlab.com/ee/user/project/integrations/prometheus.html"
target="_blank"
>{{ content }}</gl-link
>
</template>
</gl-sprintf>
</span>
</template>
</application-row>
<application-row
id="runner"
:logo-url="$options.logos.gitlabLogo"
:title="applications.runner.title"
:status="applications.runner.status"
:status-reason="applications.runner.statusReason"
:request-status="applications.runner.requestStatus"
:request-reason="applications.runner.requestReason"
:version="applications.runner.version"
:chart-repo="applications.runner.chartRepo"
:update-available="applications.runner.updateAvailable"
:installed="applications.runner.installed"
:install-failed="applications.runner.installFailed"
:update-successful="applications.runner.updateSuccessful"
:update-failed="applications.runner.updateFailed"
:uninstallable="applications.runner.uninstallable"
:uninstall-successful="applications.runner.uninstallSuccessful"
:uninstall-failed="applications.runner.uninstallFailed"
title-link="https://docs.gitlab.com/runner/"
>
<template #description>
{{
s__(`ClusterIntegration|GitLab Runner connects to the
repository and executes CI/CD jobs,
pushing results back and deploying
applications to production.`)
}}
</template>
</application-row>
<application-row
id="crossplane"
:logo-url="$options.logos.crossplaneLogo"
:title="applications.crossplane.title"
:status="applications.crossplane.status"
:status-reason="applications.crossplane.statusReason"
:request-status="applications.crossplane.requestStatus"
:request-reason="applications.crossplane.requestReason"
:installed="applications.crossplane.installed"
:install-failed="applications.crossplane.installFailed"
:uninstallable="applications.crossplane.uninstallable"
:uninstall-successful="applications.crossplane.uninstallSuccessful"
:uninstall-failed="applications.crossplane.uninstallFailed"
:install-application-request-params="{ stack: applications.crossplane.stack }"
title-link="https://crossplane.io"
>
<template #description>
<p data-testid="crossplaneDescription">
<gl-sprintf
:message="
s__(
`ClusterIntegration|Crossplane enables declarative provisioning of managed services from your cloud of choice using %{codeStart}kubectl%{codeEnd} or %{linkStart}GitLab Integration%{linkEnd}.
Crossplane runs inside your Kubernetes cluster and supports secure connectivity and secrets management between app containers and the cloud services they depend on.`,
)
"
>
<template #code="{ content }">
<code>{{ content }}</code>
</template>
<template #link="{ content }">
<gl-link
href="https://docs.gitlab.com/ee/user/clusters/applications.html#crossplane"
target="_blank"
>{{ content }}</gl-link
>
</template>
</gl-sprintf>
</p>
<div class="form-group">
<CrossplaneProviderStack :crossplane="crossplane" @set="setCrossplaneProviderStack" />
</div>
</template>
</application-row>
<application-row
id="jupyter"
:logo-url="$options.logos.jupyterhubLogo"
:title="applications.jupyter.title"
:status="applications.jupyter.status"
:status-reason="applications.jupyter.statusReason"
:request-status="applications.jupyter.requestStatus"
:request-reason="applications.jupyter.requestReason"
:installed="applications.jupyter.installed"
:install-failed="applications.jupyter.installFailed"
:uninstallable="applications.jupyter.uninstallable"
:uninstall-successful="applications.jupyter.uninstallSuccessful"
:uninstall-failed="applications.jupyter.uninstallFailed"
:install-application-request-params="{ hostname: applications.jupyter.hostname }"
title-link="https://jupyterhub.readthedocs.io/en/stable/"
>
<template #description>
<p>
{{
s__(`ClusterIntegration|JupyterHub, a multi-user Hub, spawns,
manages, and proxies multiple instances of the single-user
Jupyter notebook server. JupyterHub can be used to serve
notebooks to a class of students, a corporate data science group,
or a scientific research group.`)
}}
<gl-sprintf
:message="
s__(
'ClusterIntegration|%{boldStart}Note:%{boldEnd} Requires Ingress to be installed.',
)
"
>
<template #bold="{ content }">
<b>{{ content }}</b>
</template>
</gl-sprintf>
</p>
<template v-if="ingressExternalEndpoint">
<div class="form-group">
<label for="jupyter-hostname">{{ s__('ClusterIntegration|Jupyter Hostname') }}</label>
<div class="input-group">
<!-- eslint-disable vue/no-mutating-props -->
<input
id="jupyter-hostname"
v-model="applications.jupyter.hostname"
:readonly="jupyterInstalled"
type="text"
class="form-control js-hostname"
/>
<!-- eslint-enable vue/no-mutating-props -->
<span class="input-group-append">
<clipboard-button
:text="jupyterHostname"
:title="s__('ClusterIntegration|Copy Jupyter Hostname')"
class="js-clipboard-btn"
/>
</span>
</div>
<p v-if="ingressInstalled" class="form-text text-muted">
{{
s__(`ClusterIntegration|Replace this with your own hostname if you want.
If you do so, point hostname to Ingress IP Address from above.`)
}}
<gl-link :href="ingressDnsHelpPath" target="_blank">
{{ __('More information') }}
</gl-link>
</p>
</div>
</template>
</template>
</application-row>
<application-row
id="knative"
:logo-url="$options.logos.knativeLogo"
:title="applications.knative.title"
:status="applications.knative.status"
:status-reason="applications.knative.statusReason"
:request-status="applications.knative.requestStatus"
:request-reason="applications.knative.requestReason"
:installed="applications.knative.installed"
:install-failed="applications.knative.installFailed"
:install-application-request-params="{
hostname: applications.knative.hostname,
pages_domain_id: applications.knative.pagesDomain && applications.knative.pagesDomain.id,
}"
:uninstallable="applications.knative.uninstallable"
:uninstall-successful="applications.knative.uninstallSuccessful"
:uninstall-failed="applications.knative.uninstallFailed"
:updateable="false"
v-bind="applications.knative"
title-link="https://github.com/knative/docs"
>
<template #description>
<gl-alert v-if="!rbac" variant="info" class="rbac-notice gl-my-3" :dismissible="false">
{{
s__(`ClusterIntegration|You must have an RBAC-enabled cluster
to install Knative.`)
}}
<gl-link :href="helpPath" target="_blank">{{ __('More information') }}</gl-link>
</gl-alert>
<p>
{{
s__(`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.`)
}}
</p>
<knative-domain-editor
v-if="(knative.installed || rbac) && !preInstalledKnative"
:knative="knative"
:ingress-dns-help-path="ingressDnsHelpPath"
@save="saveKnativeDomain"
@set="setKnativeDomain"
/>
</template>
<template v-if="cloudRun" #installed-via>
<span data-testid="installed-via">
<gl-sprintf
:message="s__('ClusterIntegration|installed via %{linkStart}Cloud Run%{linkEnd}')"
>
<template #link="{ content }">
<gl-link :href="cloudRunHelpPath" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</span>
</template>
</application-row>
<application-row
id="elastic_stack"
:logo-url="$options.logos.elasticStackLogo"
:title="applications.elastic_stack.title"
:status="applications.elastic_stack.status"
:status-reason="applications.elastic_stack.statusReason"
:request-status="applications.elastic_stack.requestStatus"
:request-reason="applications.elastic_stack.requestReason"
:version="applications.elastic_stack.version"
:chart-repo="applications.elastic_stack.chartRepo"
:update-available="applications.elastic_stack.updateAvailable"
:installed="applications.elastic_stack.installed"
:install-failed="applications.elastic_stack.installFailed"
:update-successful="applications.elastic_stack.updateSuccessful"
:update-failed="applications.elastic_stack.updateFailed"
:uninstallable="applications.elastic_stack.uninstallable"
:uninstall-successful="applications.elastic_stack.uninstallSuccessful"
:uninstall-failed="applications.elastic_stack.uninstallFailed"
title-link="https://gitlab.com/gitlab-org/charts/elastic-stack"
>
<template #description>
<p>
{{
s__(
`ClusterIntegration|The elastic stack collects logs from all pods in your cluster`,
)
}}
</p>
</template>
</application-row>
<div class="gl-mt-7 gl-border-1 gl-border-t-solid gl-border-gray-100">
<!-- This empty div serves as a separator. The applications below can be externally installed using a cluster-management project. -->
</div>
<application-row
id="cilium"
:title="applications.cilium.title"
:logo-url="$options.logos.gitlabLogo"
:status="applications.cilium.status"
:status-reason="applications.cilium.statusReason"
:installable="applications.cilium.installable"
:uninstallable="applications.cilium.uninstallable"
:installed="applications.cilium.installed"
:install-failed="applications.cilium.installFailed"
:title-link="ciliumHelpPath"
>
<template #description>
<p data-testid="ciliumDescription">
<gl-sprintf
:message="
s__(
'ClusterIntegration|Protect your clusters with GitLab Container Network Policies by enforcing how pods communicate with each other and other network endpoints. %{linkStart}Learn more about configuring Network Policies here.%{linkEnd}',
)
"
>
<template #link="{ content }">
<gl-link :href="ciliumHelpPath" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
</template>
</application-row>
</div>
</section>
</template>
<script>
import { GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui';
import { s__ } from '../../locale';
export default {
name: 'CrossplaneProviderStack',
components: {
GlDropdown,
GlDropdownItem,
GlIcon,
},
props: {
stacks: {
type: Array,
required: false,
default: () => [
{
name: s__('Google Cloud Platform'),
code: 'gcp',
},
{
name: s__('Amazon Web Services'),
code: 'aws',
},
{
name: s__('Microsoft Azure'),
code: 'azure',
},
{
name: s__('Rook'),
code: 'rook',
},
],
},
crossplane: {
type: Object,
required: true,
},
},
computed: {
dropdownText() {
const result = this.stacks.reduce((map, obj) => {
// eslint-disable-next-line no-param-reassign
map[obj.code] = obj.name;
return map;
}, {});
const { stack } = this.crossplane;
if (stack !== '') {
return result[stack];
}
return s__('Select Stack');
},
validationError() {
return this.crossplane.validationError;
},
},
methods: {
selectStack(stack) {
this.$emit('set', stack);
},
},
};
</script>
<template>
<div>
<label>
{{ s__('ClusterIntegration|Enabled stack') }}
</label>
<gl-dropdown
:disabled="crossplane.installed"
:text="dropdownText"
toggle-class="dropdown-menu-toggle gl-field-error-outline"
class="w-100"
:class="{ 'gl-show-field-errors': validationError }"
>
<gl-dropdown-item v-for="stack in stacks" :key="stack.code" @click="selectStack(stack)">
<span class="ml-1">{{ stack.name }}</span>
</gl-dropdown-item>
</gl-dropdown>
<span v-if="validationError" class="gl-field-error">{{ validationError }}</span>
<p class="form-text text-muted">
{{ s__(`You must select a stack for configuring your cloud provider. Learn more about`) }}
<a
href="https://crossplane.io/docs/master/stacks-guide.html"
target="_blank"
rel="noopener noreferrer"
>{{ __('Crossplane') }}
<gl-icon name="external-link" class="vertical-align-middle" />
</a>
</p>
</div>
</template>
<script>
import {
GlDropdown,
GlDropdownDivider,
GlDropdownItem,
GlLoadingIcon,
GlSearchBoxByType,
GlSprintf,
GlButton,
GlAlert,
} from '@gitlab/ui';
import { APPLICATION_STATUS } from '~/clusters/constants';
import { __, s__ } from '~/locale';
import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
const { UPDATING, UNINSTALLING } = APPLICATION_STATUS;
export default {
components: {
GlButton,
ClipboardButton,
GlLoadingIcon,
GlDropdown,
GlDropdownDivider,
GlDropdownItem,
GlSearchBoxByType,
GlSprintf,
GlAlert,
},
props: {
knative: {
type: Object,
required: true,
},
ingressDnsHelpPath: {
type: String,
default: '',
required: false,
},
},
data() {
return {
searchQuery: '',
};
},
computed: {
saveButtonDisabled() {
return [UNINSTALLING, UPDATING].includes(this.knative.status);
},
saving() {
return [UPDATING].includes(this.knative.status);
},
saveButtonLabel() {
return this.saving ? __('Saving') : __('Save changes');
},
knativeInstalled() {
return this.knative.installed;
},
knativeExternalEndpoint() {
return this.knative.externalIp || this.knative.externalHostname;
},
knativeUpdateSuccessful() {
return this.knative.updateSuccessful;
},
knativeHostname: {
get() {
return this.knative.hostname;
},
set(hostname) {
this.selectCustomDomain(hostname);
},
},
domainDropdownText() {
return this.knativeHostname || s__('ClusterIntegration|Select existing domain or use new');
},
availableDomains() {
return this.knative.availableDomains || [];
},
filteredDomains() {
const query = this.searchQuery.toLowerCase();
return this.availableDomains.filter(({ domain }) => domain.toLowerCase().includes(query));
},
showDomainsDropdown() {
return this.availableDomains.length > 0;
},
validationError() {
return this.knative.validationError;
},
},
watch: {
knativeUpdateSuccessful(updateSuccessful) {
if (updateSuccessful) {
this.$toast.show(s__('ClusterIntegration|Knative domain name was updated successfully.'));
}
},
},
methods: {
selectDomain({ id, domain }) {
this.$emit('set', { domain, domainId: id });
},
selectCustomDomain(domain) {
this.$emit('set', { domain, domainId: null });
},
},
};
</script>
<template>
<div class="row">
<gl-alert
v-if="knative.updateFailed"
class="gl-mb-5 col-12 js-cluster-knative-domain-name-failure-message"
variant="danger"
>
{{ s__('ClusterIntegration|Something went wrong while updating Knative domain name.') }}
</gl-alert>
<div
:class="{ 'col-md-6': knativeInstalled, 'col-12': !knativeInstalled }"
class="form-group col-sm-12 mb-0"
>
<label for="knative-domainname">
<strong>{{ s__('ClusterIntegration|Knative Domain Name:') }}</strong>
</label>
<gl-dropdown
v-if="showDomainsDropdown"
:text="domainDropdownText"
toggle-class="dropdown-menu-toggle"
class="w-100 mb-2"
>
<gl-search-box-by-type
v-model.trim="searchQuery"
:placeholder="s__('ClusterIntegration|Search domains')"
/>
<gl-dropdown-item
v-for="domain in filteredDomains"
:key="domain.id"
@click="selectDomain(domain)"
>
<span class="ml-1">{{ domain.domain }}</span>
</gl-dropdown-item>
<template v-if="searchQuery">
<gl-dropdown-divider />
<gl-dropdown-item key="custom-domain" @click="selectCustomDomain(searchQuery)">
<span class="ml-1">
<gl-sprintf :message="s__('ClusterIntegration|Use %{query}')">
<template #query>
<code>{{ searchQuery }}</code>
</template>
</gl-sprintf>
</span>
</gl-dropdown-item>
</template>
</gl-dropdown>
<input
v-else
id="knative-domainname"
v-model="knativeHostname"
type="text"
class="form-control js-knative-domainname"
/>
<span v-if="validationError" class="gl-field-error">{{ validationError }}</span>
</div>
<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-endpoint">
<strong>{{ s__('ClusterIntegration|Knative Endpoint:') }}</strong>
</label>
<div v-if="knativeExternalEndpoint" class="input-group">
<input
id="knative-endpoint"
:value="knativeExternalEndpoint"
type="text"
class="form-control js-knative-endpoint"
readonly
/>
<span class="input-group-append">
<clipboard-button
:text="knativeExternalEndpoint"
:title="s__('ClusterIntegration|Copy Knative Endpoint')"
class="input-group-text js-knative-endpoint-clipboard-btn"
/>
</span>
</div>
<div v-else class="input-group">
<input type="text" class="form-control js-endpoint" readonly />
<gl-loading-icon
class="position-absolute align-self-center ml-2 js-knative-ip-loading-icon"
/>
</div>
</div>
<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
v-if="!knativeExternalEndpoint"
class="settings-message js-no-knative-endpoint-message mt-2 mr-3 mb-0 ml-3"
>
{{
s__(`ClusterIntegration|The endpoint 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>
<gl-button
class="js-knative-save-domain-button gl-mt-5 gl-ml-5"
variant="success"
category="primary"
:loading="saving"
:disabled="saveButtonDisabled"
@click="$emit('save')"
>
{{ saveButtonLabel }}
</gl-button>
</template>
</div>
</template>
<script>
import { GlButton } from '@gitlab/ui';
import { APPLICATION_STATUS } from '~/clusters/constants';
import { __ } from '~/locale';
const { UPDATING, UNINSTALLING } = APPLICATION_STATUS;
export default {
components: {
GlButton,
},
props: {
status: {
type: String,
required: true,
},
},
computed: {
disabled() {
return [UNINSTALLING, UPDATING].includes(this.status);
},
loading() {
return this.status === UNINSTALLING;
},
label() {
return this.loading ? __('Uninstalling') : __('Uninstall');
},
},
};
</script>
<template>
<gl-button :disabled="disabled" variant="default" :loading="loading">
{{ label }}
</gl-button>
</template>
<script>
import { GlModal, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import trackUninstallButtonClickMixin from 'ee_else_ce/clusters/mixins/track_uninstall_button_click';
import { sprintf, s__ } from '~/locale';
import {
HELM,
INGRESS,
CERT_MANAGER,
PROMETHEUS,
RUNNER,
KNATIVE,
JUPYTER,
ELASTIC_STACK,
} from '../constants';
const CUSTOM_APP_WARNING_TEXT = {
[HELM]: sprintf(
s__(
'ClusterIntegration|The associated Tiller pod will be deleted and cannot be restored. Your other applications will remain unaffected.',
),
{
gitlabManagedAppsNamespace: '<code>gitlab-managed-apps</code>',
},
false,
),
[INGRESS]: s__(
'ClusterIntegration|The associated load balancer and IP will be deleted and cannot be restored.',
),
[CERT_MANAGER]: s__(
'ClusterIntegration|The associated private key will be deleted and cannot be restored.',
),
[PROMETHEUS]: s__('ClusterIntegration|All data will be deleted and cannot be restored.'),
[RUNNER]: s__('ClusterIntegration|Any running pipelines will be canceled.'),
[KNATIVE]: s__(
'ClusterIntegration|The associated IP and all deployed services will be deleted and cannot be restored. Uninstalling Knative will also remove Istio from your cluster. This will not effect any other applications.',
),
[JUPYTER]: s__(
'ClusterIntegration|All data not committed to GitLab will be deleted and cannot be restored.',
),
[ELASTIC_STACK]: s__('ClusterIntegration|All data will be deleted and cannot be restored.'),
};
export default {
components: {
GlModal,
},
directives: {
SafeHtml,
},
mixins: [trackUninstallButtonClickMixin],
props: {
application: {
type: String,
required: true,
},
applicationTitle: {
type: String,
required: true,
},
},
computed: {
title() {
return sprintf(s__('ClusterIntegration|Uninstall %{appTitle}'), {
appTitle: this.applicationTitle,
});
},
warningText() {
return sprintf(
s__('ClusterIntegration|You are about to uninstall %{appTitle} from your cluster.'),
{
appTitle: this.applicationTitle,
},
);
},
customAppWarningText() {
return CUSTOM_APP_WARNING_TEXT[this.application];
},
modalId() {
return `uninstall-${this.application}`;
},
},
methods: {
confirmUninstall() {
this.trackUninstallButtonClick(this.application);
this.$emit('confirm');
},
},
};
</script>
<template>
<gl-modal
ok-variant="danger"
cancel-variant="light"
:ok-title="title"
:modal-id="modalId"
:title="title"
@ok="confirmUninstall()"
>
{{ warningText }} <span v-safe-html="customAppWarningText"></span>
</gl-modal>
</template>
<script>
/* eslint-disable vue/no-v-html */
import { GlModal } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
import { ELASTIC_STACK } from '../constants';
const CUSTOM_APP_WARNING_TEXT = {
[ELASTIC_STACK]: s__(
'ClusterIntegration|Your Elasticsearch cluster will be re-created during this upgrade. Your logs will be re-indexed, and you will lose historical logs from hosts terminated in the last 30 days.',
),
};
export default {
components: {
GlModal,
},
props: {
application: {
type: String,
required: true,
},
applicationTitle: {
type: String,
required: true,
},
},
computed: {
title() {
return sprintf(s__('ClusterIntegration|Update %{appTitle}'), {
appTitle: this.applicationTitle,
});
},
warningText() {
return sprintf(
s__('ClusterIntegration|You are about to update %{appTitle} on your cluster.'),
{
appTitle: this.applicationTitle,
},
);
},
customAppWarningText() {
return CUSTOM_APP_WARNING_TEXT[this.application];
},
modalId() {
return `update-${this.application}`;
},
},
methods: {
confirmUpdate() {
this.$emit('confirm');
},
},
};
</script>
<template>
<gl-modal
ok-variant="danger"
cancel-variant="light"
:ok-title="title"
:modal-id="modalId"
:title="title"
@ok="confirmUpdate()"
>
{{ warningText }} <span v-html="customAppWarningText"></span>
</gl-modal>
</template>
......@@ -10,64 +10,7 @@ export const PROVIDER_TYPE = {
GCP: 'gcp',
};
// These need to match what is returned from the server
export const APPLICATION_STATUS = {
NO_STATUS: null,
NOT_INSTALLABLE: 'not_installable',
INSTALLABLE: 'installable',
SCHEDULED: 'scheduled',
INSTALLING: 'installing',
INSTALLED: 'installed',
UPDATING: 'updating',
UPDATED: 'updated',
UPDATE_ERRORED: 'update_errored',
UNINSTALLING: 'uninstalling',
UNINSTALL_ERRORED: 'uninstall_errored',
ERROR: 'errored',
PRE_INSTALLED: 'pre_installed',
UNINSTALLED: 'uninstalled',
EXTERNALLY_INSTALLED: 'externally_installed',
};
/*
* The application cannot be in any of the following states without
* not being installed.
*/
export const APPLICATION_INSTALLED_STATUSES = [
APPLICATION_STATUS.INSTALLED,
APPLICATION_STATUS.UPDATING,
APPLICATION_STATUS.UNINSTALLING,
APPLICATION_STATUS.PRE_INSTALLED,
];
// These are only used client-side
export const UPDATE_EVENT = 'update';
export const INSTALL_EVENT = 'install';
export const UNINSTALL_EVENT = 'uninstall';
export const HELM = 'helm';
export const INGRESS = 'ingress';
export const JUPYTER = 'jupyter';
export const KNATIVE = 'knative';
export const RUNNER = 'runner';
export const CERT_MANAGER = 'cert_manager';
export const CROSSPLANE = 'crossplane';
export const PROMETHEUS = 'prometheus';
export const ELASTIC_STACK = 'elastic_stack';
export const APPLICATIONS = [
HELM,
INGRESS,
JUPYTER,
KNATIVE,
RUNNER,
CERT_MANAGER,
PROMETHEUS,
ELASTIC_STACK,
];
export const INGRESS_DOMAIN_SUFFIX = '.nip.io';
export const LOGGING_MODE = 'logging';
export const BLOCKING_MODE = 'blocking';
import { APPLICATION_STATUS, UPDATE_EVENT, INSTALL_EVENT, UNINSTALL_EVENT } from '../constants';
const {
NO_STATUS,
SCHEDULED,
NOT_INSTALLABLE,
INSTALLABLE,
INSTALLING,
INSTALLED,
ERROR,
UPDATING,
UPDATED,
UPDATE_ERRORED,
UNINSTALLING,
UNINSTALL_ERRORED,
PRE_INSTALLED,
UNINSTALLED,
EXTERNALLY_INSTALLED,
} = APPLICATION_STATUS;
const applicationStateMachine = {
/* When the application initially loads, it will have `NO_STATUS`
* It will transition from `NO_STATUS` once the async backend call is completed
*/
[NO_STATUS]: {
on: {
[SCHEDULED]: {
target: INSTALLING,
},
[NOT_INSTALLABLE]: {
target: NOT_INSTALLABLE,
},
[INSTALLABLE]: {
target: INSTALLABLE,
},
[INSTALLING]: {
target: INSTALLING,
},
[INSTALLED]: {
target: INSTALLED,
},
[ERROR]: {
target: INSTALLABLE,
effects: {
installFailed: true,
},
},
[UPDATING]: {
target: UPDATING,
},
[UPDATED]: {
target: INSTALLED,
},
[UPDATE_ERRORED]: {
target: INSTALLED,
effects: {
updateFailed: true,
},
},
[UNINSTALLING]: {
target: UNINSTALLING,
},
[UNINSTALL_ERRORED]: {
target: INSTALLED,
effects: {
uninstallFailed: true,
},
},
[PRE_INSTALLED]: {
target: PRE_INSTALLED,
},
[UNINSTALLED]: {
target: UNINSTALLED,
},
[EXTERNALLY_INSTALLED]: {
target: EXTERNALLY_INSTALLED,
},
},
},
[NOT_INSTALLABLE]: {
on: {
[INSTALLABLE]: {
target: INSTALLABLE,
},
},
},
[INSTALLABLE]: {
on: {
[INSTALL_EVENT]: {
target: INSTALLING,
effects: {
installFailed: false,
},
},
[NOT_INSTALLABLE]: {
target: NOT_INSTALLABLE,
},
[INSTALLED]: {
target: INSTALLED,
effects: {
installFailed: false,
},
},
[UNINSTALLED]: {
target: UNINSTALLED,
effects: {
installFailed: false,
},
},
},
},
[INSTALLING]: {
on: {
[INSTALLED]: {
target: INSTALLED,
},
[ERROR]: {
target: INSTALLABLE,
effects: {
installFailed: true,
},
},
},
},
[INSTALLED]: {
on: {
[UPDATE_EVENT]: {
target: UPDATING,
effects: {
updateFailed: false,
updateSuccessful: false,
},
},
[NOT_INSTALLABLE]: {
target: NOT_INSTALLABLE,
},
[UNINSTALL_EVENT]: {
target: UNINSTALLING,
effects: {
uninstallFailed: false,
uninstallSuccessful: false,
},
},
[UNINSTALLED]: {
target: UNINSTALLED,
},
[ERROR]: {
target: INSTALLABLE,
effects: {
installFailed: true,
},
},
},
},
[PRE_INSTALLED]: {
on: {
[UPDATE_EVENT]: {
target: UPDATING,
effects: {
updateFailed: false,
updateSuccessful: false,
},
},
[NOT_INSTALLABLE]: {
target: NOT_INSTALLABLE,
},
[UNINSTALL_EVENT]: {
target: UNINSTALLING,
effects: {
uninstallFailed: false,
uninstallSuccessful: false,
},
},
},
},
[UPDATING]: {
on: {
[UPDATED]: {
target: INSTALLED,
effects: {
updateSuccessful: true,
},
},
[UPDATE_ERRORED]: {
target: INSTALLED,
effects: {
updateFailed: true,
},
},
},
},
[UNINSTALLING]: {
on: {
[INSTALLABLE]: {
target: INSTALLABLE,
effects: {
uninstallSuccessful: true,
},
},
[NOT_INSTALLABLE]: {
target: NOT_INSTALLABLE,
effects: {
uninstallSuccessful: true,
},
},
[UNINSTALL_ERRORED]: {
target: INSTALLED,
effects: {
uninstallFailed: true,
},
},
},
},
[UNINSTALLED]: {
on: {
[INSTALLED]: {
target: INSTALLED,
},
[ERROR]: {
target: INSTALLABLE,
effects: {
installFailed: true,
},
},
},
},
};
/**
* Determines an application new state based on the application current state
* and an event. If the application current state cannot handle a given event,
* the current state is returned.
*
* @param {*} application
* @param {*} event
*/
const transitionApplicationState = (application, event) => {
const stateMachine = applicationStateMachine[application.status];
const newState = stateMachine !== undefined ? stateMachine.on[event] : false;
return newState
? {
...application,
status: newState.target,
...newState.effects,
}
: application;
};
export default transitionApplicationState;
......@@ -3,38 +3,12 @@ import axios from '../../lib/utils/axios_utils';
export default class ClusterService {
constructor(options = {}) {
this.options = options;
this.appInstallEndpointMap = {
helm: this.options.installHelmEndpoint,
ingress: this.options.installIngressEndpoint,
cert_manager: this.options.installCertManagerEndpoint,
crossplane: this.options.installCrossplaneEndpoint,
runner: this.options.installRunnerEndpoint,
prometheus: this.options.installPrometheusEndpoint,
jupyter: this.options.installJupyterEndpoint,
knative: this.options.installKnativeEndpoint,
elastic_stack: this.options.installElasticStackEndpoint,
};
this.appUpdateEndpointMap = {
knative: this.options.updateKnativeEndpoint,
};
}
fetchClusterStatus() {
return axios.get(this.options.endpoint);
}
installApplication(appId, params) {
return axios.post(this.appInstallEndpointMap[appId], params);
}
updateApplication(appId, params) {
return axios.patch(this.appUpdateEndpointMap[appId], params);
}
uninstallApplication(appId, params) {
return axios.delete(this.appInstallEndpointMap[appId], params);
}
fetchClusterEnvironments() {
return axios.get(this.options.clusterEnvironmentsEndpoint);
}
......
import { parseBoolean } from '../../lib/utils/common_utils';
import { s__ } from '../../locale';
import {
INGRESS,
JUPYTER,
KNATIVE,
CERT_MANAGER,
CROSSPLANE,
RUNNER,
APPLICATION_INSTALLED_STATUSES,
APPLICATION_STATUS,
INSTALL_EVENT,
UPDATE_EVENT,
UNINSTALL_EVENT,
ELASTIC_STACK,
} from '../constants';
import transitionApplicationState from '../services/application_state_machine';
const isApplicationInstalled = (appStatus) => APPLICATION_INSTALLED_STATUSES.includes(appStatus);
const applicationInitialState = {
status: null,
statusReason: null,
requestReason: null,
installable: true,
installed: false,
installFailed: false,
uninstallable: false,
uninstallFailed: false,
uninstallSuccessful: false,
validationError: null,
};
export default class ClusterStore {
constructor() {
this.state = {
helpPath: null,
helmHelpPath: null,
ingressHelpPath: null,
environmentsHelpPath: null,
clustersHelpPath: null,
deployBoardsHelpPath: null,
cloudRunHelpPath: null,
status: null,
providerType: null,
preInstalledKnative: false,
rbac: false,
statusReason: null,
applications: {
helm: {
...applicationInitialState,
title: s__('ClusterIntegration|Legacy Helm Tiller server'),
},
ingress: {
...applicationInitialState,
title: s__('ClusterIntegration|Ingress'),
externalIp: null,
externalHostname: null,
updateFailed: false,
updateAvailable: false,
},
cert_manager: {
...applicationInitialState,
title: s__('ClusterIntegration|Cert-Manager'),
email: null,
},
crossplane: {
...applicationInitialState,
title: s__('ClusterIntegration|Crossplane'),
stack: null,
},
runner: {
...applicationInitialState,
title: s__('ClusterIntegration|GitLab Runner'),
version: null,
chartRepo: 'https://gitlab.com/gitlab-org/charts/gitlab-runner',
updateAvailable: null,
updateSuccessful: false,
updateFailed: false,
},
prometheus: {
...applicationInitialState,
title: s__('ClusterIntegration|Prometheus'),
},
jupyter: {
...applicationInitialState,
title: s__('ClusterIntegration|JupyterHub'),
hostname: null,
},
knative: {
...applicationInitialState,
title: s__('ClusterIntegration|Knative'),
hostname: null,
isEditingDomain: false,
externalIp: null,
externalHostname: null,
updateSuccessful: false,
updateFailed: false,
},
elastic_stack: {
...applicationInitialState,
title: s__('ClusterIntegration|Elastic Stack'),
},
cilium: {
...applicationInitialState,
title: s__('ClusterIntegration|GitLab Container Network Policies'),
installable: false,
},
},
environments: [],
fetchingEnvironments: false,
};
......@@ -118,10 +22,6 @@ export default class ClusterStore {
});
}
setManagePrometheusPath(managePrometheusPath) {
this.state.managePrometheusPath = managePrometheusPath;
}
updateStatus(status) {
this.state.status = status;
}
......@@ -130,10 +30,6 @@ export default class ClusterStore {
this.state.providerType = providerType;
}
updatePreInstalledKnative(preInstalledKnative) {
this.state.preInstalledKnative = parseBoolean(preInstalledKnative);
}
updateRbac(rbac) {
this.state.rbac = parseBoolean(rbac);
}
......@@ -142,112 +38,9 @@ export default class ClusterStore {
this.state.statusReason = reason;
}
installApplication(appId) {
this.handleApplicationEvent(appId, INSTALL_EVENT);
}
notifyInstallFailure(appId) {
this.handleApplicationEvent(appId, APPLICATION_STATUS.ERROR);
}
updateApplication(appId) {
this.handleApplicationEvent(appId, UPDATE_EVENT);
}
notifyUpdateFailure(appId) {
this.handleApplicationEvent(appId, APPLICATION_STATUS.UPDATE_ERRORED);
}
uninstallApplication(appId) {
this.handleApplicationEvent(appId, UNINSTALL_EVENT);
}
notifyUninstallFailure(appId) {
this.handleApplicationEvent(appId, APPLICATION_STATUS.UNINSTALL_ERRORED);
}
handleApplicationEvent(appId, event) {
const currentAppState = this.state.applications[appId];
this.state.applications[appId] = transitionApplicationState(currentAppState, event);
}
updateAppProperty(appId, prop, value) {
this.state.applications[appId][prop] = value;
}
updateStateFromServer(serverState = {}) {
this.state.status = serverState.status;
this.state.statusReason = serverState.status_reason;
serverState.applications.forEach((serverAppEntry) => {
const {
name: appId,
status,
status_reason: statusReason,
version,
update_available: updateAvailable,
can_uninstall: uninstallable,
} = serverAppEntry;
const currentApplicationState = this.state.applications[appId] || {};
const nextApplicationState = transitionApplicationState(currentApplicationState, status);
this.state.applications[appId] = {
...currentApplicationState,
...nextApplicationState,
statusReason,
installed: isApplicationInstalled(nextApplicationState.status),
uninstallable,
};
if (appId === INGRESS) {
this.state.applications.ingress.externalIp = serverAppEntry.external_ip;
this.state.applications.ingress.externalHostname = serverAppEntry.external_hostname;
this.state.applications.ingress.updateAvailable = updateAvailable;
} else if (appId === CERT_MANAGER) {
this.state.applications.cert_manager.email =
this.state.applications.cert_manager.email || serverAppEntry.email;
} else if (appId === CROSSPLANE) {
this.state.applications.crossplane.stack =
this.state.applications.crossplane.stack || serverAppEntry.stack;
} else if (appId === JUPYTER) {
this.state.applications.jupyter.hostname = this.updateHostnameIfUnset(
this.state.applications.jupyter.hostname,
serverAppEntry.hostname,
'jupyter',
);
} else if (appId === KNATIVE) {
if (serverAppEntry.available_domains) {
this.state.applications.knative.availableDomains = serverAppEntry.available_domains;
}
if (!this.state.applications.knative.isEditingDomain) {
this.state.applications.knative.pagesDomain =
serverAppEntry.pages_domain || this.state.applications.knative.pagesDomain;
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;
this.state.applications.knative.externalHostname =
serverAppEntry.external_hostname || this.state.applications.knative.externalHostname;
} else if (appId === RUNNER) {
this.state.applications.runner.version = version;
this.state.applications.runner.updateAvailable = updateAvailable;
} else if (appId === ELASTIC_STACK) {
this.state.applications.elastic_stack.version = version;
this.state.applications.elastic_stack.updateAvailable = updateAvailable;
}
});
}
updateHostnameIfUnset(current, updated, fallback) {
return (
current ||
updated ||
(this.state.applications.ingress.externalIp
? `${fallback}.${this.state.applications.ingress.externalIp}.nip.io`
: '')
);
}
toggleFetchEnvironments(isFetching) {
......
.cluster-applications-table#js-cluster-applications
- active = params[:tab] == 'apps'
%li.nav-item{ role: 'presentation' }
%a#cluster-apps-tab.nav-link.qa-applications{ class: active_when(active), href: clusterable.cluster_path(@cluster.id, params: {tab: 'apps'}) }
%span= _('Applications')
......@@ -2,21 +2,10 @@
- add_to_breadcrumbs _('Kubernetes Clusters'), clusterable.index_path
- breadcrumb_title @cluster.name
- page_title _('Kubernetes Cluster')
- manage_prometheus_path = edit_project_service_path(@cluster.project, 'prometheus') if @project
- cluster_environments_path = clusterable.environments_cluster_path(@cluster)
- status_path = clusterable.cluster_status_cluster_path(@cluster.id, format: :json) if can?(current_user, :admin_cluster, @cluster)
.edit-cluster-form.js-edit-cluster-form{ data: { status_path: status_path,
install_helm_path: clusterable.install_applications_cluster_path(@cluster, :helm),
install_ingress_path: clusterable.install_applications_cluster_path(@cluster, :ingress),
install_cert_manager_path: clusterable.install_applications_cluster_path(@cluster, :cert_manager),
install_crossplane_path: clusterable.install_applications_cluster_path(@cluster, :crossplane),
install_prometheus_path: clusterable.install_applications_cluster_path(@cluster, :prometheus),
install_runner_path: clusterable.install_applications_cluster_path(@cluster, :runner),
install_jupyter_path: clusterable.install_applications_cluster_path(@cluster, :jupyter),
install_knative_path: clusterable.install_applications_cluster_path(@cluster, :knative),
update_knative_path: clusterable.update_applications_cluster_path(@cluster, :knative),
install_elastic_stack_path: clusterable.install_applications_cluster_path(@cluster, :elastic_stack),
cluster_environments_path: cluster_environments_path,
toggle_status: @cluster.enabled? ? 'true': 'false',
has_rbac: has_rbac_enabled?(@cluster) ? 'true': 'false',
......@@ -24,15 +13,11 @@
cluster_status: @cluster.status_name,
cluster_status_reason: @cluster.status_reason,
provider_type: @cluster.provider_type,
pre_installed_knative: @cluster.knative_pre_installed? ? 'true': 'false',
help_path: help_page_path('user/project/clusters/index.md'),
environments_help_path: help_page_path('ci/environments/index.md', anchor: 'create-a-static-environment'),
clusters_help_path: help_page_path('user/project/clusters/index.md', anchor: 'deploying-to-a-kubernetes-cluster'),
deploy_boards_help_path: help_page_path('user/project/deploy_boards.md', anchor: 'enabling-deploy-boards'),
cloud_run_help_path: help_page_path('user/project/clusters/add_gke_clusters.md', anchor: 'cloud-run-for-anthos'),
manage_prometheus_path: manage_prometheus_path,
cluster_id: @cluster.id,
cilium_help_path: help_page_path('user/clusters/applications.md', anchor: 'install-cilium-using-gitlab-cicd')} }
cluster_id: @cluster.id } }
.js-cluster-application-notice
.flash-container
......
......@@ -3402,9 +3402,6 @@ msgstr ""
msgid "Amazon EKS integration allows you to provision EKS clusters from GitLab."
msgstr ""
msgid "Amazon Web Services"
msgstr ""
msgid "Amazon Web Services Logo"
msgstr ""
......@@ -6928,21 +6925,9 @@ msgstr ""
msgid "ClusterApplicationsRemoved|One-click application management was removed in GitLab 14.0. Your applications are still installed in your cluster, and integrations continue working."
msgstr ""
msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
msgstr ""
msgid "ClusterIntegration|%{boldStart}Note:%{boldEnd} Requires Ingress to be installed."
msgstr ""
msgid "ClusterIntegration|%{linkStart}More information%{linkEnd}"
msgstr ""
msgid "ClusterIntegration|%{title} uninstalled successfully."
msgstr ""
msgid "ClusterIntegration|%{title} updated successfully."
msgstr ""
msgid "ClusterIntegration|A cluster management project can be used to run deployment jobs with Kubernetes %{code_open}cluster-admin%{code_close} privileges."
msgstr ""
......@@ -6976,12 +6961,6 @@ msgstr ""
msgid "ClusterIntegration|Advanced options on this Kubernetes cluster’s integration"
msgstr ""
msgid "ClusterIntegration|All data not committed to GitLab will be deleted and cannot be restored."
msgstr ""
msgid "ClusterIntegration|All data will be deleted and cannot be restored."
msgstr ""
msgid "ClusterIntegration|Allow GitLab to manage namespace and service accounts for this cluster. %{linkStart}More information%{linkEnd}"
msgstr ""
......@@ -7015,9 +6994,6 @@ msgstr ""
msgid "ClusterIntegration|Any project namespaces"
msgstr ""
msgid "ClusterIntegration|Any running pipelines will be canceled."
msgstr ""
msgid "ClusterIntegration|Apply for credit"
msgstr ""
......@@ -7036,15 +7012,6 @@ msgstr ""
msgid "ClusterIntegration|CA Certificate"
msgstr ""
msgid "ClusterIntegration|Can be safely removed. Prior to GitLab 13.2, GitLab used a remote Tiller server to manage the applications. GitLab no longer uses this server. Uninstalling this server will not affect your other applications. This row will disappear afterwards."
msgstr ""
msgid "ClusterIntegration|Cert-Manager"
msgstr ""
msgid "ClusterIntegration|Cert-Manager is a native Kubernetes certificate management controller that helps with issuing certificates. Installing Cert-Manager on your cluster will issue a certificate by %{linkStart}Let's Encrypt%{linkEnd} and ensure that certificates are valid and up-to-date."
msgstr ""
msgid "ClusterIntegration|Certificate Authority bundle (PEM format)"
msgstr ""
......@@ -7066,9 +7033,6 @@ msgstr ""
msgid "ClusterIntegration|Choose the worker node %{linkStart}instance type%{linkEnd}."
msgstr ""
msgid "ClusterIntegration|Choose which applications to install on your Kubernetes cluster."
msgstr ""
msgid "ClusterIntegration|Choose which of your environments will use this cluster."
msgstr ""
......@@ -7108,15 +7072,6 @@ msgstr ""
msgid "ClusterIntegration|Copy CA Certificate"
msgstr ""
msgid "ClusterIntegration|Copy Ingress Endpoint"
msgstr ""
msgid "ClusterIntegration|Copy Jupyter Hostname"
msgstr ""
msgid "ClusterIntegration|Copy Knative Endpoint"
msgstr ""
msgid "ClusterIntegration|Copy Kubernetes cluster name"
msgstr ""
......@@ -7165,12 +7120,6 @@ msgstr ""
msgid "ClusterIntegration|Creating Kubernetes cluster"
msgstr ""
msgid "ClusterIntegration|Crossplane"
msgstr ""
msgid "ClusterIntegration|Crossplane enables declarative provisioning of managed services from your cloud of choice using %{codeStart}kubectl%{codeEnd} or %{linkStart}GitLab Integration%{linkEnd}. Crossplane runs inside your Kubernetes cluster and supports secure connectivity and secrets management between app containers and the cloud services they depend on."
msgstr ""
msgid "ClusterIntegration|Deletes all GitLab resources attached to this cluster during removal"
msgstr ""
......@@ -7186,9 +7135,6 @@ msgstr ""
msgid "ClusterIntegration|Elastic Kubernetes Service"
msgstr ""
msgid "ClusterIntegration|Elastic Stack"
msgstr ""
msgid "ClusterIntegration|Enable Cloud Run for Anthos"
msgstr ""
......@@ -7204,9 +7150,6 @@ msgstr ""
msgid "ClusterIntegration|Enable this setting if using role-based access control (RBAC)."
msgstr ""
msgid "ClusterIntegration|Enabled stack"
msgstr ""
msgid "ClusterIntegration|Enter new Service Token"
msgstr ""
......@@ -7252,18 +7195,9 @@ msgstr ""
msgid "ClusterIntegration|GitLab Agent managed clusters"
msgstr ""
msgid "ClusterIntegration|GitLab Container Network Policies"
msgstr ""
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
msgid "ClusterIntegration|GitLab Runner"
msgstr ""
msgid "ClusterIntegration|GitLab Runner connects to the repository and executes CI/CD jobs, pushing results back and deploying applications to production."
msgstr ""
msgid "ClusterIntegration|GitLab failed to authenticate."
msgstr ""
......@@ -7300,18 +7234,6 @@ msgstr ""
msgid "ClusterIntegration|In order to view the health of your cluster, you must first enable Prometheus in the Integrations tab."
msgstr ""
msgid "ClusterIntegration|Ingress"
msgstr ""
msgid "ClusterIntegration|Ingress Endpoint"
msgstr ""
msgid "ClusterIntegration|Ingress gives you a way to route requests to services based on the request host or path, centralizing a number of services into a single entrypoint."
msgstr ""
msgid "ClusterIntegration|Installing Ingress may incur additional costs. Learn more about %{linkStart}pricing%{linkEnd}."
msgstr ""
msgid "ClusterIntegration|Instance cluster"
msgstr ""
......@@ -7333,39 +7255,9 @@ msgstr ""
msgid "ClusterIntegration|Integrations allow you to use applications installed in your cluster as part of your GitLab workflow."
msgstr ""
msgid "ClusterIntegration|Issuer Email"
msgstr ""
msgid "ClusterIntegration|Issuers represent a certificate authority. You must provide an email address for your Issuer."
msgstr ""
msgid "ClusterIntegration|Jupyter Hostname"
msgstr ""
msgid "ClusterIntegration|JupyterHub"
msgstr ""
msgid "ClusterIntegration|JupyterHub, a multi-user Hub, spawns, manages, and proxies multiple instances of the single-user Jupyter notebook server. JupyterHub can be used to serve notebooks to a class of students, a corporate data science group, or a scientific research group."
msgstr ""
msgid "ClusterIntegration|Key pair name"
msgstr ""
msgid "ClusterIntegration|Knative"
msgstr ""
msgid "ClusterIntegration|Knative Domain Name:"
msgstr ""
msgid "ClusterIntegration|Knative Endpoint:"
msgstr ""
msgid "ClusterIntegration|Knative domain name was updated successfully."
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."
msgstr ""
msgid "ClusterIntegration|Kubernetes cluster is being created..."
msgstr ""
......@@ -7399,9 +7291,6 @@ msgstr ""
msgid "ClusterIntegration|Learn more about instance Kubernetes clusters"
msgstr ""
msgid "ClusterIntegration|Legacy Helm Tiller server"
msgstr ""
msgid "ClusterIntegration|Loading IAM Roles"
msgstr ""
......@@ -7492,9 +7381,6 @@ msgstr ""
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr ""
msgid "ClusterIntegration|Point a wildcard DNS to this generated endpoint in order to access your application after it has been deployed."
msgstr ""
msgid "ClusterIntegration|Project cluster"
msgstr ""
......@@ -7504,15 +7390,6 @@ msgstr ""
msgid "ClusterIntegration|Project namespace prefix (optional, unique)"
msgstr ""
msgid "ClusterIntegration|Prometheus"
msgstr ""
msgid "ClusterIntegration|Prometheus is an open-source monitoring system with %{linkStart}GitLab Integration%{linkEnd} to monitor deployed applications."
msgstr ""
msgid "ClusterIntegration|Protect your clusters with GitLab Container Network Policies by enforcing how pods communicate with each other and other network endpoints. %{linkStart}Learn more about configuring Network Policies here.%{linkEnd}"
msgstr ""
msgid "ClusterIntegration|Provider details"
msgstr ""
......@@ -7549,15 +7426,6 @@ msgstr ""
msgid "ClusterIntegration|Removes cluster from project but keeps associated resources"
msgstr ""
msgid "ClusterIntegration|Replace this with your own hostname if you want. If you do so, point hostname to Ingress IP Address from above."
msgstr ""
msgid "ClusterIntegration|Request to begin installing failed"
msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
msgid "ClusterIntegration|Save changes"
msgstr ""
......@@ -7570,9 +7438,6 @@ msgstr ""
msgid "ClusterIntegration|Search VPCs"
msgstr ""
msgid "ClusterIntegration|Search domains"
msgstr ""
msgid "ClusterIntegration|Search instance types"
msgstr ""
......@@ -7621,15 +7486,9 @@ msgstr ""
msgid "ClusterIntegration|Select a region to choose a VPC"
msgstr ""
msgid "ClusterIntegration|Select a stack to install Crossplane."
msgstr ""
msgid "ClusterIntegration|Select a zone to choose a network"
msgstr ""
msgid "ClusterIntegration|Select existing domain or use new"
msgstr ""
msgid "ClusterIntegration|Select machine type"
msgstr ""
......@@ -7672,15 +7531,6 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong while creating your Kubernetes cluster"
msgstr ""
msgid "ClusterIntegration|Something went wrong while installing %{title}"
msgstr ""
msgid "ClusterIntegration|Something went wrong while uninstalling %{title}"
msgstr ""
msgid "ClusterIntegration|Something went wrong while updating Knative domain name."
msgstr ""
msgid "ClusterIntegration|Specifying a domain will allow you to use Auto Review Apps and Auto Deploy stages for %{linkStart}Auto DevOps.%{linkEnd} The domain should have a wildcard DNS configured matching the domain. "
msgstr ""
......@@ -7696,24 +7546,6 @@ msgstr ""
msgid "ClusterIntegration|The URL used to access the Kubernetes API."
msgstr ""
msgid "ClusterIntegration|The associated IP and all deployed services will be deleted and cannot be restored. Uninstalling Knative will also remove Istio from your cluster. This will not effect any other applications."
msgstr ""
msgid "ClusterIntegration|The associated Tiller pod will be deleted and cannot be restored. Your other applications will remain unaffected."
msgstr ""
msgid "ClusterIntegration|The associated load balancer and IP will be deleted and cannot be restored."
msgstr ""
msgid "ClusterIntegration|The associated private key will be deleted and cannot be restored."
msgstr ""
msgid "ClusterIntegration|The elastic stack collects logs from all pods in your cluster"
msgstr ""
msgid "ClusterIntegration|The endpoint is in the process of being assigned. Please check your Kubernetes cluster or Quotas on Google Kubernetes Engine if it takes a long time."
msgstr ""
msgid "ClusterIntegration|The namespace associated with your project. This will be used for deploy boards, logs, and Web terminals."
msgstr ""
......@@ -7744,9 +7576,6 @@ msgstr ""
msgid "ClusterIntegration|This will permanently delete the following resources:"
msgstr ""
msgid "ClusterIntegration|To access your application after deployment, point a wildcard DNS to the Knative Endpoint."
msgstr ""
msgid "ClusterIntegration|To create a cluster, first create a project on %{docsLinkStart}Google Cloud Platform%{docsLinkEnd}."
msgstr ""
......@@ -7768,21 +7597,9 @@ msgstr ""
msgid "ClusterIntegration|Unable to Connect"
msgstr ""
msgid "ClusterIntegration|Uninstall %{appTitle}"
msgstr ""
msgid "ClusterIntegration|Unknown Error"
msgstr ""
msgid "ClusterIntegration|Update %{appTitle}"
msgstr ""
msgid "ClusterIntegration|Update failed. Please check the logs and try again."
msgstr ""
msgid "ClusterIntegration|Use %{query}"
msgstr ""
msgid "ClusterIntegration|Uses the Cloud Run, Istio, and HTTP Load Balancing addons for this cluster."
msgstr ""
......@@ -7807,27 +7624,12 @@ msgstr ""
msgid "ClusterIntegration|You are about to remove your cluster integration."
msgstr ""
msgid "ClusterIntegration|You are about to uninstall %{appTitle} from your cluster."
msgstr ""
msgid "ClusterIntegration|You are about to update %{appTitle} on your cluster."
msgstr ""
msgid "ClusterIntegration|You must grant access to your organization’s AWS resources in order to create a new EKS cluster. To grant access, create a provision role using the account and external ID below and provide us the ARN."
msgstr ""
msgid "ClusterIntegration|You must have an RBAC-enabled cluster to install Knative."
msgstr ""
msgid "ClusterIntegration|You must specify a domain before you can install Knative."
msgstr ""
msgid "ClusterIntegration|You should select at least two subnets"
msgstr ""
msgid "ClusterIntegration|Your Elasticsearch cluster will be re-created during this upgrade. Your logs will be re-indexed, and you will lose historical logs from hosts terminated in the last 30 days."
msgstr ""
msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}"
msgstr ""
......@@ -7843,9 +7645,6 @@ msgstr ""
msgid "ClusterIntegration|access to Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|installed via %{linkStart}Cloud Run%{linkEnd}"
msgstr ""
msgid "ClusterIntegration|meets the requirements"
msgstr ""
......@@ -9654,9 +9453,6 @@ msgstr ""
msgid "Cron time zone"
msgstr ""
msgid "Crossplane"
msgstr ""
msgid "Crowd"
msgstr ""
......@@ -13452,9 +13248,6 @@ msgstr ""
msgid "ExternalWikiService|https://example.com/xxx/wiki/..."
msgstr ""
msgid "Externally installed"
msgstr ""
msgid "Facebook"
msgstr ""
......@@ -15403,9 +15196,6 @@ msgstr ""
msgid "Goal of the changes and what reviewers should be aware of"
msgstr ""
msgid "Google Cloud Platform"
msgstr ""
msgid "Google authentication is not %{link_start}properly configured%{link_end}. Ask your GitLab administrator if you want to use this service."
msgstr ""
......@@ -17579,9 +17369,6 @@ msgstr ""
msgid "Insights|This project is filtered out in the insights.yml file (see the projects.only config for more information)."
msgstr ""
msgid "Install"
msgstr ""
msgid "Install GitLab Runner and ensure it's running."
msgstr ""
......@@ -17594,12 +17381,6 @@ msgstr ""
msgid "Installation"
msgstr ""
msgid "Installed"
msgstr ""
msgid "Installing"
msgstr ""
msgid "Instance"
msgid_plural "Instances"
msgstr[0] ""
......@@ -19979,9 +19760,6 @@ msgstr ""
msgid "Makes this issue confidential."
msgstr ""
msgid "Manage"
msgstr ""
msgid "Manage Web IDE features."
msgstr ""
......@@ -21148,9 +20926,6 @@ msgstr ""
msgid "Mi"
msgstr ""
msgid "Microsoft Azure"
msgstr ""
msgid "Middleman project with Static Site Editor support"
msgstr ""
......@@ -28044,9 +27819,6 @@ msgstr ""
msgid "Retry this job in order to create the necessary resources."
msgstr ""
msgid "Retry update"
msgstr ""
msgid "Retry verification"
msgstr ""
......@@ -28153,9 +27925,6 @@ msgstr ""
msgid "Rollback"
msgstr ""
msgid "Rook"
msgstr ""
msgid "Ruby"
msgstr ""
......@@ -29324,9 +29093,6 @@ msgstr ""
msgid "Select Page"
msgstr ""
msgid "Select Stack"
msgstr ""
msgid "Select a branch"
msgstr ""
......@@ -34869,12 +34635,6 @@ msgstr ""
msgid "Unhappy?"
msgstr ""
msgid "Uninstall"
msgstr ""
msgid "Uninstalling"
msgstr ""
msgid "Units|ms"
msgstr ""
......@@ -35100,9 +34860,6 @@ msgstr ""
msgid "Updated %{updated_at} by %{updated_by}"
msgstr ""
msgid "Updated to %{linkStart}chart v%{linkEnd}"
msgstr ""
msgid "Updates"
msgstr ""
......@@ -37596,9 +37353,6 @@ msgstr ""
msgid "You must provide your current password in order to change it."
msgstr ""
msgid "You must select a stack for configuring your cloud provider. Learn more about"
msgstr ""
msgid "You must solve the CAPTCHA in order to submit"
msgstr ""
......
......@@ -6,10 +6,6 @@ module QA
module Infrastructure
module Kubernetes
class Show < Page::Base
view 'app/assets/javascripts/clusters/components/applications.vue' do
element :ingress_ip_address, 'id="ingress-endpoint"' # rubocop:disable QA/ElementWithPattern
end
view 'app/assets/javascripts/clusters/forms/components/integration_form.vue' do
element :integration_status_toggle, required: true
element :base_domain_field, required: true
......@@ -20,15 +16,6 @@ module QA
element :details, required: true
end
view 'app/views/clusters/clusters/_applications_tab.html.haml' do
element :applications, required: true
end
view 'app/assets/javascripts/clusters/components/application_row.vue' do
element :install_button
element :uninstall_button
end
view 'app/views/clusters/clusters/_health.html.haml' do
element :cluster_health_section
end
......@@ -42,36 +29,6 @@ module QA
click_element :details
end
def open_applications
has_element?(:applications, wait: 30)
click_element :applications
end
def install!(application_name)
within_element(application_name) do
has_element?(:install_button, application: application_name, wait: 30)
click_element :install_button
end
end
def await_installed(application_name)
within_element(application_name) do
has_element?(:uninstall_button, application: application_name, wait: 300, skip_finished_loading_check: true)
end
end
def has_application_installed?(application_name)
within_element(application_name) do
has_element?(:uninstall_button, application: application_name, wait: 300)
end
end
def ingress_ip
# We need to wait longer since it can take some time before the
# ip address is assigned for the ingress controller
page.find('#ingress-endpoint', wait: 1200).value
end
def set_domain(domain)
fill_element :base_domain_field, domain
end
......
......@@ -3,6 +3,8 @@
module QA
module Resource
module KubernetesCluster
# TODO: This resource is currently broken, since one-click apps have been removed.
# See https://gitlab.com/gitlab-org/gitlab/-/issues/333818
class ProjectCluster < Base
attr_writer :cluster,
:install_ingress, :install_prometheus, :install_runner, :domain
......@@ -40,6 +42,8 @@ module QA
# We must wait a few seconds for permissions to be set up correctly for new cluster
sleep 25
# TODO: These steps do not work anymore, see https://gitlab.com/gitlab-org/gitlab/-/issues/333818
# Open applications tab
show.open_applications
......
......@@ -2,15 +2,12 @@ import MockAdapter from 'axios-mock-adapter';
import { loadHTMLFixture } from 'helpers/fixtures';
import { setTestTimeout } from 'helpers/timeout';
import Clusters from '~/clusters/clusters_bundle';
import { APPLICATION_STATUS, APPLICATIONS, RUNNER } from '~/clusters/constants';
import axios from '~/lib/utils/axios_utils';
import initProjectSelectDropdown from '~/project_select';
jest.mock('~/lib/utils/poll');
jest.mock('~/project_select');
const { INSTALLING, INSTALLABLE, INSTALLED, UNINSTALLING } = APPLICATION_STATUS;
describe('Clusters', () => {
setTestTimeout(1000);
......@@ -57,67 +54,6 @@ describe('Clusters', () => {
});
});
describe('checkForNewInstalls', () => {
const INITIAL_APP_MAP = {
helm: { status: null, title: 'Helm Tiller' },
ingress: { status: null, title: 'Ingress' },
runner: { status: null, title: 'GitLab Runner' },
};
it('does not show alert when things transition from initial null state to something', () => {
cluster.checkForNewInstalls(INITIAL_APP_MAP, {
...INITIAL_APP_MAP,
helm: { status: INSTALLABLE, title: 'Helm Tiller' },
});
const flashMessage = document.querySelector('.js-cluster-application-notice .flash-text');
expect(flashMessage).toBeNull();
});
it('shows an alert when something gets newly installed', () => {
cluster.checkForNewInstalls(
{
...INITIAL_APP_MAP,
helm: { status: INSTALLING, title: 'Helm Tiller' },
},
{
...INITIAL_APP_MAP,
helm: { status: INSTALLED, title: 'Helm Tiller' },
},
);
const flashMessage = document.querySelector('.js-cluster-application-notice .flash-text');
expect(flashMessage).not.toBeNull();
expect(flashMessage.textContent.trim()).toEqual(
'Helm Tiller was successfully installed on your Kubernetes cluster',
);
});
it('shows an alert when multiple things gets newly installed', () => {
cluster.checkForNewInstalls(
{
...INITIAL_APP_MAP,
helm: { status: INSTALLING, title: 'Helm Tiller' },
ingress: { status: INSTALLABLE, title: 'Ingress' },
},
{
...INITIAL_APP_MAP,
helm: { status: INSTALLED, title: 'Helm Tiller' },
ingress: { status: INSTALLED, title: 'Ingress' },
},
);
const flashMessage = document.querySelector('.js-cluster-application-notice .flash-text');
expect(flashMessage).not.toBeNull();
expect(flashMessage.textContent.trim()).toEqual(
'Helm Tiller, Ingress was successfully installed on your Kubernetes cluster',
);
});
});
describe('updateContainer', () => {
const { location } = window;
......@@ -237,77 +173,6 @@ describe('Clusters', () => {
});
});
describe('installApplication', () => {
it.each(APPLICATIONS)('tries to install %s', (applicationId, done) => {
jest.spyOn(cluster.service, 'installApplication').mockResolvedValue();
cluster.store.state.applications[applicationId].status = INSTALLABLE;
const params = {};
if (applicationId === 'knative') {
params.hostname = 'test-example.com';
}
// eslint-disable-next-line promise/valid-params
cluster
.installApplication({ id: applicationId, params })
.then(() => {
expect(cluster.store.state.applications[applicationId].status).toEqual(INSTALLING);
expect(cluster.store.state.applications[applicationId].requestReason).toEqual(null);
expect(cluster.service.installApplication).toHaveBeenCalledWith(applicationId, params);
done();
})
.catch();
});
it('sets error request status when the request fails', () => {
jest
.spyOn(cluster.service, 'installApplication')
.mockRejectedValueOnce(new Error('STUBBED ERROR'));
cluster.store.state.applications.helm.status = INSTALLABLE;
const promise = cluster.installApplication({ id: 'helm' });
return promise.then(() => {
expect(cluster.store.state.applications.helm.status).toEqual(INSTALLABLE);
expect(cluster.store.state.applications.helm.installFailed).toBe(true);
expect(cluster.store.state.applications.helm.requestReason).toBeDefined();
});
});
});
describe('uninstallApplication', () => {
it.each(APPLICATIONS)('tries to uninstall %s', (applicationId) => {
jest.spyOn(cluster.service, 'uninstallApplication').mockResolvedValueOnce();
cluster.store.state.applications[applicationId].status = INSTALLED;
cluster.uninstallApplication({ id: applicationId });
expect(cluster.store.state.applications[applicationId].status).toEqual(UNINSTALLING);
expect(cluster.store.state.applications[applicationId].requestReason).toEqual(null);
expect(cluster.service.uninstallApplication).toHaveBeenCalledWith(applicationId);
});
it('sets error request status when the uninstall request fails', () => {
jest
.spyOn(cluster.service, 'uninstallApplication')
.mockRejectedValueOnce(new Error('STUBBED ERROR'));
cluster.store.state.applications.helm.status = INSTALLED;
const promise = cluster.uninstallApplication({ id: 'helm' });
return promise.then(() => {
expect(cluster.store.state.applications.helm.status).toEqual(INSTALLED);
expect(cluster.store.state.applications.helm.uninstallFailed).toBe(true);
expect(cluster.store.state.applications.helm.requestReason).toBeDefined();
});
});
});
describe('fetch cluster environments success', () => {
beforeEach(() => {
jest.spyOn(cluster.store, 'toggleFetchEnvironments').mockReturnThis();
......@@ -328,7 +193,6 @@ describe('Clusters', () => {
describe('handleClusterStatusSuccess', () => {
beforeEach(() => {
jest.spyOn(cluster.store, 'updateStateFromServer').mockReturnThis();
jest.spyOn(cluster, 'checkForNewInstalls').mockReturnThis();
jest.spyOn(cluster, 'updateContainer').mockReturnThis();
cluster.handleClusterStatusSuccess({ data: {} });
});
......@@ -337,38 +201,8 @@ describe('Clusters', () => {
expect(cluster.store.updateStateFromServer).toHaveBeenCalled();
});
it('checks for new installable apps', () => {
expect(cluster.checkForNewInstalls).toHaveBeenCalled();
});
it('updates message containers', () => {
expect(cluster.updateContainer).toHaveBeenCalled();
});
});
describe('updateApplication', () => {
const params = { version: '1.0.0' };
let storeUpdateApplication;
let installApplication;
beforeEach(() => {
storeUpdateApplication = jest.spyOn(cluster.store, 'updateApplication');
installApplication = jest.spyOn(cluster.service, 'installApplication');
cluster.updateApplication({ id: RUNNER, params });
});
afterEach(() => {
storeUpdateApplication.mockRestore();
installApplication.mockRestore();
});
it('calls store updateApplication method', () => {
expect(storeUpdateApplication).toHaveBeenCalledWith(RUNNER);
});
it('sends installApplication request', () => {
expect(installApplication).toHaveBeenCalledWith(RUNNER, params);
});
});
});
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Applications Cert-Manager application shows the correct description 1`] = `
<p
data-testid="certManagerDescription"
>
Cert-Manager is a native Kubernetes certificate management controller that helps with issuing certificates. Installing Cert-Manager on your cluster will issue a certificate by
<a
class="gl-link"
href="https://letsencrypt.org/"
rel="noopener noreferrer"
target="_blank"
>
Let's Encrypt
</a>
and ensure that certificates are valid and up-to-date.
</p>
`;
exports[`Applications Cilium application shows the correct description 1`] = `
<p
data-testid="ciliumDescription"
>
Protect your clusters with GitLab Container Network Policies by enforcing how pods communicate with each other and other network endpoints.
<a
class="gl-link"
href="cilium-help-path"
rel="noopener"
target="_blank"
>
Learn more about configuring Network Policies here.
</a>
</p>
`;
exports[`Applications Crossplane application shows the correct description 1`] = `
<p
data-testid="crossplaneDescription"
>
Crossplane enables declarative provisioning of managed services from your cloud of choice using
<code>
kubectl
</code>
or
<a
class="gl-link"
href="https://docs.gitlab.com/ee/user/clusters/applications.html#crossplane"
rel="noopener noreferrer"
target="_blank"
>
GitLab Integration
</a>
. Crossplane runs inside your Kubernetes cluster and supports secure connectivity and secrets management between app containers and the cloud services they depend on.
</p>
`;
exports[`Applications Ingress application shows the correct warning message 1`] = `
<span
data-testid="ingressCostWarning"
>
Installing Ingress may incur additional costs. Learn more about
<a
class="gl-link"
href="https://cloud.google.com/compute/pricing#lb"
rel="noopener noreferrer"
target="_blank"
>
pricing
</a>
.
</span>
`;
exports[`Applications Knative application shows the correct description 1`] = `
<span
data-testid="installed-via"
>
installed via
<a
class="gl-link"
href=""
rel="noopener"
target="_blank"
>
Cloud Run
</a>
</span>
`;
exports[`Applications Prometheus application shows the correct description 1`] = `
<span
data-testid="prometheusDescription"
>
Prometheus is an open-source monitoring system with
<a
class="gl-link"
href="https://docs.gitlab.com/ee/user/project/integrations/prometheus.html"
rel="noopener noreferrer"
target="_blank"
>
GitLab Integration
</a>
to monitor deployed applications.
</span>
`;
import { GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import ApplicationRow from '~/clusters/components/application_row.vue';
import UninstallApplicationConfirmationModal from '~/clusters/components/uninstall_application_confirmation_modal.vue';
import UpdateApplicationConfirmationModal from '~/clusters/components/update_application_confirmation_modal.vue';
import { APPLICATION_STATUS, ELASTIC_STACK } from '~/clusters/constants';
import eventHub from '~/clusters/event_hub';
import { DEFAULT_APPLICATION_STATE } from '../services/mock_data';
describe('Application Row', () => {
let wrapper;
afterEach(() => {
wrapper.destroy();
});
const mountComponent = (data) => {
wrapper = shallowMount(ApplicationRow, {
stubs: { GlSprintf },
propsData: {
...DEFAULT_APPLICATION_STATE,
...data,
},
});
};
describe('Title', () => {
it('shows title', () => {
mountComponent({ titleLink: null });
const title = wrapper.find('.js-cluster-application-title');
expect(title.element).toBeInstanceOf(HTMLSpanElement);
expect(title.text()).toEqual(DEFAULT_APPLICATION_STATE.title);
});
it('shows title link', () => {
expect(DEFAULT_APPLICATION_STATE.titleLink).toBeDefined();
mountComponent();
const title = wrapper.find('.js-cluster-application-title');
expect(title.element).toBeInstanceOf(HTMLAnchorElement);
expect(title.text()).toEqual(DEFAULT_APPLICATION_STATE.title);
});
});
describe('Install button', () => {
const button = () => wrapper.find('.js-cluster-application-install-button');
const checkButtonState = (label, loading, disabled) => {
expect(button().text()).toEqual(label);
expect(button().props('loading')).toEqual(loading);
expect(button().props('disabled')).toEqual(disabled);
};
it('has indeterminate state on page load', () => {
mountComponent({ status: null });
expect(button().text()).toBe('');
});
it('has install button', () => {
mountComponent();
expect(button().exists()).toBe(true);
});
it('has disabled "Install" when APPLICATION_STATUS.NOT_INSTALLABLE', () => {
mountComponent({ status: APPLICATION_STATUS.NOT_INSTALLABLE });
checkButtonState('Install', false, true);
});
it('has enabled "Install" when APPLICATION_STATUS.INSTALLABLE', () => {
mountComponent({ status: APPLICATION_STATUS.INSTALLABLE });
checkButtonState('Install', false, false);
});
it('has loading "Installing" when APPLICATION_STATUS.INSTALLING', () => {
mountComponent({ status: APPLICATION_STATUS.INSTALLING });
checkButtonState('Installing', true, true);
});
it('has disabled "Install" when APPLICATION_STATUS.UNINSTALLED', () => {
mountComponent({ status: APPLICATION_STATUS.UNINSTALLED });
checkButtonState('Install', false, true);
});
it('has disabled "Externally installed" when APPLICATION_STATUS.EXTERNALLY_INSTALLED', () => {
mountComponent({ status: APPLICATION_STATUS.EXTERNALLY_INSTALLED });
checkButtonState('Externally installed', false, true);
});
it('has disabled "Installed" when application is installed and not uninstallable', () => {
mountComponent({
status: APPLICATION_STATUS.INSTALLED,
installed: true,
uninstallable: false,
});
checkButtonState('Installed', false, true);
});
it('hides when application is installed and uninstallable', () => {
mountComponent({
status: APPLICATION_STATUS.INSTALLED,
installed: true,
uninstallable: true,
});
expect(button().exists()).toBe(false);
});
it('has enabled "Install" when install fails', () => {
mountComponent({
status: APPLICATION_STATUS.INSTALLABLE,
installFailed: true,
});
checkButtonState('Install', false, false);
});
it('has disabled "Install" when installation disabled', () => {
mountComponent({
status: APPLICATION_STATUS.INSTALLABLE,
installable: false,
});
checkButtonState('Install', false, true);
});
it('has enabled "Install" when REQUEST_FAILURE (so you can try installing again)', () => {
mountComponent({ status: APPLICATION_STATUS.INSTALLABLE });
checkButtonState('Install', false, false);
});
it('clicking install button emits event', () => {
const spy = jest.spyOn(eventHub, '$emit');
mountComponent({ status: APPLICATION_STATUS.INSTALLABLE });
button().vm.$emit('click');
expect(spy).toHaveBeenCalledWith('installApplication', {
id: DEFAULT_APPLICATION_STATE.id,
params: {},
});
});
it('clicking install button when installApplicationRequestParams are provided emits event', () => {
const spy = jest.spyOn(eventHub, '$emit');
mountComponent({
status: APPLICATION_STATUS.INSTALLABLE,
installApplicationRequestParams: { hostname: 'jupyter' },
});
button().vm.$emit('click');
expect(spy).toHaveBeenCalledWith('installApplication', {
id: DEFAULT_APPLICATION_STATE.id,
params: { hostname: 'jupyter' },
});
});
it('clicking disabled install button emits nothing', () => {
const spy = jest.spyOn(eventHub, '$emit');
mountComponent({ status: APPLICATION_STATUS.INSTALLING });
expect(button().props('disabled')).toEqual(true);
button().vm.$emit('click');
expect(spy).not.toHaveBeenCalled();
});
});
describe('Uninstall button', () => {
it('displays button when app is installed and uninstallable', () => {
mountComponent({
installed: true,
uninstallable: true,
status: APPLICATION_STATUS.NOT_INSTALLABLE,
});
const uninstallButton = wrapper.find('.js-cluster-application-uninstall-button');
expect(uninstallButton.exists()).toBe(true);
});
it('displays a success toast message if application uninstall was successful', async () => {
mountComponent({
title: 'GitLab Runner',
uninstallSuccessful: false,
});
wrapper.vm.$toast = { show: jest.fn() };
wrapper.setProps({ uninstallSuccessful: true });
await wrapper.vm.$nextTick();
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(
'GitLab Runner uninstalled successfully.',
);
});
});
describe('when confirmation modal triggers confirm event', () => {
it('triggers uninstallApplication event', () => {
jest.spyOn(eventHub, '$emit');
mountComponent();
wrapper.find(UninstallApplicationConfirmationModal).vm.$emit('confirm');
expect(eventHub.$emit).toHaveBeenCalledWith('uninstallApplication', {
id: DEFAULT_APPLICATION_STATE.id,
});
});
});
describe('Update button', () => {
const button = () => wrapper.find('.js-cluster-application-update-button');
it('has indeterminate state on page load', () => {
mountComponent();
expect(button().exists()).toBe(false);
});
it('has enabled "Update" when "updateAvailable" is true', () => {
mountComponent({ updateAvailable: true });
expect(button().exists()).toBe(true);
expect(button().text()).toContain('Update');
});
it('has enabled "Retry update" when update process fails', () => {
mountComponent({
status: APPLICATION_STATUS.INSTALLED,
updateFailed: true,
});
expect(button().exists()).toBe(true);
expect(button().text()).toContain('Retry update');
});
it('has disabled "Updating" when APPLICATION_STATUS.UPDATING', () => {
mountComponent({ status: APPLICATION_STATUS.UPDATING });
expect(button().exists()).toBe(true);
expect(button().text()).toContain('Updating');
});
it('clicking update button emits event', () => {
const spy = jest.spyOn(eventHub, '$emit');
mountComponent({
status: APPLICATION_STATUS.INSTALLED,
updateAvailable: true,
});
button().vm.$emit('click');
expect(spy).toHaveBeenCalledWith('updateApplication', {
id: DEFAULT_APPLICATION_STATE.id,
params: {},
});
});
it('clicking disabled update button emits nothing', () => {
const spy = jest.spyOn(eventHub, '$emit');
mountComponent({ status: APPLICATION_STATUS.UPDATING });
button().vm.$emit('click');
expect(spy).not.toHaveBeenCalled();
});
it('displays an error message if application update failed', () => {
mountComponent({
title: 'GitLab Runner',
status: APPLICATION_STATUS.INSTALLED,
updateFailed: true,
});
const failureMessage = wrapper.find('.js-cluster-application-update-details');
expect(failureMessage.exists()).toBe(true);
expect(failureMessage.text()).toContain(
'Update failed. Please check the logs and try again.',
);
});
it('displays a success toast message if application update was successful', async () => {
mountComponent({
title: 'GitLab Runner',
updateSuccessful: false,
});
wrapper.vm.$toast = { show: jest.fn() };
wrapper.setProps({ updateSuccessful: true });
await wrapper.vm.$nextTick();
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('GitLab Runner updated successfully.');
});
describe('when updating does not require confirmation', () => {
beforeEach(() => mountComponent({ updateAvailable: true }));
it('the modal is not rendered', () => {
expect(wrapper.find(UpdateApplicationConfirmationModal).exists()).toBe(false);
});
it('the correct button is rendered', () => {
expect(wrapper.find("[data-qa-selector='update_button']").exists()).toBe(true);
});
});
describe('when updating requires confirmation', () => {
beforeEach(() => {
mountComponent({
updateAvailable: true,
id: ELASTIC_STACK,
version: '1.1.2',
});
});
it('displays a modal', () => {
expect(wrapper.find(UpdateApplicationConfirmationModal).exists()).toBe(true);
});
it('the correct button is rendered', () => {
expect(wrapper.find("[data-qa-selector='update_button_with_confirmation']").exists()).toBe(
true,
);
});
it('triggers updateApplication event', () => {
jest.spyOn(eventHub, '$emit');
wrapper.find(UpdateApplicationConfirmationModal).vm.$emit('confirm');
expect(eventHub.$emit).toHaveBeenCalledWith('updateApplication', {
id: ELASTIC_STACK,
params: {},
});
});
});
describe('updating Elastic Stack special case', () => {
it('needs confirmation if version is lower than 3.0.0', () => {
mountComponent({
updateAvailable: true,
id: ELASTIC_STACK,
version: '1.1.2',
});
expect(wrapper.find("[data-qa-selector='update_button_with_confirmation']").exists()).toBe(
true,
);
expect(wrapper.find(UpdateApplicationConfirmationModal).exists()).toBe(true);
});
it('does not need confirmation is version is 3.0.0', () => {
mountComponent({
updateAvailable: true,
id: ELASTIC_STACK,
version: '3.0.0',
});
expect(wrapper.find("[data-qa-selector='update_button']").exists()).toBe(true);
expect(wrapper.find(UpdateApplicationConfirmationModal).exists()).toBe(false);
});
it('does not need confirmation if version is higher than 3.0.0', () => {
mountComponent({
updateAvailable: true,
id: ELASTIC_STACK,
version: '5.2.1',
});
expect(wrapper.find("[data-qa-selector='update_button']").exists()).toBe(true);
expect(wrapper.find(UpdateApplicationConfirmationModal).exists()).toBe(false);
});
});
});
describe('Version', () => {
const updateDetails = () => wrapper.find('.js-cluster-application-update-details');
const versionEl = () => wrapper.find('.js-cluster-application-update-version');
it('displays a version number if application has been updated', () => {
const version = '0.1.45';
mountComponent({
status: APPLICATION_STATUS.INSTALLED,
updateSuccessful: true,
version,
});
expect(updateDetails().text()).toBe(`Updated to chart v${version}`);
});
it('contains a link to the chart repo if application has been updated', () => {
const version = '0.1.45';
const chartRepo = 'https://gitlab.com/gitlab-org/charts/gitlab-runner';
mountComponent({
status: APPLICATION_STATUS.INSTALLED,
updateSuccessful: true,
chartRepo,
version,
});
expect(versionEl().attributes('href')).toEqual(chartRepo);
expect(versionEl().props('target')).toEqual('_blank');
});
it('does not display a version number if application update failed', () => {
const version = '0.1.45';
mountComponent({
status: APPLICATION_STATUS.INSTALLED,
updateFailed: true,
version,
});
expect(updateDetails().text()).toBe('Update failed');
expect(versionEl().exists()).toBe(false);
});
it('displays updating when the application update is currently updating', () => {
mountComponent({
status: APPLICATION_STATUS.UPDATING,
updateSuccessful: true,
version: '1.2.3',
});
expect(updateDetails().text()).toBe('Updating');
expect(versionEl().exists()).toBe(false);
});
});
describe('Error block', () => {
const generalErrorMessage = () => wrapper.find('.js-cluster-application-general-error-message');
describe('when nothing fails', () => {
it('does not show error block', () => {
mountComponent();
expect(generalErrorMessage().exists()).toBe(false);
});
});
describe('when install or uninstall fails', () => {
const statusReason = 'We broke it 0.0';
const requestReason = 'We broke the request 0.0';
beforeEach(() => {
mountComponent({
status: APPLICATION_STATUS.ERROR,
statusReason,
requestReason,
installFailed: true,
});
});
it('shows status reason if it is available', () => {
const statusErrorMessage = wrapper.find('.js-cluster-application-status-error-message');
expect(statusErrorMessage.text()).toEqual(statusReason);
});
it('shows request reason if it is available', () => {
const requestErrorMessage = wrapper.find('.js-cluster-application-request-error-message');
expect(requestErrorMessage.text()).toEqual(requestReason);
});
});
describe('when install fails', () => {
beforeEach(() => {
mountComponent({
status: APPLICATION_STATUS.ERROR,
installFailed: true,
});
});
it('shows a general message indicating the installation failed', () => {
expect(generalErrorMessage().text()).toEqual(
`Something went wrong while installing ${DEFAULT_APPLICATION_STATE.title}`,
);
});
});
describe('when uninstall fails', () => {
beforeEach(() => {
mountComponent({
status: APPLICATION_STATUS.ERROR,
uninstallFailed: true,
});
});
it('shows a general message indicating the uninstalling failed', () => {
expect(generalErrorMessage().text()).toEqual(
`Something went wrong while uninstalling ${DEFAULT_APPLICATION_STATE.title}`,
);
});
});
});
});
import { shallowMount, mount } from '@vue/test-utils';
import ApplicationRow from '~/clusters/components/application_row.vue';
import Applications from '~/clusters/components/applications.vue';
import CrossplaneProviderStack from '~/clusters/components/crossplane_provider_stack.vue';
import KnativeDomainEditor from '~/clusters/components/knative_domain_editor.vue';
import { CLUSTER_TYPE, PROVIDER_TYPE } from '~/clusters/constants';
import eventHub from '~/clusters/event_hub';
import { APPLICATIONS_MOCK_STATE } from '../services/mock_data';
describe('Applications', () => {
let wrapper;
beforeEach(() => {
gon.features = gon.features || {};
});
const createComponent = ({ applications, type, propsData } = {}, isShallow) => {
const mountMethod = isShallow ? shallowMount : mount;
wrapper = mountMethod(Applications, {
stubs: { ApplicationRow },
propsData: {
type,
applications: { ...APPLICATIONS_MOCK_STATE, ...applications },
...propsData,
},
});
};
const createShallowComponent = (options) => createComponent(options, true);
const findByTestId = (id) => wrapper.find(`[data-testid="${id}"]`);
afterEach(() => {
wrapper.destroy();
});
describe('Project cluster applications', () => {
beforeEach(() => {
createComponent({ type: CLUSTER_TYPE.PROJECT });
});
it('renders a row for Ingress', () => {
expect(wrapper.find('.js-cluster-application-row-ingress').exists()).toBe(true);
});
it('renders a row for Cert-Manager', () => {
expect(wrapper.find('.js-cluster-application-row-cert_manager').exists()).toBe(true);
});
it('renders a row for Crossplane', () => {
expect(wrapper.find('.js-cluster-application-row-crossplane').exists()).toBe(true);
});
it('renders a row for Prometheus', () => {
expect(wrapper.find('.js-cluster-application-row-prometheus').exists()).toBe(true);
});
it('renders a row for GitLab Runner', () => {
expect(wrapper.find('.js-cluster-application-row-runner').exists()).toBe(true);
});
it('renders a row for Jupyter', () => {
expect(wrapper.find('.js-cluster-application-row-jupyter').exists()).toBe(true);
});
it('renders a row for Knative', () => {
expect(wrapper.find('.js-cluster-application-row-knative').exists()).toBe(true);
});
it('renders a row for Elastic Stack', () => {
expect(wrapper.find('.js-cluster-application-row-elastic_stack').exists()).toBe(true);
});
it('renders a row for Cilium', () => {
expect(wrapper.find('.js-cluster-application-row-cilium').exists()).toBe(true);
});
});
describe('Group cluster applications', () => {
beforeEach(() => {
createComponent({ type: CLUSTER_TYPE.GROUP });
});
it('renders a row for Ingress', () => {
expect(wrapper.find('.js-cluster-application-row-ingress').exists()).toBe(true);
});
it('renders a row for Cert-Manager', () => {
expect(wrapper.find('.js-cluster-application-row-cert_manager').exists()).toBe(true);
});
it('renders a row for Crossplane', () => {
expect(wrapper.find('.js-cluster-application-row-crossplane').exists()).toBe(true);
});
it('renders a row for Prometheus', () => {
expect(wrapper.find('.js-cluster-application-row-prometheus').exists()).toBe(true);
});
it('renders a row for GitLab Runner', () => {
expect(wrapper.find('.js-cluster-application-row-runner').exists()).toBe(true);
});
it('renders a row for Jupyter', () => {
expect(wrapper.find('.js-cluster-application-row-jupyter').exists()).toBe(true);
});
it('renders a row for Knative', () => {
expect(wrapper.find('.js-cluster-application-row-knative').exists()).toBe(true);
});
it('renders a row for Elastic Stack', () => {
expect(wrapper.find('.js-cluster-application-row-elastic_stack').exists()).toBe(true);
});
it('renders a row for Cilium', () => {
expect(wrapper.find('.js-cluster-application-row-cilium').exists()).toBe(true);
});
});
describe('Instance cluster applications', () => {
beforeEach(() => {
createComponent({ type: CLUSTER_TYPE.INSTANCE });
});
it('renders a row for Ingress', () => {
expect(wrapper.find('.js-cluster-application-row-ingress').exists()).toBe(true);
});
it('renders a row for Cert-Manager', () => {
expect(wrapper.find('.js-cluster-application-row-cert_manager').exists()).toBe(true);
});
it('renders a row for Crossplane', () => {
expect(wrapper.find('.js-cluster-application-row-crossplane').exists()).toBe(true);
});
it('renders a row for Prometheus', () => {
expect(wrapper.find('.js-cluster-application-row-prometheus').exists()).toBe(true);
});
it('renders a row for GitLab Runner', () => {
expect(wrapper.find('.js-cluster-application-row-runner').exists()).toBe(true);
});
it('renders a row for Jupyter', () => {
expect(wrapper.find('.js-cluster-application-row-jupyter').exists()).toBe(true);
});
it('renders a row for Knative', () => {
expect(wrapper.find('.js-cluster-application-row-knative').exists()).toBe(true);
});
it('renders a row for Elastic Stack', () => {
expect(wrapper.find('.js-cluster-application-row-elastic_stack').exists()).toBe(true);
});
it('renders a row for Cilium', () => {
expect(wrapper.find('.js-cluster-application-row-cilium').exists()).toBe(true);
});
});
describe('Helm application', () => {
it('does not render a row for Helm Tiller', () => {
createComponent();
expect(wrapper.find('.js-cluster-application-row-helm').exists()).toBe(false);
});
});
describe('Ingress application', () => {
it('shows the correct warning message', () => {
createComponent();
expect(findByTestId('ingressCostWarning').element).toMatchSnapshot();
});
describe('when installed', () => {
describe('with ip address', () => {
it('renders ip address with a clipboard button', () => {
createComponent({
applications: {
ingress: {
title: 'Ingress',
status: 'installed',
externalIp: '0.0.0.0',
},
},
});
expect(wrapper.find('.js-endpoint').element.value).toEqual('0.0.0.0');
expect(wrapper.find('.js-clipboard-btn').attributes('data-clipboard-text')).toEqual(
'0.0.0.0',
);
});
});
describe('with hostname', () => {
it('renders hostname with a clipboard button', () => {
createComponent({
applications: {
ingress: {
title: 'Ingress',
status: 'installed',
externalHostname: 'localhost.localdomain',
},
cert_manager: { title: 'Cert-Manager' },
crossplane: { title: 'Crossplane', stack: '' },
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub', hostname: '' },
knative: { title: 'Knative', hostname: '' },
elastic_stack: { title: 'Elastic Stack' },
cilium: { title: 'GitLab Container Network Policies' },
},
});
expect(wrapper.find('.js-endpoint').element.value).toEqual('localhost.localdomain');
expect(wrapper.find('.js-clipboard-btn').attributes('data-clipboard-text')).toEqual(
'localhost.localdomain',
);
});
});
describe('without ip address', () => {
it('renders an input text with a loading icon and an alert text', () => {
createComponent({
applications: {
ingress: {
title: 'Ingress',
status: 'installed',
},
},
});
expect(wrapper.find('.js-ingress-ip-loading-icon').exists()).toBe(true);
expect(wrapper.find('.js-no-endpoint-message').exists()).toBe(true);
});
});
});
describe('before installing', () => {
it('does not render the IP address', () => {
createComponent();
expect(wrapper.text()).not.toContain('Ingress IP Address');
expect(wrapper.find('.js-endpoint').exists()).toBe(false);
});
});
});
describe('Cert-Manager application', () => {
it('shows the correct description', () => {
createComponent();
expect(findByTestId('certManagerDescription').element).toMatchSnapshot();
});
describe('when not installed', () => {
it('renders email & allows editing', () => {
createComponent({
applications: {
cert_manager: {
title: 'Cert-Manager',
email: 'before@example.com',
status: 'installable',
},
},
});
expect(wrapper.find('.js-email').element.value).toEqual('before@example.com');
expect(wrapper.find('.js-email').attributes('readonly')).toBe(undefined);
});
});
describe('when installed', () => {
it('renders email in readonly', () => {
createComponent({
applications: {
cert_manager: {
title: 'Cert-Manager',
email: 'after@example.com',
status: 'installed',
},
},
});
expect(wrapper.find('.js-email').element.value).toEqual('after@example.com');
expect(wrapper.find('.js-email').attributes('readonly')).toEqual('readonly');
});
});
});
describe('Jupyter application', () => {
describe('with ingress installed with ip & jupyter installable', () => {
it('renders hostname active input', () => {
createComponent({
applications: {
ingress: {
title: 'Ingress',
status: 'installed',
externalIp: '1.1.1.1',
},
},
});
expect(
wrapper.find('.js-cluster-application-row-jupyter .js-hostname').attributes('readonly'),
).toEqual(undefined);
});
});
describe('with ingress installed without external ip', () => {
it('does not render hostname input', () => {
createComponent({
applications: {
ingress: { title: 'Ingress', status: 'installed' },
},
});
expect(wrapper.find('.js-cluster-application-row-jupyter .js-hostname').exists()).toBe(
false,
);
});
});
describe('with ingress & jupyter installed', () => {
it('renders readonly input', () => {
createComponent({
applications: {
ingress: {
title: 'Ingress',
status: 'installed',
externalIp: '1.1.1.1',
},
jupyter: { title: 'JupyterHub', status: 'installed', hostname: '' },
},
});
expect(
wrapper.find('.js-cluster-application-row-jupyter .js-hostname').attributes('readonly'),
).toEqual('readonly');
});
});
describe('without ingress installed', () => {
beforeEach(() => {
createComponent();
});
it('does not render input', () => {
expect(wrapper.find('.js-cluster-application-row-jupyter .js-hostname').exists()).toBe(
false,
);
});
});
});
describe('Prometheus application', () => {
it('shows the correct description', () => {
createComponent();
expect(findByTestId('prometheusDescription').element).toMatchSnapshot();
});
});
describe('Knative application', () => {
const availableDomain = {
id: 4,
domain: 'newhostname.com',
};
const propsData = {
applications: {
knative: {
title: 'Knative',
hostname: 'example.com',
status: 'installed',
externalIp: '1.1.1.1',
installed: true,
availableDomains: [availableDomain],
pagesDomain: null,
},
},
};
let knativeDomainEditor;
beforeEach(() => {
createShallowComponent(propsData);
jest.spyOn(eventHub, '$emit');
knativeDomainEditor = wrapper.find(KnativeDomainEditor);
});
it('shows the correct description', async () => {
createComponent();
wrapper.setProps({
providerType: PROVIDER_TYPE.GCP,
preInstalledKnative: true,
});
await wrapper.vm.$nextTick();
expect(findByTestId('installed-via').element).toMatchSnapshot();
});
it('emits saveKnativeDomain event when knative domain editor emits save event', () => {
propsData.applications.knative.hostname = availableDomain.domain;
propsData.applications.knative.pagesDomain = availableDomain;
knativeDomainEditor.vm.$emit('save');
expect(eventHub.$emit).toHaveBeenCalledWith('saveKnativeDomain', {
id: 'knative',
params: {
hostname: availableDomain.domain,
pages_domain_id: availableDomain.id,
},
});
});
it('emits saveKnativeDomain event when knative domain editor emits save event with custom domain', () => {
const newHostName = 'someothernewhostname.com';
propsData.applications.knative.hostname = newHostName;
propsData.applications.knative.pagesDomain = null;
knativeDomainEditor.vm.$emit('save');
expect(eventHub.$emit).toHaveBeenCalledWith('saveKnativeDomain', {
id: 'knative',
params: {
hostname: newHostName,
pages_domain_id: undefined,
},
});
});
it('emits setKnativeHostname event when knative domain editor emits change event', () => {
wrapper.find(KnativeDomainEditor).vm.$emit('set', {
domain: availableDomain.domain,
domainId: availableDomain.id,
});
expect(eventHub.$emit).toHaveBeenCalledWith('setKnativeDomain', {
id: 'knative',
domain: availableDomain.domain,
domainId: availableDomain.id,
});
});
});
describe('Crossplane application', () => {
const propsData = {
applications: {
crossplane: {
title: 'Crossplane',
stack: {
code: '',
},
},
},
};
beforeEach(() => createShallowComponent(propsData));
it('renders the correct Component', () => {
const crossplane = wrapper.find(CrossplaneProviderStack);
expect(crossplane.exists()).toBe(true);
});
it('shows the correct description', () => {
createComponent();
expect(findByTestId('crossplaneDescription').element).toMatchSnapshot();
});
});
describe('Elastic Stack application', () => {
describe('with elastic stack installable', () => {
it('renders the install button enabled', () => {
createComponent();
expect(
wrapper
.find(
'.js-cluster-application-row-elastic_stack .js-cluster-application-install-button',
)
.attributes('disabled'),
).toBeUndefined();
});
});
describe('elastic stack installed', () => {
it('renders uninstall button', () => {
createComponent({
applications: {
elastic_stack: { title: 'Elastic Stack', status: 'installed' },
},
});
expect(
wrapper
.find(
'.js-cluster-application-row-elastic_stack .js-cluster-application-install-button',
)
.attributes('disabled'),
).toEqual('disabled');
});
});
});
describe('Cilium application', () => {
it('shows the correct description', () => {
createComponent({ propsData: { ciliumHelpPath: 'cilium-help-path' } });
expect(findByTestId('ciliumDescription').element).toMatchSnapshot();
});
});
});
import { GlDropdownItem, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import KnativeDomainEditor from '~/clusters/components/knative_domain_editor.vue';
import { APPLICATION_STATUS } from '~/clusters/constants';
const { UPDATING } = APPLICATION_STATUS;
describe('KnativeDomainEditor', () => {
let wrapper;
let knative;
const createComponent = (props = {}) => {
wrapper = shallowMount(KnativeDomainEditor, {
propsData: { ...props },
});
};
beforeEach(() => {
knative = {
title: 'Knative',
hostname: 'example.com',
installed: true,
};
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('knative has an assigned IP address', () => {
beforeEach(() => {
knative.externalIp = '1.1.1.1';
createComponent({ knative });
});
it('renders ip address with a clipboard button', () => {
expect(wrapper.find('.js-knative-endpoint').exists()).toBe(true);
expect(wrapper.find('.js-knative-endpoint').element.value).toEqual(knative.externalIp);
});
it('displays ip address clipboard button', () => {
expect(wrapper.find('.js-knative-endpoint-clipboard-btn').attributes('text')).toEqual(
knative.externalIp,
);
});
it('renders domain & allows editing', () => {
const domainNameInput = wrapper.find('.js-knative-domainname');
expect(domainNameInput.element.value).toEqual(knative.hostname);
expect(domainNameInput.attributes('readonly')).toBeFalsy();
});
it('renders an update/save Knative domain button', () => {
expect(wrapper.find('.js-knative-save-domain-button').exists()).toBe(true);
});
});
describe('knative without ip address', () => {
beforeEach(() => {
knative.externalIp = null;
createComponent({ knative });
});
it('renders an input text with a loading icon', () => {
expect(wrapper.find('.js-knative-ip-loading-icon').exists()).toBe(true);
});
it('renders message indicating there is not IP address assigned', () => {
expect(wrapper.find('.js-no-knative-endpoint-message').exists()).toBe(true);
});
});
describe('clicking save changes button', () => {
beforeEach(() => {
createComponent({ knative });
});
it('triggers save event and pass current knative hostname', () => {
wrapper.find(GlButton).vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted('save').length).toEqual(1);
});
});
});
describe('when knative domain name was saved successfully', () => {
beforeEach(() => {
createComponent({ knative });
});
it('displays toast indicating a successful update', () => {
wrapper.vm.$toast = { show: jest.fn() };
wrapper.setProps({ knative: { updateSuccessful: true, ...knative } });
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(
'Knative domain name was updated successfully.',
);
});
});
});
describe('when knative domain name input changes', () => {
it('emits "set" event with updated domain name', () => {
const newDomain = {
id: 4,
domain: 'newhostname.com',
};
createComponent({ knative: { ...knative, availableDomains: [newDomain] } });
jest.spyOn(wrapper.vm, 'selectDomain');
wrapper.find(GlDropdownItem).vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.selectDomain).toHaveBeenCalledWith(newDomain);
expect(wrapper.emitted('set')[0]).toEqual([
{
domain: newDomain.domain,
domainId: newDomain.id,
},
]);
});
});
it('emits "set" event with updated custom domain name', () => {
const newHostname = 'newhostname.com';
createComponent({ knative });
jest.spyOn(wrapper.vm, 'selectCustomDomain');
wrapper.setData({ knativeHostname: newHostname });
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.selectCustomDomain).toHaveBeenCalledWith(newHostname);
expect(wrapper.emitted('set')[0]).toEqual([
{
domain: newHostname,
domainId: null,
},
]);
});
});
});
describe('when updating knative domain name failed', () => {
beforeEach(() => {
createComponent({ knative });
});
it('displays an error banner indicating the operation failure', () => {
wrapper.setProps({ knative: { updateFailed: true, ...knative } });
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find('.js-cluster-knative-domain-name-failure-message').exists()).toBe(true);
});
});
});
describe(`when knative status is ${UPDATING}`, () => {
beforeEach(() => {
createComponent({ knative: { status: UPDATING, ...knative } });
});
it('renders loading spinner in save button', () => {
expect(wrapper.find(GlButton).props('loading')).toBe(true);
});
it('renders disabled save button', () => {
expect(wrapper.find(GlButton).props('disabled')).toBe(true);
});
it('renders save button with "Saving" label', () => {
expect(wrapper.find(GlButton).text()).toBe('Saving');
});
});
});
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import UninstallApplicationButton from '~/clusters/components/uninstall_application_button.vue';
import { APPLICATION_STATUS } from '~/clusters/constants';
const { INSTALLED, UPDATING, UNINSTALLING } = APPLICATION_STATUS;
describe('UninstallApplicationButton', () => {
let wrapper;
const createComponent = (props = {}) => {
wrapper = shallowMount(UninstallApplicationButton, {
propsData: { ...props },
});
};
afterEach(() => {
wrapper.destroy();
});
describe.each`
status | loading | disabled | text
${INSTALLED} | ${false} | ${false} | ${'Uninstall'}
${UPDATING} | ${false} | ${true} | ${'Uninstall'}
${UNINSTALLING} | ${true} | ${true} | ${'Uninstalling'}
`('when app status is $status', ({ loading, disabled, status, text }) => {
beforeEach(() => {
createComponent({ status });
});
it(`renders a button with loading=${loading} and disabled=${disabled}`, () => {
expect(wrapper.find(GlButton).props()).toMatchObject({ loading, disabled });
});
it(`renders a button with text="${text}"`, () => {
expect(wrapper.find(GlButton).text()).toBe(text);
});
});
});
import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import UninstallApplicationConfirmationModal from '~/clusters/components/uninstall_application_confirmation_modal.vue';
import { INGRESS } from '~/clusters/constants';
describe('UninstallApplicationConfirmationModal', () => {
let wrapper;
const appTitle = 'Ingress';
const createComponent = (props = {}) => {
wrapper = shallowMount(UninstallApplicationConfirmationModal, {
propsData: { ...props },
});
};
afterEach(() => {
wrapper.destroy();
});
beforeEach(() => {
createComponent({ application: INGRESS, applicationTitle: appTitle });
});
it(`renders a modal with a title "Uninstall ${appTitle}"`, () => {
expect(wrapper.find(GlModal).attributes('title')).toEqual(`Uninstall ${appTitle}`);
});
it(`renders a modal with an ok button labeled "Uninstall ${appTitle}"`, () => {
expect(wrapper.find(GlModal).attributes('ok-title')).toEqual(`Uninstall ${appTitle}`);
});
describe('when ok button is clicked', () => {
beforeEach(() => {
jest.spyOn(wrapper.vm, 'trackUninstallButtonClick');
wrapper.find(GlModal).vm.$emit('ok');
});
it('emits confirm event', () =>
wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted('confirm')).toBeTruthy();
}));
it('calls track uninstall button click mixin', () => {
expect(wrapper.vm.trackUninstallButtonClick).toHaveBeenCalledWith(INGRESS);
});
});
it('displays a warning text indicating the app will be uninstalled', () => {
expect(wrapper.text()).toContain(`You are about to uninstall ${appTitle} from your cluster.`);
});
it('displays a custom warning text depending on the application', () => {
expect(wrapper.text()).toContain(
`The associated load balancer and IP will be deleted and cannot be restored.`,
);
});
});
import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import UpdateApplicationConfirmationModal from '~/clusters/components/update_application_confirmation_modal.vue';
import { ELASTIC_STACK } from '~/clusters/constants';
describe('UpdateApplicationConfirmationModal', () => {
let wrapper;
const appTitle = 'Elastic stack';
const createComponent = (props = {}) => {
wrapper = shallowMount(UpdateApplicationConfirmationModal, {
propsData: { ...props },
});
};
afterEach(() => {
wrapper.destroy();
});
beforeEach(() => {
createComponent({ application: ELASTIC_STACK, applicationTitle: appTitle });
});
it(`renders a modal with a title "Update ${appTitle}"`, () => {
expect(wrapper.find(GlModal).attributes('title')).toEqual(`Update ${appTitle}`);
});
it(`renders a modal with an ok button labeled "Update ${appTitle}"`, () => {
expect(wrapper.find(GlModal).attributes('ok-title')).toEqual(`Update ${appTitle}`);
});
describe('when ok button is clicked', () => {
beforeEach(() => {
wrapper.find(GlModal).vm.$emit('ok');
});
it('emits confirm event', () =>
wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted('confirm')).toBeTruthy();
}));
it('displays a warning text indicating the app will be updated', () => {
expect(wrapper.text()).toContain(`You are about to update ${appTitle} on your cluster.`);
});
it('displays a custom warning text depending on the application', () => {
expect(wrapper.text()).toContain(
`Your Elasticsearch cluster will be re-created during this upgrade. Your logs will be re-indexed, and you will lose historical logs from hosts terminated in the last 30 days.`,
);
});
});
});
import {
APPLICATION_STATUS,
UNINSTALL_EVENT,
UPDATE_EVENT,
INSTALL_EVENT,
} from '~/clusters/constants';
import transitionApplicationState from '~/clusters/services/application_state_machine';
const {
NO_STATUS,
SCHEDULED,
NOT_INSTALLABLE,
INSTALLABLE,
INSTALLING,
INSTALLED,
ERROR,
UPDATING,
UPDATED,
UPDATE_ERRORED,
UNINSTALLING,
UNINSTALL_ERRORED,
UNINSTALLED,
PRE_INSTALLED,
EXTERNALLY_INSTALLED,
} = APPLICATION_STATUS;
const NO_EFFECTS = 'no effects';
describe('applicationStateMachine', () => {
const noEffectsToEmptyObject = (effects) => (typeof effects === 'string' ? {} : effects);
describe(`current state is ${NO_STATUS}`, () => {
it.each`
expectedState | event | effects
${INSTALLING} | ${SCHEDULED} | ${NO_EFFECTS}
${NOT_INSTALLABLE} | ${NOT_INSTALLABLE} | ${NO_EFFECTS}
${INSTALLABLE} | ${INSTALLABLE} | ${NO_EFFECTS}
${INSTALLING} | ${INSTALLING} | ${NO_EFFECTS}
${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS}
${INSTALLABLE} | ${ERROR} | ${{ installFailed: true }}
${UPDATING} | ${UPDATING} | ${NO_EFFECTS}
${INSTALLED} | ${UPDATED} | ${NO_EFFECTS}
${INSTALLED} | ${UPDATE_ERRORED} | ${{ updateFailed: true }}
${UNINSTALLING} | ${UNINSTALLING} | ${NO_EFFECTS}
${INSTALLED} | ${UNINSTALL_ERRORED} | ${{ uninstallFailed: true }}
${UNINSTALLED} | ${UNINSTALLED} | ${NO_EFFECTS}
${PRE_INSTALLED} | ${PRE_INSTALLED} | ${NO_EFFECTS}
${EXTERNALLY_INSTALLED} | ${EXTERNALLY_INSTALLED} | ${NO_EFFECTS}
`(`transitions to $expectedState on $event event and applies $effects`, (data) => {
const { expectedState, event, effects } = data;
const currentAppState = {
status: NO_STATUS,
};
expect(transitionApplicationState(currentAppState, event)).toEqual({
status: expectedState,
...noEffectsToEmptyObject(effects),
});
});
});
describe(`current state is ${NOT_INSTALLABLE}`, () => {
it.each`
expectedState | event | effects
${INSTALLABLE} | ${INSTALLABLE} | ${NO_EFFECTS}
`(`transitions to $expectedState on $event event and applies $effects`, (data) => {
const { expectedState, event, effects } = data;
const currentAppState = {
status: NOT_INSTALLABLE,
};
expect(transitionApplicationState(currentAppState, event)).toEqual({
status: expectedState,
...noEffectsToEmptyObject(effects),
});
});
});
describe(`current state is ${INSTALLABLE}`, () => {
it.each`
expectedState | event | effects
${INSTALLING} | ${INSTALL_EVENT} | ${{ installFailed: false }}
${INSTALLED} | ${INSTALLED} | ${{ installFailed: false }}
${NOT_INSTALLABLE} | ${NOT_INSTALLABLE} | ${NO_EFFECTS}
${UNINSTALLED} | ${UNINSTALLED} | ${{ installFailed: false }}
`(`transitions to $expectedState on $event event and applies $effects`, (data) => {
const { expectedState, event, effects } = data;
const currentAppState = {
status: INSTALLABLE,
};
expect(transitionApplicationState(currentAppState, event)).toEqual({
status: expectedState,
...noEffectsToEmptyObject(effects),
});
});
});
describe(`current state is ${INSTALLING}`, () => {
it.each`
expectedState | event | effects
${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS}
${INSTALLABLE} | ${ERROR} | ${{ installFailed: true }}
`(`transitions to $expectedState on $event event and applies $effects`, (data) => {
const { expectedState, event, effects } = data;
const currentAppState = {
status: INSTALLING,
};
expect(transitionApplicationState(currentAppState, event)).toEqual({
status: expectedState,
...noEffectsToEmptyObject(effects),
});
});
});
describe(`current state is ${INSTALLED}`, () => {
it.each`
expectedState | event | effects
${UPDATING} | ${UPDATE_EVENT} | ${{ updateFailed: false, updateSuccessful: false }}
${UNINSTALLING} | ${UNINSTALL_EVENT} | ${{ uninstallFailed: false, uninstallSuccessful: false }}
${NOT_INSTALLABLE} | ${NOT_INSTALLABLE} | ${NO_EFFECTS}
${UNINSTALLED} | ${UNINSTALLED} | ${NO_EFFECTS}
${INSTALLABLE} | ${ERROR} | ${{ installFailed: true }}
`(`transitions to $expectedState on $event event and applies $effects`, (data) => {
const { expectedState, event, effects } = data;
const currentAppState = {
status: INSTALLED,
};
expect(transitionApplicationState(currentAppState, event)).toEqual({
status: expectedState,
...noEffectsToEmptyObject(effects),
});
});
});
describe(`current state is ${UPDATING}`, () => {
it.each`
expectedState | event | effects
${INSTALLED} | ${UPDATED} | ${{ updateSuccessful: true }}
${INSTALLED} | ${UPDATE_ERRORED} | ${{ updateFailed: true }}
`(`transitions to $expectedState on $event event and applies $effects`, (data) => {
const { expectedState, event, effects } = data;
const currentAppState = {
status: UPDATING,
};
expect(transitionApplicationState(currentAppState, event)).toEqual({
status: expectedState,
...effects,
});
});
});
describe(`current state is ${UNINSTALLING}`, () => {
it.each`
expectedState | event | effects
${INSTALLABLE} | ${INSTALLABLE} | ${{ uninstallSuccessful: true }}
${INSTALLED} | ${UNINSTALL_ERRORED} | ${{ uninstallFailed: true }}
`(`transitions to $expectedState on $event event and applies $effects`, (data) => {
const { expectedState, event, effects } = data;
const currentAppState = {
status: UNINSTALLING,
};
expect(transitionApplicationState(currentAppState, event)).toEqual({
status: expectedState,
...effects,
});
});
});
describe(`current state is ${UNINSTALLED}`, () => {
it.each`
expectedState | event | effects
${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS}
${INSTALLABLE} | ${ERROR} | ${{ installFailed: true }}
`(`transitions to $expectedState on $event event and applies $effects`, (data) => {
const { expectedState, event, effects } = data;
const currentAppState = {
status: UNINSTALLED,
};
expect(transitionApplicationState(currentAppState, event)).toEqual({
status: expectedState,
...noEffectsToEmptyObject(effects),
});
});
});
describe('current state is undefined', () => {
it('returns the current state without having any effects', () => {
const currentAppState = {};
expect(transitionApplicationState(currentAppState, INSTALLABLE)).toEqual(currentAppState);
});
});
describe('with event is undefined', () => {
it('returns the current state without having any effects', () => {
const currentAppState = {
status: NO_STATUS,
};
expect(transitionApplicationState(currentAppState, undefined)).toEqual(currentAppState);
});
});
});
import { GlDropdownItem, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import CrossplaneProviderStack from '~/clusters/components/crossplane_provider_stack.vue';
describe('CrossplaneProviderStack component', () => {
let wrapper;
const defaultProps = {
stacks: [
{
name: 'Google Cloud Platform',
code: 'gcp',
},
{
name: 'Amazon Web Services',
code: 'aws',
},
],
};
function createComponent(props = {}) {
const propsData = {
...defaultProps,
...props,
};
wrapper = shallowMount(CrossplaneProviderStack, {
propsData,
});
}
beforeEach(() => {
const crossplane = {
title: 'crossplane',
stack: '',
};
createComponent({ crossplane });
});
const findDropdownElements = () => wrapper.findAll(GlDropdownItem);
const findFirstDropdownElement = () => findDropdownElements().at(0);
afterEach(() => {
wrapper.destroy();
});
it('renders all of the available stacks in the dropdown', () => {
const dropdownElements = findDropdownElements();
expect(dropdownElements.length).toBe(defaultProps.stacks.length);
defaultProps.stacks.forEach((stack, index) =>
expect(dropdownElements.at(index).text()).toEqual(stack.name),
);
});
it('displays the correct label for the first dropdown item if a stack is selected', () => {
const crossplane = {
title: 'crossplane',
stack: 'gcp',
};
createComponent({ crossplane });
expect(wrapper.vm.dropdownText).toBe('Google Cloud Platform');
});
it('emits the "set" event with the selected stack value', () => {
const crossplane = {
title: 'crossplane',
stack: 'gcp',
};
createComponent({ crossplane });
findFirstDropdownElement().vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted().set[0][0].code).toEqual('gcp');
});
});
it('renders the correct dropdown text when no stack is selected', () => {
expect(wrapper.vm.dropdownText).toBe('Select Stack');
});
it('renders an external link', () => {
expect(wrapper.find(GlIcon).props('name')).toBe('external-link');
});
});
import { APPLICATION_STATUS } from '~/clusters/constants';
const CLUSTERS_MOCK_DATA = {
GET: {
'/gitlab-org/gitlab-shell/clusters/1/status.json': {
data: {
status: 'errored',
status_reason: 'Failed to request to CloudPlatform.',
applications: [
{
name: 'helm',
status: APPLICATION_STATUS.INSTALLABLE,
status_reason: null,
can_uninstall: false,
},
{
name: 'ingress',
status: APPLICATION_STATUS.ERROR,
status_reason: 'Cannot connect',
external_ip: null,
external_hostname: null,
can_uninstall: false,
},
{
name: 'runner',
status: APPLICATION_STATUS.INSTALLING,
status_reason: null,
can_uninstall: false,
},
{
name: 'prometheus',
status: APPLICATION_STATUS.ERROR,
status_reason: 'Cannot connect',
can_uninstall: false,
},
{
name: 'jupyter',
status: APPLICATION_STATUS.INSTALLING,
status_reason: 'Cannot connect',
can_uninstall: false,
},
{
name: 'knative',
status: APPLICATION_STATUS.INSTALLING,
status_reason: 'Cannot connect',
can_uninstall: false,
},
{
name: 'cert_manager',
status: APPLICATION_STATUS.ERROR,
status_reason: 'Cannot connect',
email: 'test@example.com',
can_uninstall: false,
},
{
name: 'crossplane',
status: APPLICATION_STATUS.ERROR,
status_reason: 'Cannot connect',
can_uninstall: false,
},
{
name: 'elastic_stack',
status: APPLICATION_STATUS.ERROR,
status_reason: 'Cannot connect',
can_uninstall: false,
},
],
},
},
'/gitlab-org/gitlab-shell/clusters/2/status.json': {
data: {
status: 'errored',
status_reason: 'Failed to request to CloudPlatform.',
applications: [
{
name: 'helm',
status: APPLICATION_STATUS.INSTALLED,
status_reason: null,
},
{
name: 'ingress',
status: APPLICATION_STATUS.INSTALLED,
status_reason: 'Cannot connect',
external_ip: '1.1.1.1',
external_hostname: null,
},
{
name: 'runner',
status: APPLICATION_STATUS.INSTALLING,
status_reason: null,
},
{
name: 'prometheus',
status: APPLICATION_STATUS.ERROR,
status_reason: 'Cannot connect',
},
{
name: 'jupyter',
status: APPLICATION_STATUS.INSTALLABLE,
status_reason: 'Cannot connect',
},
{
name: 'knative',
status: APPLICATION_STATUS.INSTALLABLE,
status_reason: 'Cannot connect',
},
{
name: 'cert_manager',
status: APPLICATION_STATUS.ERROR,
status_reason: 'Cannot connect',
email: 'test@example.com',
},
{
name: 'crossplane',
status: APPLICATION_STATUS.ERROR,
status_reason: 'Cannot connect',
stack: 'gcp',
},
{
name: 'elastic_stack',
status: APPLICATION_STATUS.ERROR,
status_reason: 'Cannot connect',
},
],
},
},
},
POST: {
'/gitlab-org/gitlab-shell/clusters/1/applications/helm': {},
'/gitlab-org/gitlab-shell/clusters/1/applications/ingress': {},
'/gitlab-org/gitlab-shell/clusters/1/applications/crossplane': {},
'/gitlab-org/gitlab-shell/clusters/1/applications/cert_manager': {},
'/gitlab-org/gitlab-shell/clusters/1/applications/runner': {},
'/gitlab-org/gitlab-shell/clusters/1/applications/prometheus': {},
'/gitlab-org/gitlab-shell/clusters/1/applications/jupyter': {},
'/gitlab-org/gitlab-shell/clusters/1/applications/knative': {},
'/gitlab-org/gitlab-shell/clusters/1/applications/elastic_stack': {},
},
};
const DEFAULT_APPLICATION_STATE = {
id: 'some-app',
title: 'My App',
titleLink: 'https://about.gitlab.com/',
description: 'Some description about this interesting application!',
status: null,
statusReason: null,
requestReason: null,
};
const APPLICATIONS_MOCK_STATE = {
helm: { title: 'Helm Tiller', status: 'installable' },
ingress: {
title: 'Ingress',
status: 'installable',
},
crossplane: { title: 'Crossplane', status: 'installable', stack: '' },
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: '' },
elastic_stack: { title: 'Elastic Stack', status: 'installable' },
cilium: {
title: 'GitLab Container Network Policies',
status: 'not_installable',
},
POST: {},
};
export { CLUSTERS_MOCK_DATA, DEFAULT_APPLICATION_STATE, APPLICATIONS_MOCK_STATE };
export { CLUSTERS_MOCK_DATA };
import { APPLICATION_INSTALLED_STATUSES, APPLICATION_STATUS, RUNNER } from '~/clusters/constants';
import ClustersStore from '~/clusters/stores/clusters_store';
import { CLUSTERS_MOCK_DATA } from '../services/mock_data';
......@@ -31,17 +30,6 @@ describe('Clusters Store', () => {
});
});
describe('updateAppProperty', () => {
it('should store new request reason', () => {
expect(store.state.applications.helm.requestReason).toEqual(null);
const newReason = 'We broke it.';
store.updateAppProperty('helm', 'requestReason', newReason);
expect(store.state.applications.helm.requestReason).toEqual(newReason);
});
});
describe('updateStateFromServer', () => {
it('should store new polling data from server', () => {
const mockResponseData =
......@@ -50,196 +38,16 @@ describe('Clusters Store', () => {
expect(store.state).toEqual({
helpPath: null,
helmHelpPath: null,
ingressHelpPath: null,
environmentsHelpPath: null,
clustersHelpPath: null,
deployBoardsHelpPath: null,
cloudRunHelpPath: null,
status: mockResponseData.status,
statusReason: mockResponseData.status_reason,
providerType: null,
preInstalledKnative: false,
rbac: false,
applications: {
helm: {
title: 'Legacy Helm Tiller server',
status: mockResponseData.applications[0].status,
statusReason: mockResponseData.applications[0].status_reason,
requestReason: null,
installable: true,
installed: false,
installFailed: false,
uninstallable: false,
uninstallSuccessful: false,
uninstallFailed: false,
validationError: null,
},
ingress: {
title: 'Ingress',
status: APPLICATION_STATUS.INSTALLABLE,
statusReason: mockResponseData.applications[1].status_reason,
requestReason: null,
externalIp: null,
externalHostname: null,
installable: true,
installed: false,
installFailed: true,
uninstallable: false,
updateFailed: false,
uninstallSuccessful: false,
uninstallFailed: false,
validationError: null,
},
runner: {
title: 'GitLab Runner',
status: mockResponseData.applications[2].status,
statusReason: mockResponseData.applications[2].status_reason,
requestReason: null,
version: mockResponseData.applications[2].version,
updateAvailable: mockResponseData.applications[2].update_available,
chartRepo: 'https://gitlab.com/gitlab-org/charts/gitlab-runner',
installable: true,
installed: false,
installFailed: false,
updateFailed: false,
updateSuccessful: false,
uninstallable: false,
uninstallSuccessful: false,
uninstallFailed: false,
validationError: null,
},
prometheus: {
title: 'Prometheus',
status: APPLICATION_STATUS.INSTALLABLE,
statusReason: mockResponseData.applications[3].status_reason,
requestReason: null,
installable: true,
installed: false,
installFailed: true,
uninstallable: false,
uninstallSuccessful: false,
uninstallFailed: false,
validationError: null,
},
jupyter: {
title: 'JupyterHub',
status: mockResponseData.applications[4].status,
statusReason: mockResponseData.applications[4].status_reason,
requestReason: null,
hostname: '',
installable: true,
installed: false,
installFailed: false,
uninstallable: false,
uninstallSuccessful: false,
uninstallFailed: false,
validationError: null,
},
knative: {
title: 'Knative',
status: mockResponseData.applications[5].status,
statusReason: mockResponseData.applications[5].status_reason,
requestReason: null,
hostname: null,
isEditingDomain: false,
externalIp: null,
externalHostname: null,
installable: true,
installed: false,
installFailed: false,
uninstallable: false,
uninstallSuccessful: false,
uninstallFailed: false,
updateSuccessful: false,
updateFailed: false,
validationError: null,
},
cert_manager: {
title: 'Cert-Manager',
status: APPLICATION_STATUS.INSTALLABLE,
installFailed: true,
statusReason: mockResponseData.applications[6].status_reason,
requestReason: null,
email: mockResponseData.applications[6].email,
installable: true,
installed: false,
uninstallable: false,
uninstallSuccessful: false,
uninstallFailed: false,
validationError: null,
},
elastic_stack: {
title: 'Elastic Stack',
status: APPLICATION_STATUS.INSTALLABLE,
installFailed: true,
statusReason: mockResponseData.applications[7].status_reason,
requestReason: null,
installable: true,
installed: false,
uninstallable: false,
uninstallSuccessful: false,
uninstallFailed: false,
validationError: null,
},
crossplane: {
title: 'Crossplane',
status: APPLICATION_STATUS.INSTALLABLE,
installFailed: true,
statusReason: mockResponseData.applications[8].status_reason,
requestReason: null,
installable: true,
installed: false,
uninstallable: false,
uninstallSuccessful: false,
uninstallFailed: false,
validationError: null,
},
cilium: {
title: 'GitLab Container Network Policies',
status: null,
statusReason: null,
requestReason: null,
installable: false,
installed: false,
installFailed: false,
uninstallable: false,
uninstallSuccessful: false,
uninstallFailed: false,
validationError: null,
},
},
environments: [],
fetchingEnvironments: false,
});
});
describe.each(APPLICATION_INSTALLED_STATUSES)(
'given the current app status is %s',
(status) => {
it('marks application as installed', () => {
const mockResponseData =
CLUSTERS_MOCK_DATA.GET['/gitlab-org/gitlab-shell/clusters/2/status.json'].data;
const runnerAppIndex = 2;
mockResponseData.applications[runnerAppIndex].status = status;
store.updateStateFromServer(mockResponseData);
expect(store.state.applications[RUNNER].installed).toBe(true);
});
},
);
it('sets default hostname for jupyter when ingress has a ip address', () => {
const mockResponseData =
CLUSTERS_MOCK_DATA.GET['/gitlab-org/gitlab-shell/clusters/2/status.json'].data;
store.updateStateFromServer(mockResponseData);
expect(store.state.applications.jupyter.hostname).toEqual(
`jupyter.${store.state.applications.ingress.externalIp}.nip.io`,
);
});
});
});
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