Commit 14d1a057 authored by Thong Kuah's avatar Thong Kuah

Merge branch 'cluster-environments-integration' into 'master'

Cluster environments BE/FE integration

See merge request gitlab-org/gitlab-ee!15515
parents 59022e4f b01275f1
...@@ -39,6 +39,7 @@ export default class Clusters { ...@@ -39,6 +39,7 @@ export default class Clusters {
updateKnativePath, updateKnativePath,
installPrometheusPath, installPrometheusPath,
managePrometheusPath, managePrometheusPath,
clusterEnvironmentsPath,
hasRbac, hasRbac,
clusterType, clusterType,
clusterStatus, clusterStatus,
...@@ -79,6 +80,7 @@ export default class Clusters { ...@@ -79,6 +80,7 @@ export default class Clusters {
installJupyterEndpoint: installJupyterPath, installJupyterEndpoint: installJupyterPath,
installKnativeEndpoint: installKnativePath, installKnativeEndpoint: installKnativePath,
updateKnativeEndpoint: updateKnativePath, updateKnativeEndpoint: updateKnativePath,
clusterEnvironmentsEndpoint: clusterEnvironmentsPath,
}); });
this.installApplication = this.installApplication.bind(this); this.installApplication = this.installApplication.bind(this);
...@@ -109,6 +111,10 @@ export default class Clusters { ...@@ -109,6 +111,10 @@ export default class Clusters {
this.initApplications(clusterType); this.initApplications(clusterType);
this.initEnvironments(); this.initEnvironments();
if (clusterEnvironmentsPath) {
this.fetchEnvironments();
}
this.updateContainer(null, this.store.state.status, this.store.state.statusReason); this.updateContainer(null, this.store.state.status, this.store.state.statusReason);
this.addListeners(); this.addListeners();
...@@ -162,6 +168,7 @@ export default class Clusters { ...@@ -162,6 +168,7 @@ export default class Clusters {
render(createElement) { render(createElement) {
return createElement(Environments, { return createElement(Environments, {
props: { props: {
isFetching: this.state.fetchingEnvironments,
environments: this.state.environments, environments: this.state.environments,
environmentsHelpPath: this.state.environmentsHelpPath, environmentsHelpPath: this.state.environmentsHelpPath,
clustersHelpPath: this.state.clustersHelpPath, clustersHelpPath: this.state.clustersHelpPath,
...@@ -172,6 +179,18 @@ export default class Clusters { ...@@ -172,6 +179,18 @@ export default class Clusters {
}); });
} }
fetchEnvironments() {
this.store.toggleFetchEnvironments(true);
this.service
.fetchClusterEnvironments()
.then(data => {
this.store.toggleFetchEnvironments(false);
this.store.updateEnvironments(data.data);
})
.catch(() => Clusters.handleError());
}
static initDismissableCallout() { static initDismissableCallout() {
const callout = document.querySelector('.js-cluster-security-warning'); const callout = document.querySelector('.js-cluster-security-warning');
PersistentUserCallout.factory(callout); PersistentUserCallout.factory(callout);
......
...@@ -33,6 +33,10 @@ export default class ClusterService { ...@@ -33,6 +33,10 @@ export default class ClusterService {
return axios.delete(this.appInstallEndpointMap[appId], params); return axios.delete(this.appInstallEndpointMap[appId], params);
} }
fetchClusterEnvironments() {
return axios.get(this.options.clusterEnvironmentsEndpoint);
}
static updateCluster(endpoint, data) { static updateCluster(endpoint, data) {
return axios.put(endpoint, data); return axios.put(endpoint, data);
} }
......
...@@ -84,6 +84,7 @@ export default class ClusterStore { ...@@ -84,6 +84,7 @@ export default class ClusterStore {
}, },
}, },
environments: [], environments: [],
fetchingEnvironments: false,
}; };
} }
...@@ -206,6 +207,10 @@ export default class ClusterStore { ...@@ -206,6 +207,10 @@ export default class ClusterStore {
}); });
} }
toggleFetchEnvironments(isFetching) {
this.state.fetchingEnvironments = isFetching;
}
updateEnvironments(environments = []) { updateEnvironments(environments = []) {
this.state.environments = environments.map(environment => ({ this.state.environments = environments.map(environment => ({
name: environment.name, name: environment.name,
...@@ -215,7 +220,7 @@ export default class ClusterStore { ...@@ -215,7 +220,7 @@ export default class ClusterStore {
rolloutStatus: { rolloutStatus: {
instances: environment.rollout_status ? environment.rollout_status.instances : [], instances: environment.rollout_status ? environment.rollout_status.instances : [],
}, },
updatedAt: environment.updatedAt, updatedAt: environment.updated_at,
})); }));
} }
} }
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
install_jupyter_path: clusterable.install_applications_cluster_path(@cluster, :jupyter), install_jupyter_path: clusterable.install_applications_cluster_path(@cluster, :jupyter),
install_knative_path: clusterable.install_applications_cluster_path(@cluster, :knative), install_knative_path: clusterable.install_applications_cluster_path(@cluster, :knative),
update_knative_path: clusterable.update_applications_cluster_path(@cluster, :knative), update_knative_path: clusterable.update_applications_cluster_path(@cluster, :knative),
cluster_environments_path: clusterable.environments_cluster_path(@cluster),
toggle_status: @cluster.enabled? ? 'true': 'false', toggle_status: @cluster.enabled? ? 'true': 'false',
has_rbac: has_rbac_enabled?(@cluster) ? 'true': 'false', has_rbac: has_rbac_enabled?(@cluster) ? 'true': 'false',
cluster_type: @cluster.cluster_type, cluster_type: @cluster.cluster_type,
......
# Cluster Environments **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/13392) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.3.
Cluster environments provide a consolidated view of which CI [environments](../../ci/environments.md) are
deployed to the Kubernetes cluster and it:
- Shows the project and the relevant environment related to the deployment.
- Displays the status of the pods for that environment.
## Overview
NOTE: **Note:**
Cluster environments are only available for
[group-level clusters](../group/clusters/index.md).
Support for [instance-level](../instance/clusters/index.md) clusters is
[planned](https://gitlab.com/gitlab-org/gitlab-ce/issues/63985).
With cluster environments, you can gain insight into:
- Which projects are deployed to the cluster.
- How many pods are in use for each project's environment.
- The CI job that was used to deploy to that environment.
![Cluster environments page](img/cluster_environments_table_v12_3.png)
Access to cluster environments is restricted to [group maintainers and
owners](../permissions.md#group-members-permissions)
## Usage
In order to:
- Track environments for the cluster, you must
[deploy to a Kubernetes cluster](../project/clusters/index.md#deploying-to-a-kubernetes-cluster)
successfully.
- Show pod usage correctly, you must
[enable Deploy Boards](../project/deploy_boards.md#enabling-deploy-boards).
Once you have successful deployments to your group-level cluster:
1. Navigate to your group's **Kubernetes** page.
1. Click on the **Environments** tab.
NOTE: **Note:**
Only successful deployments to the cluster is included in this page.
Non-cluster environments will not be included.
<script> <script>
import { GlTable, GlLink, GlEmptyState } from '@gitlab/ui'; import { GlTable, GlLink, GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
...@@ -11,9 +11,14 @@ export default { ...@@ -11,9 +11,14 @@ export default {
GlLink, GlLink,
Icon, Icon,
TimeAgo, TimeAgo,
GlLoadingIcon,
deploymentInstance: () => import('ee_component/vue_shared/components/deployment_instance.vue'), deploymentInstance: () => import('ee_component/vue_shared/components/deployment_instance.vue'),
}, },
props: { props: {
isFetching: {
type: Boolean,
required: true,
},
environments: { environments: {
type: Array, type: Array,
required: true, required: true,
...@@ -33,7 +38,7 @@ export default { ...@@ -33,7 +38,7 @@ export default {
}, },
computed: { computed: {
isEmpty() { isEmpty() {
return this.environments.length === 0; return !this.isFetching && this.environments.length === 0;
}, },
tableEmptyStateText() { tableEmptyStateText() {
const text = __( const text = __(
...@@ -57,7 +62,9 @@ export default { ...@@ -57,7 +62,9 @@ export default {
let podsInUse = 0; let podsInUse = 0;
this.environments.forEach(environment => { this.environments.forEach(environment => {
podsInUse += environment.rolloutStatus.instances.length; if (this.hasInstances(environment.rolloutStatus)) {
podsInUse += environment.rolloutStatus.instances.length;
}
}); });
return podsInUse; return podsInUse;
...@@ -76,6 +83,9 @@ export default { ...@@ -76,6 +83,9 @@ export default {
}, },
]; ];
}, },
methods: {
hasInstances: rolloutStatus => rolloutStatus.instances && rolloutStatus.instances.length,
},
}; };
</script> </script>
...@@ -90,7 +100,12 @@ export default { ...@@ -90,7 +100,12 @@ export default {
<div slot="description" v-html="tableEmptyStateText"></div> <div slot="description" v-html="tableEmptyStateText"></div>
</gl-empty-state> </gl-empty-state>
<gl-table v-else :fields="$options.fields" :items="environments" head-variant="white"> <gl-table
v-if="!isFetching && !isEmpty"
:fields="$options.fields"
:items="environments"
head-variant="white"
>
<!-- column: Project --> <!-- column: Project -->
<template slot="project" slot-scope="data"> <template slot="project" slot-scope="data">
<a :href="`/${data.value.path_with_namespace}`">{{ data.value.name }}</a> <a :href="`/${data.value.path_with_namespace}`">{{ data.value.name }}</a>
...@@ -112,7 +127,7 @@ export default { ...@@ -112,7 +127,7 @@ export default {
</template> </template>
<template slot="rolloutStatus" slot-scope="row"> <template slot="rolloutStatus" slot-scope="row">
<div v-if="row.item.rolloutStatus.instances.length" class="d-flex flex-wrap flex-row"> <div v-if="hasInstances(row.item.rolloutStatus)" class="d-flex flex-wrap flex-row">
<template v-for="(instance, i) in row.item.rolloutStatus.instances"> <template v-for="(instance, i) in row.item.rolloutStatus.instances">
<deployment-instance <deployment-instance
:key="i" :key="i"
...@@ -141,5 +156,7 @@ export default { ...@@ -141,5 +156,7 @@ export default {
<time-ago :time="data.value" /> <time-ago :time="data.value" />
</template> </template>
</gl-table> </gl-table>
<gl-loading-icon v-if="isFetching" :size="2" class="mt-3" />
</div> </div>
</template> </template>
...@@ -14,6 +14,8 @@ module Clusters ...@@ -14,6 +14,8 @@ module Clusters
expose :rollout_status, if: -> (*) { can_read_cluster_deployments? }, using: ::RolloutStatusEntity expose :rollout_status, if: -> (*) { can_read_cluster_deployments? }, using: ::RolloutStatusEntity
expose :updated_at
private private
alias_method :environment, :object alias_method :environment, :object
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
%ul.nav-links.mobile-separator.nav.nav-tabs{ role: 'tablist' } %ul.nav-links.mobile-separator.nav.nav-tabs{ role: 'tablist' }
%li.nav-item{ role: 'presentation' } %li.nav-item{ role: 'presentation' }
%a.nav-link{ class: active_when(is_configure_active), href: clusterable.cluster_path(@cluster.id, params: {tab: 'configure'}), id: 'group-cluster-configure-tab' } %a.nav-link{ class: active_when(is_configure_active), href: clusterable.cluster_path(@cluster.id, params: {tab: 'configure'}), id: 'group-cluster-configure-tab' }
%span= _('Configure') %span= _('Configuration')
%li.nav-item{ role: 'presentation' } %li.nav-item{ role: 'presentation' }
%a.nav-link{ class: active_when(!is_configure_active), href: clusterable.cluster_path(@cluster.id, params: {tab: 'environments'}), id: 'group-cluster-environments-tab' } %a.nav-link{ class: active_when(!is_configure_active), href: clusterable.cluster_path(@cluster.id, params: {tab: 'environments'}), id: 'group-cluster-environments-tab' }
%span= _('Environments') %span= _('Environments')
......
...@@ -19,8 +19,12 @@ ...@@ -19,8 +19,12 @@
] ]
}, },
"rollout_status": { "rollout_status": {
"$ref": "../rollout_status.json" "oneOf": [
} { "type": "null" },
{ "$ref": "../rollout_status.json" }
]
},
"updated_at": { "type": "date" }
}, },
"additionalProperties": false "additionalProperties": false
} }
...@@ -17,6 +17,7 @@ describe('Environments', () => { ...@@ -17,6 +17,7 @@ describe('Environments', () => {
environmentsHelpPath: 'path/to/environments', environmentsHelpPath: 'path/to/environments',
clustersHelpPath: 'path/to/clusters', clustersHelpPath: 'path/to/clusters',
deployBoardsHelpPath: 'path/to/clusters', deployBoardsHelpPath: 'path/to/clusters',
isFetching: false,
}; };
wrapper = mount(Component, { wrapper = mount(Component, {
......
...@@ -22,24 +22,16 @@ describe Clusters::EnvironmentEntity do ...@@ -22,24 +22,16 @@ describe Clusters::EnvironmentEntity do
subject { described_class.new(environment, request: request).as_json } subject { described_class.new(environment, request: request).as_json }
it 'exposes project' do
expect(subject).to include(:project)
end
it 'exposes last_deployment' do
expect(subject).to include(:last_deployment)
end
it 'exposes environment_path' do
expect(subject).to include(:environment_path)
end
context 'deploy board available' do context 'deploy board available' do
before do before do
allow(group).to receive(:feature_available?).and_call_original allow(group).to receive(:feature_available?).and_call_original
allow(group).to receive(:feature_available?).with(:cluster_deployments).and_return(true) allow(group).to receive(:feature_available?).with(:cluster_deployments).and_return(true)
end end
it 'matches expected schema' do
expect(subject.with_indifferent_access).to match_schema('clusters/environment', dir: 'ee')
end
it 'exposes rollout_status' do it 'exposes rollout_status' do
expect(subject).to include(:rollout_status) expect(subject).to include(:rollout_status)
end end
...@@ -50,6 +42,10 @@ describe Clusters::EnvironmentEntity do ...@@ -50,6 +42,10 @@ describe Clusters::EnvironmentEntity do
allow(group).to receive(:feature_available?).with(:cluster_deployments).and_return(false) allow(group).to receive(:feature_available?).with(:cluster_deployments).and_return(false)
end end
it 'matches expected schema' do
expect(subject.with_indifferent_access).to match_schema('clusters/environment', dir: 'ee')
end
it 'does not expose rollout_status' do it 'does not expose rollout_status' do
expect(subject).not_to include(:rollout_status) expect(subject).not_to include(:rollout_status)
end end
......
...@@ -3918,7 +3918,7 @@ msgstr "" ...@@ -3918,7 +3918,7 @@ msgstr ""
msgid "Confidentiality" msgid "Confidentiality"
msgstr "" msgstr ""
msgid "Configure" msgid "Configuration"
msgstr "" msgstr ""
msgid "Configure GitLab runners to start using the Web Terminal. %{helpStart}Learn more.%{helpEnd}" msgid "Configure GitLab runners to start using the Web Terminal. %{helpStart}Learn more.%{helpEnd}"
......
...@@ -152,6 +152,7 @@ describe('Clusters Store', () => { ...@@ -152,6 +152,7 @@ describe('Clusters Store', () => {
}, },
}, },
environments: [], environments: [],
fetchingEnvironments: false,
}); });
}); });
......
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