Commit eed6b9a6 authored by Anna Vovchenko's avatar Anna Vovchenko Committed by Peter Hegman

Add version column to the Agent listing page

As we want to enhance the Agent listing page with useful information,
we are adding a new column showing the Agent version.
The column also identifies common issues:
- version mismatch among pods;
- outdated version.

Changelog: added
parent 16bd2926
<script> <script>
import { GlLink, GlTable, GlIcon, GlSprintf, GlTooltip, GlPopover } from '@gitlab/ui'; import { GlLink, GlTable, GlIcon, GlSprintf, GlTooltip, GlPopover } from '@gitlab/ui';
import { s__ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
import { helpPagePath } from '~/helpers/help_page_helper'; import { helpPagePath } from '~/helpers/help_page_helper';
import { AGENT_STATUSES } from '../constants'; import { AGENT_STATUSES, I18N_AGENT_TABLE } from '../constants';
import { getAgentConfigPath } from '../clusters_util'; import { getAgentConfigPath } from '../clusters_util';
import DeleteAgentButton from './delete_agent_button.vue'; import DeleteAgentButton from './delete_agent_button.vue';
export default { export default {
i18n: { i18n: I18N_AGENT_TABLE,
nameLabel: s__('ClusterAgents|Name'),
statusLabel: s__('ClusterAgents|Connection status'),
lastContactLabel: s__('ClusterAgents|Last contact'),
configurationLabel: s__('ClusterAgents|Configuration'),
troubleshootingText: s__('ClusterAgents|Learn how to troubleshoot'),
neverConnectedText: s__('ClusterAgents|Never'),
},
components: { components: {
GlLink, GlLink,
GlTable, GlTable,
...@@ -29,9 +21,13 @@ export default { ...@@ -29,9 +21,13 @@ export default {
}, },
mixins: [timeagoMixin], mixins: [timeagoMixin],
AGENT_STATUSES, AGENT_STATUSES,
troubleshooting_link: helpPagePath('user/clusters/agent/index', { troubleshootingLink: helpPagePath('user/clusters/agent/index', {
anchor: 'troubleshooting', anchor: 'troubleshooting',
}), }),
versionUpdateLink: helpPagePath('user/clusters/agent/install/index', {
anchor: 'update-the-agent-version',
}),
inject: ['gitlabVersion'],
props: { props: {
agents: { agents: {
required: true, required: true,
...@@ -67,6 +63,11 @@ export default { ...@@ -67,6 +63,11 @@ export default {
label: this.$options.i18n.lastContactLabel, label: this.$options.i18n.lastContactLabel,
tdClass, tdClass,
}, },
{
key: 'version',
label: this.$options.i18n.versionLabel,
tdClass,
},
{ {
key: 'configuration', key: 'configuration',
label: this.$options.i18n.configurationLabel, label: this.$options.i18n.configurationLabel,
...@@ -79,19 +80,77 @@ export default { ...@@ -79,19 +80,77 @@ export default {
}, },
]; ];
}, },
agentsList() {
if (!this.agents.length) {
return [];
}
return this.agents.map((agent) => {
const versions = this.getAgentVersions(agent);
return { ...agent, versions };
});
},
}, },
methods: { methods: {
getCellId(item) { getStatusCellId(item) {
return `connection-status-${item.name}`; return `connection-status-${item.name}`;
}, },
getVersionCellId(item) {
return `version-${item.name}`;
},
getPopoverTestId(item) {
return `popover-${item.name}`;
},
getAgentConfigPath, getAgentConfigPath,
getAgentVersions(agent) {
const agentConnections = agent.connections?.nodes || [];
const agentVersions = agentConnections.map((agentConnection) =>
agentConnection.metadata.version.replace('v', ''),
);
const uniqueAgentVersions = [...new Set(agentVersions)];
return uniqueAgentVersions.sort((a, b) => a.localeCompare(b));
},
getAgentVersionString(agent) {
return agent.versions[0] || '';
},
isVersionMismatch(agent) {
return agent.versions.length > 1;
},
isVersionOutdated(agent) {
if (!agent.versions.length) return false;
const [agentMajorVersion, agentMinorVersion] = this.getAgentVersionString(agent).split('.');
const [gitlabMajorVersion, gitlabMinorVersion] = this.gitlabVersion.split('.');
const majorVersionMismatch = agentMajorVersion !== gitlabMajorVersion;
// We should warn user if their current GitLab and agent versions are more than 1 minor version apart:
const minorVersionMismatch = Math.abs(agentMinorVersion - gitlabMinorVersion) > 1;
return majorVersionMismatch || minorVersionMismatch;
},
getVersionPopoverTitle(agent) {
if (this.isVersionMismatch(agent) && this.isVersionOutdated(agent)) {
return this.$options.i18n.versionMismatchOutdatedTitle;
} else if (this.isVersionMismatch(agent)) {
return this.$options.i18n.versionMismatchTitle;
} else if (this.isVersionOutdated(agent)) {
return this.$options.i18n.versionOutdatedTitle;
}
return null;
},
}, },
}; };
</script> </script>
<template> <template>
<gl-table <gl-table
:items="agents" :items="agentsList"
:fields="fields" :fields="fields"
stacked="md" stacked="md"
head-variant="white" head-variant="white"
...@@ -106,19 +165,23 @@ export default { ...@@ -106,19 +165,23 @@ export default {
</template> </template>
<template #cell(status)="{ item }"> <template #cell(status)="{ item }">
<span :id="getCellId(item)" class="gl-md-pr-5" data-testid="cluster-agent-connection-status"> <span
:id="getStatusCellId(item)"
class="gl-md-pr-5"
data-testid="cluster-agent-connection-status"
>
<span :class="$options.AGENT_STATUSES[item.status].class" class="gl-mr-3"> <span :class="$options.AGENT_STATUSES[item.status].class" class="gl-mr-3">
<gl-icon :name="$options.AGENT_STATUSES[item.status].icon" :size="12" /></span <gl-icon :name="$options.AGENT_STATUSES[item.status].icon" :size="12" /></span
>{{ $options.AGENT_STATUSES[item.status].name }} >{{ $options.AGENT_STATUSES[item.status].name }}
</span> </span>
<gl-tooltip v-if="item.status === 'active'" :target="getCellId(item)" placement="right"> <gl-tooltip v-if="item.status === 'active'" :target="getStatusCellId(item)" placement="right">
<gl-sprintf :message="$options.AGENT_STATUSES[item.status].tooltip.title" <gl-sprintf :message="$options.AGENT_STATUSES[item.status].tooltip.title"
><template #timeAgo>{{ timeFormatted(item.lastContact) }}</template> ><template #timeAgo>{{ timeFormatted(item.lastContact) }}</template>
</gl-sprintf> </gl-sprintf>
</gl-tooltip> </gl-tooltip>
<gl-popover <gl-popover
v-else v-else
:target="getCellId(item)" :target="getStatusCellId(item)"
:title="$options.AGENT_STATUSES[item.status].tooltip.title" :title="$options.AGENT_STATUSES[item.status].tooltip.title"
placement="right" placement="right"
container="viewport" container="viewport"
...@@ -129,7 +192,7 @@ export default { ...@@ -129,7 +192,7 @@ export default {
> >
</p> </p>
<p class="gl-mb-0"> <p class="gl-mb-0">
<gl-link :href="$options.troubleshooting_link" target="_blank" class="gl-font-sm"> <gl-link :href="$options.troubleshootingLink" target="_blank" class="gl-font-sm">
{{ $options.i18n.troubleshootingText }}</gl-link {{ $options.i18n.troubleshootingText }}</gl-link
> >
</p> </p>
...@@ -143,6 +206,52 @@ export default { ...@@ -143,6 +206,52 @@ export default {
</span> </span>
</template> </template>
<template #cell(version)="{ item }">
<span :id="getVersionCellId(item)" data-testid="cluster-agent-version">
{{ getAgentVersionString(item) }}
<gl-icon
v-if="isVersionMismatch(item) || isVersionOutdated(item)"
name="warning"
class="gl-text-orange-500 gl-ml-2"
/>
</span>
<gl-popover
v-if="isVersionMismatch(item) || isVersionOutdated(item)"
:target="getVersionCellId(item)"
:title="getVersionPopoverTitle(item)"
:data-testid="getPopoverTestId(item)"
placement="right"
container="viewport"
>
<div v-if="isVersionMismatch(item) && isVersionOutdated(item)">
<p>{{ $options.i18n.versionMismatchText }}</p>
<p class="gl-mb-0">
<gl-sprintf :message="$options.i18n.versionOutdatedText">
<template #version>{{ gitlabVersion }}</template>
</gl-sprintf>
<gl-link :href="$options.versionUpdateLink" class="gl-font-sm">
{{ $options.i18n.viewDocsText }}</gl-link
>
</p>
</div>
<p v-else-if="isVersionMismatch(item)" class="gl-mb-0">
{{ $options.i18n.versionMismatchText }}
</p>
<p v-else-if="isVersionOutdated(item)" class="gl-mb-0">
<gl-sprintf :message="$options.i18n.versionOutdatedText">
<template #version>{{ gitlabVersion }}</template>
</gl-sprintf>
<gl-link :href="$options.versionUpdateLink" class="gl-font-sm">
{{ $options.i18n.viewDocsText }}</gl-link
>
</p>
</gl-popover>
</template>
<template #cell(configuration)="{ item }"> <template #cell(configuration)="{ item }">
<span data-testid="cluster-agent-configuration-link"> <span data-testid="cluster-agent-configuration-link">
<gl-link v-if="item.configFolder" :href="item.configFolder.webPath"> <gl-link v-if="item.configFolder" :href="item.configFolder.webPath">
......
...@@ -64,6 +64,27 @@ export const STATUSES = { ...@@ -64,6 +64,27 @@ export const STATUSES = {
creating: { title: __('Creating') }, creating: { title: __('Creating') },
}; };
export const I18N_AGENT_TABLE = {
nameLabel: s__('ClusterAgents|Name'),
statusLabel: s__('ClusterAgents|Connection status'),
lastContactLabel: s__('ClusterAgents|Last contact'),
versionLabel: __('Version'),
configurationLabel: s__('ClusterAgents|Configuration'),
optionsLabel: __('Options'),
troubleshootingText: s__('ClusterAgents|Learn how to troubleshoot'),
neverConnectedText: s__('ClusterAgents|Never'),
versionMismatchTitle: s__('ClusterAgents|Agent version mismatch'),
versionMismatchText: s__(
"ClusterAgents|The Agent version do not match each other across your cluster's pods. This can happen when a new Agent version was just deployed and Kubernetes is shutting down the old pods.",
),
versionOutdatedTitle: s__('ClusterAgents|Agent version update required'),
versionOutdatedText: s__(
'ClusterAgents|Your Agent version is out of sync with your GitLab version (v%{version}), which might cause compatibility problems. Update the Agent installed on your cluster to the most recent version.',
),
versionMismatchOutdatedTitle: s__('ClusterAgents|Agent version mismatch and update'),
viewDocsText: s__('ClusterAgents|How to update the Agent?'),
};
export const I18N_AGENT_MODAL = { export const I18N_AGENT_MODAL = {
agent_registration: { agent_registration: {
registerAgentButton: s__('ClusterAgents|Register'), registerAgentButton: s__('ClusterAgents|Register'),
......
...@@ -2,6 +2,13 @@ fragment ClusterAgentFragment on ClusterAgent { ...@@ -2,6 +2,13 @@ fragment ClusterAgentFragment on ClusterAgent {
id id
name name
webPath webPath
connections {
nodes {
metadata {
version
}
}
}
tokens { tokens {
nodes { nodes {
id id
......
...@@ -27,6 +27,7 @@ export default () => { ...@@ -27,6 +27,7 @@ export default () => {
clustersEmptyStateImage, clustersEmptyStateImage,
canAddCluster, canAddCluster,
canAdminCluster, canAdminCluster,
gitlabVersion,
} = el.dataset; } = el.dataset;
return new Vue({ return new Vue({
...@@ -42,6 +43,7 @@ export default () => { ...@@ -42,6 +43,7 @@ export default () => {
clustersEmptyStateImage, clustersEmptyStateImage,
canAddCluster: parseBoolean(canAddCluster), canAddCluster: parseBoolean(canAddCluster),
canAdminCluster: parseBoolean(canAdminCluster), canAdminCluster: parseBoolean(canAdminCluster),
gitlabVersion,
}, },
store: createStore(el.dataset), store: createStore(el.dataset),
render(createElement) { render(createElement) {
......
...@@ -29,7 +29,7 @@ module Resolvers ...@@ -29,7 +29,7 @@ module Resolvers
def get_connected_agents def get_connected_agents
kas_client.get_connected_agents(project: project) kas_client.get_connected_agents(project: project)
rescue GRPC::BadStatus => e rescue GRPC::BadStatus, Gitlab::Kas::Client::ConfigurationError => e
raise Gitlab::Graphql::Errors::ResourceNotAvailable, e.class.name raise Gitlab::Graphql::Errors::ResourceNotAvailable, e.class.name
end end
......
...@@ -39,7 +39,8 @@ module ClustersHelper ...@@ -39,7 +39,8 @@ module ClustersHelper
empty_state_image: image_path('illustrations/empty-state/empty-state-agents.svg'), empty_state_image: image_path('illustrations/empty-state/empty-state-agents.svg'),
project_path: clusterable.full_path, project_path: clusterable.full_path,
add_cluster_path: clusterable.new_path(tab: 'add'), add_cluster_path: clusterable.new_path(tab: 'add'),
kas_address: Gitlab::Kas.external_url kas_address: Gitlab::Kas.external_url,
gitlab_version: Gitlab.version_info
}.merge(js_clusters_list_data(clusterable)) }.merge(js_clusters_list_data(clusterable))
end end
......
...@@ -77,7 +77,7 @@ file and: ...@@ -77,7 +77,7 @@ file and:
1. From your project's sidebar, select **Infrastructure > Kubernetes clusters**. 1. From your project's sidebar, select **Infrastructure > Kubernetes clusters**.
1. Select **Actions**. 1. Select **Actions**.
1. From the **Select an Agent** dropdown list, select the Agent you want to register and select **Register an Agent**. 1. From the **Select an Agent** dropdown list, select the Agent you want to register and select **Register an Agent**.
1. GitLab generates a registration token for this Agent. Securely store this secret token as you cannot view it again. 1. GitLab generates a registration token for this Agent. Securely store this secret token, as you need it to install the Agent onto your cluster and to [update the Agent](#update-the-agent-version) to another version.
1. Copy the command under **Recommended installation method**. You need it to install the Agent onto your cluster through the one-liner installation method. 1. Copy the command under **Recommended installation method**. You need it to install the Agent onto your cluster through the one-liner installation method.
### Install the Agent onto the cluster ### Install the Agent onto the cluster
...@@ -203,6 +203,35 @@ and you're good to go. You can create multiple Agents, for example: ...@@ -203,6 +203,35 @@ and you're good to go. You can create multiple Agents, for example:
- To reach your cluster from different projects. - To reach your cluster from different projects.
- To connect multiple clusters to GitLab. - To connect multiple clusters to GitLab.
## Update the Agent version
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/340882) in GitLab 14.8, GitLab warns you on the Agent's list page to update the Agent version installed on your cluster.
To update the Agent's version on your cluster, you need to re-run the [installation command](#install-the-agent-onto-the-cluster)
with a newer `--agent-version`. Make sure to specify the other required parameters: `--kas-address`, `--namespace`, and `--agent-token`.
You can find the available `agentk` versions in [the container registry](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/container_registry/1223205?sort=desc).
If you don't have access to your Agent's token, you can retrieve it from your cluster:
1. On your computer, open the terminal and connect to your cluster.
1. To retrieve the namespace, run:
```shell
kubectl get namespaces
```
1. To retrieve the secret, run:
```shell
kubectl -n <namespace> get secrets
```
1. To retrieve the token, run:
```shell
kubectl -n <namespace> get secret <secret-name> --template={{.data.token}} | base64 --decode
```
## Example projects ## Example projects
The following example projects can help you get started with the Agent. The following example projects can help you get started with the Agent.
...@@ -222,7 +251,8 @@ A feature introduced in a given GitLab minor version might work with other `agen ...@@ -222,7 +251,8 @@ A feature introduced in a given GitLab minor version might work with other `agen
To make sure that it works, use at least the same `agentk` and `kas` minor version. For example, To make sure that it works, use at least the same `agentk` and `kas` minor version. For example,
if your GitLab version is 14.2, use at least `agentk` 14.2 and `kas` 14.2. if your GitLab version is 14.2, use at least `agentk` 14.2 and `kas` 14.2.
We recommend upgrading your `kas` installations together with GitLab instances' upgrades, and to upgrade the `agentk` installations after upgrading GitLab. We recommend upgrading your `kas` installations together with GitLab instances' upgrades, and to
[upgrade the `agentk` installations](#update-the-agent-version) after upgrading GitLab.
The available `agentk` and `kas` versions can be found in The available `agentk` and `kas` versions can be found in
[the container registry](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/container_registry/). [the container registry](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/container_registry/).
...@@ -7601,6 +7601,15 @@ msgstr "" ...@@ -7601,6 +7601,15 @@ msgstr ""
msgid "ClusterAgents|Agent never connected to GitLab" msgid "ClusterAgents|Agent never connected to GitLab"
msgstr "" msgstr ""
msgid "ClusterAgents|Agent version mismatch"
msgstr ""
msgid "ClusterAgents|Agent version mismatch and update"
msgstr ""
msgid "ClusterAgents|Agent version update required"
msgstr ""
msgid "ClusterAgents|All" msgid "ClusterAgents|All"
msgstr "" msgstr ""
...@@ -7700,6 +7709,9 @@ msgstr "" ...@@ -7700,6 +7709,9 @@ msgstr ""
msgid "ClusterAgents|How to register an agent?" msgid "ClusterAgents|How to register an agent?"
msgstr "" msgstr ""
msgid "ClusterAgents|How to update the Agent?"
msgstr ""
msgid "ClusterAgents|Install new Agent" msgid "ClusterAgents|Install new Agent"
msgstr "" msgstr ""
...@@ -7781,6 +7793,9 @@ msgstr "" ...@@ -7781,6 +7793,9 @@ msgstr ""
msgid "ClusterAgents|Tell us what you think" msgid "ClusterAgents|Tell us what you think"
msgstr "" msgstr ""
msgid "ClusterAgents|The Agent version do not match each other across your cluster's pods. This can happen when a new Agent version was just deployed and Kubernetes is shutting down the old pods."
msgstr ""
msgid "ClusterAgents|The GitLab Agent provides an increased level of security when connecting Kubernetes clusters to GitLab. %{linkStart}Learn more about the GitLab Agent.%{linkEnd}" msgid "ClusterAgents|The GitLab Agent provides an increased level of security when connecting Kubernetes clusters to GitLab. %{linkStart}Learn more about the GitLab Agent.%{linkEnd}"
msgstr "" msgstr ""
...@@ -7834,6 +7849,9 @@ msgstr "" ...@@ -7834,6 +7849,9 @@ msgstr ""
msgid "ClusterAgents|You will need to create a token to connect to your agent" msgid "ClusterAgents|You will need to create a token to connect to your agent"
msgstr "" msgstr ""
msgid "ClusterAgents|Your Agent version is out of sync with your GitLab version (v%{version}), which might cause compatibility problems. Update the Agent installed on your cluster to the most recent version."
msgstr ""
msgid "ClusterAgents|Your instance doesn't have the %{linkStart}GitLab Agent Server (KAS)%{linkEnd} set up. Ask a GitLab Administrator to install it." msgid "ClusterAgents|Your instance doesn't have the %{linkStart}GitLab Agent Server (KAS)%{linkEnd} set up. Ask a GitLab Administrator to install it."
msgstr "" msgstr ""
......
...@@ -15,6 +15,7 @@ RSpec.describe 'Cluster agent registration', :js do ...@@ -15,6 +15,7 @@ RSpec.describe 'Cluster agent registration', :js do
double(agent_name: 'example-agent-1', path: '.gitlab/agents/example-agent-1/config.yaml'), double(agent_name: 'example-agent-1', path: '.gitlab/agents/example-agent-1/config.yaml'),
double(agent_name: 'example-agent-2', path: '.gitlab/agents/example-agent-2/config.yaml') double(agent_name: 'example-agent-2', path: '.gitlab/agents/example-agent-2/config.yaml')
]) ])
allow(client).to receive(:get_connected_agents).and_return([])
end end
allow(Devise).to receive(:friendly_token).and_return('example-agent-token') allow(Devise).to receive(:friendly_token).and_return('example-agent-token')
......
...@@ -10,6 +10,11 @@ RSpec.describe 'ClusterAgents', :js do ...@@ -10,6 +10,11 @@ RSpec.describe 'ClusterAgents', :js do
let(:user) { project.creator } let(:user) { project.creator }
before do before do
allow(Gitlab::Kas).to receive(:enabled?).and_return(true)
allow_next_instance_of(Gitlab::Kas::Client) do |client|
allow(client).to receive(:get_connected_agents).and_return([])
end
gitlab_sign_in(user) gitlab_sign_in(user)
end end
......
import { GlLink, GlIcon } from '@gitlab/ui'; import { GlLink, GlIcon } from '@gitlab/ui';
import { sprintf } from '~/locale';
import AgentTable from '~/clusters_list/components/agent_table.vue'; import AgentTable from '~/clusters_list/components/agent_table.vue';
import DeleteAgentButton from '~/clusters_list/components/delete_agent_button.vue'; import DeleteAgentButton from '~/clusters_list/components/delete_agent_button.vue';
import { ACTIVE_CONNECTION_TIME } from '~/clusters_list/constants'; import { I18N_AGENT_TABLE } from '~/clusters_list/constants';
import { mountExtended } from 'helpers/vue_test_utils_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component'; import { stubComponent } from 'helpers/stub_component';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
import { clusterAgents, connectedTimeNow, connectedTimeInactive } from './mock_data';
const connectedTimeNow = new Date();
const connectedTimeInactive = new Date(connectedTimeNow.getTime() - ACTIVE_CONNECTION_TIME);
const provideData = { const provideData = {
projectPath: 'path/to/project', gitlabVersion: '14.8',
}; };
const propsData = { const propsData = {
agents: [ agents: clusterAgents,
{
name: 'agent-1',
id: 'agent-1-id',
configFolder: {
webPath: '/agent/full/path',
},
webPath: '/agent-1',
status: 'unused',
lastContact: null,
tokens: null,
},
{
name: 'agent-2',
id: 'agent-2-id',
webPath: '/agent-2',
status: 'active',
lastContact: connectedTimeNow.getTime(),
tokens: {
nodes: [
{
lastUsedAt: connectedTimeNow,
},
],
},
},
{
name: 'agent-3',
id: 'agent-3-id',
webPath: '/agent-3',
status: 'inactive',
lastContact: connectedTimeInactive.getTime(),
tokens: {
nodes: [
{
lastUsedAt: connectedTimeInactive,
},
],
},
},
],
}; };
const DeleteAgentButtonStub = stubComponent(DeleteAgentButton, { const DeleteAgentButtonStub = stubComponent(DeleteAgentButton, {
template: `<div></div>`, template: `<div></div>`,
}); });
const outdatedTitle = I18N_AGENT_TABLE.versionOutdatedTitle;
const mismatchTitle = I18N_AGENT_TABLE.versionMismatchTitle;
const mismatchOutdatedTitle = I18N_AGENT_TABLE.versionMismatchOutdatedTitle;
const outdatedText = sprintf(I18N_AGENT_TABLE.versionOutdatedText, {
version: provideData.gitlabVersion,
});
const mismatchText = I18N_AGENT_TABLE.versionMismatchText;
describe('AgentTable', () => { describe('AgentTable', () => {
let wrapper; let wrapper;
...@@ -67,6 +34,7 @@ describe('AgentTable', () => { ...@@ -67,6 +34,7 @@ describe('AgentTable', () => {
const findStatusIcon = (at) => wrapper.findAllComponents(GlIcon).at(at); const findStatusIcon = (at) => wrapper.findAllComponents(GlIcon).at(at);
const findStatusText = (at) => wrapper.findAllByTestId('cluster-agent-connection-status').at(at); const findStatusText = (at) => wrapper.findAllByTestId('cluster-agent-connection-status').at(at);
const findLastContactText = (at) => wrapper.findAllByTestId('cluster-agent-last-contact').at(at); const findLastContactText = (at) => wrapper.findAllByTestId('cluster-agent-last-contact').at(at);
const findVersionText = (at) => wrapper.findAllByTestId('cluster-agent-version').at(at);
const findConfiguration = (at) => const findConfiguration = (at) =>
wrapper.findAllByTestId('cluster-agent-configuration-link').at(at); wrapper.findAllByTestId('cluster-agent-configuration-link').at(at);
const findDeleteAgentButton = () => wrapper.findAllComponents(DeleteAgentButton); const findDeleteAgentButton = () => wrapper.findAllComponents(DeleteAgentButton);
...@@ -92,7 +60,7 @@ describe('AgentTable', () => { ...@@ -92,7 +60,7 @@ describe('AgentTable', () => {
agentName | link | lineNumber agentName | link | lineNumber
${'agent-1'} | ${'/agent-1'} | ${0} ${'agent-1'} | ${'/agent-1'} | ${0}
${'agent-2'} | ${'/agent-2'} | ${1} ${'agent-2'} | ${'/agent-2'} | ${1}
`('displays agent link', ({ agentName, link, lineNumber }) => { `('displays agent link for $agentName', ({ agentName, link, lineNumber }) => {
expect(findAgentLink(lineNumber).text()).toBe(agentName); expect(findAgentLink(lineNumber).text()).toBe(agentName);
expect(findAgentLink(lineNumber).attributes('href')).toBe(link); expect(findAgentLink(lineNumber).attributes('href')).toBe(link);
}); });
...@@ -102,33 +70,92 @@ describe('AgentTable', () => { ...@@ -102,33 +70,92 @@ describe('AgentTable', () => {
${'Never connected'} | ${'status-neutral'} | ${0} ${'Never connected'} | ${'status-neutral'} | ${0}
${'Connected'} | ${'status-success'} | ${1} ${'Connected'} | ${'status-success'} | ${1}
${'Not connected'} | ${'severity-critical'} | ${2} ${'Not connected'} | ${'severity-critical'} | ${2}
`('displays agent connection status', ({ status, iconName, lineNumber }) => { `(
'displays agent connection status as "$status" at line $lineNumber',
({ status, iconName, lineNumber }) => {
expect(findStatusText(lineNumber).text()).toBe(status); expect(findStatusText(lineNumber).text()).toBe(status);
expect(findStatusIcon(lineNumber).props('name')).toBe(iconName); expect(findStatusIcon(lineNumber).props('name')).toBe(iconName);
}); },
);
it.each` it.each`
lastContact | lineNumber lastContact | lineNumber
${'Never'} | ${0} ${'Never'} | ${0}
${timeagoMixin.methods.timeFormatted(connectedTimeNow)} | ${1} ${timeagoMixin.methods.timeFormatted(connectedTimeNow)} | ${1}
${timeagoMixin.methods.timeFormatted(connectedTimeInactive)} | ${2} ${timeagoMixin.methods.timeFormatted(connectedTimeInactive)} | ${2}
`('displays agent last contact time', ({ lastContact, lineNumber }) => { `(
'displays agent last contact time as "$lastContact" at line $lineNumber',
({ lastContact, lineNumber }) => {
expect(findLastContactText(lineNumber).text()).toBe(lastContact); expect(findLastContactText(lineNumber).text()).toBe(lastContact);
},
);
describe.each`
agent | version | podsNumber | versionMismatch | versionOutdated | title | texts | lineNumber
${'agent-1'} | ${''} | ${1} | ${false} | ${false} | ${''} | ${''} | ${0}
${'agent-2'} | ${'14.8'} | ${2} | ${false} | ${false} | ${''} | ${''} | ${1}
${'agent-3'} | ${'14.5'} | ${1} | ${false} | ${true} | ${outdatedTitle} | ${[outdatedText]} | ${2}
${'agent-4'} | ${'14.7'} | ${2} | ${true} | ${false} | ${mismatchTitle} | ${[mismatchText]} | ${3}
${'agent-5'} | ${'14.3'} | ${2} | ${true} | ${true} | ${mismatchOutdatedTitle} | ${[mismatchText, outdatedText]} | ${4}
`(
'agent version column at line $lineNumber',
({
agent,
version,
podsNumber,
versionMismatch,
versionOutdated,
title,
texts,
lineNumber,
}) => {
const findIcon = () => findVersionText(lineNumber).find(GlIcon);
const findPopover = () => wrapper.findByTestId(`popover-${agent}`);
const versionWarning = versionMismatch || versionOutdated;
it('shows the correct agent version', () => {
expect(findVersionText(lineNumber).text()).toBe(version);
});
if (versionWarning) {
it(`shows a warning icon when agent versions mismatch is ${versionMismatch} and outdated is ${versionOutdated} and the number of pods is ${podsNumber}`, () => {
expect(findIcon().props('name')).toBe('warning');
});
it(`renders correct title for the popover when agent versions mismatch is ${versionMismatch} and outdated is ${versionOutdated}`, () => {
expect(findPopover().props('title')).toBe(title);
});
it(`renders correct text for the popover when agent versions mismatch is ${versionMismatch} and outdated is ${versionOutdated}`, () => {
texts.forEach((text) => {
expect(findPopover().text()).toContain(text);
});
}); });
} else {
it(`doesn't show a warning icon with a popover when agent versions mismatch is ${versionMismatch} and outdated is ${versionOutdated} and the number of pods is ${podsNumber}`, () => {
expect(findIcon().exists()).toBe(false);
expect(findPopover().exists()).toBe(false);
});
}
},
);
it.each` it.each`
agentPath | hasLink | lineNumber agentPath | hasLink | lineNumber
${'.gitlab/agents/agent-1'} | ${true} | ${0} ${'.gitlab/agents/agent-1'} | ${true} | ${0}
${'.gitlab/agents/agent-2'} | ${false} | ${1} ${'.gitlab/agents/agent-2'} | ${false} | ${1}
`('displays config file path', ({ agentPath, hasLink, lineNumber }) => { `(
'displays config file path as "$agentPath" at line $lineNumber',
({ agentPath, hasLink, lineNumber }) => {
const findLink = findConfiguration(lineNumber).find(GlLink); const findLink = findConfiguration(lineNumber).find(GlLink);
expect(findLink.exists()).toBe(hasLink); expect(findLink.exists()).toBe(hasLink);
expect(findConfiguration(lineNumber).text()).toBe(agentPath); expect(findConfiguration(lineNumber).text()).toBe(agentPath);
}); },
);
it('displays actions menu for each agent', () => { it('displays actions menu for each agent', () => {
expect(findDeleteAgentButton()).toHaveLength(3); expect(findDeleteAgentButton()).toHaveLength(5);
}); });
}); });
}); });
...@@ -40,7 +40,13 @@ describe('Agents', () => { ...@@ -40,7 +40,13 @@ describe('Agents', () => {
data: { data: {
project: { project: {
id: '1', id: '1',
clusterAgents: { nodes: agents, pageInfo, tokens: { nodes: [] }, count }, clusterAgents: {
nodes: agents,
pageInfo,
connections: { nodes: [] },
tokens: { nodes: [] },
count,
},
repository: { tree: { trees: { nodes: trees, pageInfo } } }, repository: { tree: { trees: { nodes: trees, pageInfo } } },
}, },
}, },
...@@ -89,12 +95,14 @@ describe('Agents', () => { ...@@ -89,12 +95,14 @@ describe('Agents', () => {
id: '1', id: '1',
name: 'agent-1', name: 'agent-1',
webPath: '/agent-1', webPath: '/agent-1',
connections: null,
tokens: null, tokens: null,
}, },
{ {
id: '2', id: '2',
name: 'agent-2', name: 'agent-2',
webPath: '/agent-2', webPath: '/agent-2',
connections: null,
tokens: { tokens: {
nodes: [ nodes: [
{ {
...@@ -125,6 +133,7 @@ describe('Agents', () => { ...@@ -125,6 +133,7 @@ describe('Agents', () => {
configFolder: undefined, configFolder: undefined,
status: 'unused', status: 'unused',
lastContact: null, lastContact: null,
connections: null,
tokens: null, tokens: null,
}, },
{ {
...@@ -138,6 +147,7 @@ describe('Agents', () => { ...@@ -138,6 +147,7 @@ describe('Agents', () => {
webPath: '/agent-2', webPath: '/agent-2',
status: 'active', status: 'active',
lastContact: new Date(testDate).getTime(), lastContact: new Date(testDate).getTime(),
connections: null,
tokens: { tokens: {
nodes: [ nodes: [
{ {
......
import { ACTIVE_CONNECTION_TIME } from '~/clusters_list/constants';
export const agentConfigurationsResponse = { export const agentConfigurationsResponse = {
data: { data: {
project: { project: {
...@@ -10,3 +12,113 @@ export const agentConfigurationsResponse = { ...@@ -10,3 +12,113 @@ export const agentConfigurationsResponse = {
}, },
}, },
}; };
export const connectedTimeNow = new Date();
export const connectedTimeInactive = new Date(connectedTimeNow.getTime() - ACTIVE_CONNECTION_TIME);
export const clusterAgents = [
{
name: 'agent-1',
id: 'agent-1-id',
configFolder: {
webPath: '/agent/full/path',
},
webPath: '/agent-1',
status: 'unused',
lastContact: null,
tokens: null,
},
{
name: 'agent-2',
id: 'agent-2-id',
webPath: '/agent-2',
status: 'active',
lastContact: connectedTimeNow.getTime(),
connections: {
nodes: [
{
metadata: { version: 'v14.8' },
},
{
metadata: { version: 'v14.8' },
},
],
},
tokens: {
nodes: [
{
lastUsedAt: connectedTimeNow,
},
],
},
},
{
name: 'agent-3',
id: 'agent-3-id',
webPath: '/agent-3',
status: 'inactive',
lastContact: connectedTimeInactive.getTime(),
connections: {
nodes: [
{
metadata: { version: 'v14.5' },
},
],
},
tokens: {
nodes: [
{
lastUsedAt: connectedTimeInactive,
},
],
},
},
{
name: 'agent-4',
id: 'agent-4-id',
webPath: '/agent-4',
status: 'inactive',
lastContact: connectedTimeInactive.getTime(),
connections: {
nodes: [
{
metadata: { version: 'v14.7' },
},
{
metadata: { version: 'v14.8' },
},
],
},
tokens: {
nodes: [
{
lastUsedAt: connectedTimeInactive,
},
],
},
},
{
name: 'agent-5',
id: 'agent-5-id',
webPath: '/agent-5',
status: 'inactive',
lastContact: connectedTimeInactive.getTime(),
connections: {
nodes: [
{
metadata: { version: 'v14.5' },
},
{
metadata: { version: 'v14.3' },
},
],
},
tokens: {
nodes: [
{
lastUsedAt: connectedTimeInactive,
},
],
},
},
];
...@@ -10,6 +10,9 @@ const token = { ...@@ -10,6 +10,9 @@ const token = {
const tokens = { const tokens = {
nodes: [token], nodes: [token],
}; };
const connections = {
nodes: [],
};
const pageInfo = { const pageInfo = {
endCursor: '', endCursor: '',
hasNextPage: false, hasNextPage: false,
...@@ -23,6 +26,7 @@ export const createAgentResponse = { ...@@ -23,6 +26,7 @@ export const createAgentResponse = {
createClusterAgent: { createClusterAgent: {
clusterAgent: { clusterAgent: {
...agent, ...agent,
connections,
tokens, tokens,
}, },
errors: [], errors: [],
...@@ -35,6 +39,7 @@ export const createAgentErrorResponse = { ...@@ -35,6 +39,7 @@ export const createAgentErrorResponse = {
createClusterAgent: { createClusterAgent: {
clusterAgent: { clusterAgent: {
...agent, ...agent,
connections,
tokens, tokens,
}, },
errors: ['could not create agent'], errors: ['could not create agent'],
...@@ -66,7 +71,7 @@ export const getAgentResponse = { ...@@ -66,7 +71,7 @@ export const getAgentResponse = {
data: { data: {
project: { project: {
id: 'project-1', id: 'project-1',
clusterAgents: { nodes: [{ ...agent, tokens }], pageInfo, count }, clusterAgents: { nodes: [{ ...agent, connections, tokens }], pageInfo, count },
repository: { repository: {
tree: { tree: {
trees: { nodes: [{ ...agent, path: null }], pageInfo }, trees: { nodes: [{ ...agent, path: null }], pageInfo },
......
...@@ -152,6 +152,10 @@ RSpec.describe ClustersHelper do ...@@ -152,6 +152,10 @@ RSpec.describe ClustersHelper do
it 'displays kas address' do it 'displays kas address' do
expect(subject[:kas_address]).to eq(Gitlab::Kas.external_url) expect(subject[:kas_address]).to eq(Gitlab::Kas.external_url)
end end
it 'displays GitLab version' do
expect(subject[:gitlab_version]).to eq(Gitlab.version_info)
end
end end
describe '#js_cluster_new' do describe '#js_cluster_new' do
......
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