Commit e1dd2d51 authored by Phil Hughes's avatar Phil Hughes

Merge branch...

Merge branch '347240-change-the-ui-to-support-agent-registration-with-and-without-a-config' into 'master'

Change the UI to support agent registration with and without a config

See merge request gitlab-org/gitlab!82036
parents bfeb5f25 388f87fa
<script> <script>
import { GlLink, GlTable, GlIcon, GlSprintf, GlTooltip, GlPopover } from '@gitlab/ui'; import {
GlLink,
GlTable,
GlIcon,
GlSprintf,
GlTooltip,
GlTooltipDirective,
GlPopover,
} from '@gitlab/ui';
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';
...@@ -19,12 +27,18 @@ export default { ...@@ -19,12 +27,18 @@ export default {
TimeAgoTooltip, TimeAgoTooltip,
DeleteAgentButton, DeleteAgentButton,
}, },
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [timeagoMixin], mixins: [timeagoMixin],
AGENT_STATUSES, AGENT_STATUSES,
troubleshootingLink: helpPagePath('user/clusters/agent/troubleshooting'), troubleshootingLink: helpPagePath('user/clusters/agent/troubleshooting'),
versionUpdateLink: helpPagePath('user/clusters/agent/install/index', { versionUpdateLink: helpPagePath('user/clusters/agent/install/index', {
anchor: 'update-the-agent-version', anchor: 'update-the-agent-version',
}), }),
configHelpLink: helpPagePath('user/clusters/agent/install/index', {
anchor: 'create-an-agent-without-configuration-file',
}),
inject: ['gitlabVersion'], inject: ['gitlabVersion'],
props: { props: {
agents: { agents: {
...@@ -256,7 +270,16 @@ export default { ...@@ -256,7 +270,16 @@ export default {
{{ getAgentConfigPath(item.name) }} {{ getAgentConfigPath(item.name) }}
</gl-link> </gl-link>
<span v-else>{{ getAgentConfigPath(item.name) }}</span> <span v-else
>{{ $options.i18n.defaultConfigText }}
<gl-link
v-gl-tooltip
:href="$options.configHelpLink"
:title="$options.i18n.defaultConfigTooltip"
:aria-label="$options.i18n.defaultConfigTooltip"
class="gl-vertical-align-middle"
><gl-icon name="question" :size="14" /></gl-link
></span>
</span> </span>
</template> </template>
......
...@@ -116,9 +116,6 @@ export default { ...@@ -116,9 +116,6 @@ export default {
}, },
}, },
methods: { methods: {
reloadAgents() {
this.$apollo.queries.agents.refetch();
},
nextPage() { nextPage() {
this.cursor = { this.cursor = {
first: MAX_LIST_COUNT, first: MAX_LIST_COUNT,
......
<script> <script>
import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; import {
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
GlSearchBoxByType,
GlSprintf,
} from '@gitlab/ui';
import { I18N_AVAILABLE_AGENTS_DROPDOWN } from '../constants'; import { I18N_AVAILABLE_AGENTS_DROPDOWN } from '../constants';
export default { export default {
...@@ -8,6 +14,9 @@ export default { ...@@ -8,6 +14,9 @@ export default {
components: { components: {
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
GlDropdownDivider,
GlSearchBoxByType,
GlSprintf,
}, },
props: { props: {
isRegistering: { isRegistering: {
...@@ -22,6 +31,7 @@ export default { ...@@ -22,6 +31,7 @@ export default {
data() { data() {
return { return {
selectedAgent: null, selectedAgent: null,
searchTerm: '',
}; };
}, },
computed: { computed: {
...@@ -34,22 +44,45 @@ export default { ...@@ -34,22 +44,45 @@ export default {
return this.selectedAgent; return this.selectedAgent;
}, },
shouldRenderCreateButton() {
return this.searchTerm && !this.availableAgents.includes(this.searchTerm);
},
filteredResults() {
const lowerCasedSearchTerm = this.searchTerm.toLowerCase();
return this.availableAgents.filter((resultString) =>
resultString.toLowerCase().includes(lowerCasedSearchTerm),
);
},
}, },
methods: { methods: {
selectAgent(agent) { selectAgent(agent) {
this.$emit('agentSelected', agent); this.$emit('agentSelected', agent);
this.selectedAgent = agent; this.selectedAgent = agent;
this.clearSearch();
}, },
isSelected(agent) { isSelected(agent) {
return this.selectedAgent === agent; return this.selectedAgent === agent;
}, },
clearSearch() {
this.searchTerm = '';
},
focusSearch() {
this.$refs.searchInput.focusInput();
},
handleShow() {
this.clearSearch();
this.focusSearch();
},
}, },
}; };
</script> </script>
<template> <template>
<gl-dropdown :text="dropdownText" :loading="isRegistering"> <gl-dropdown :text="dropdownText" :loading="isRegistering" @shown="handleShow">
<template #header>
<gl-search-box-by-type ref="searchInput" v-model.trim="searchTerm" />
</template>
<gl-dropdown-item <gl-dropdown-item
v-for="agent in availableAgents" v-for="agent in filteredResults"
:key="agent" :key="agent"
:is-checked="isSelected(agent)" :is-checked="isSelected(agent)"
is-check-item is-check-item
...@@ -57,5 +90,16 @@ export default { ...@@ -57,5 +90,16 @@ export default {
> >
{{ agent }} {{ agent }}
</gl-dropdown-item> </gl-dropdown-item>
<gl-dropdown-item v-if="!filteredResults.length" ref="noMatchingResults">{{
$options.i18n.noResults
}}</gl-dropdown-item>
<template v-if="shouldRenderCreateButton">
<gl-dropdown-divider />
<gl-dropdown-item data-testid="create-config-button" @click="selectAgent(searchTerm)">
<gl-sprintf :message="$options.i18n.createButton">
<template #searchTerm>{{ searchTerm }}</template>
</gl-sprintf>
</gl-dropdown-item>
</template>
</gl-dropdown> </gl-dropdown>
</template> </template>
...@@ -35,6 +35,7 @@ const trackingMixin = Tracking.mixin({ label: EVENT_LABEL_MODAL }); ...@@ -35,6 +35,7 @@ const trackingMixin = Tracking.mixin({ label: EVENT_LABEL_MODAL });
export default { export default {
modalId: INSTALL_AGENT_MODAL_ID, modalId: INSTALL_AGENT_MODAL_ID,
i18n: I18N_AGENT_MODAL,
EVENT_ACTIONS_OPEN, EVENT_ACTIONS_OPEN,
EVENT_ACTIONS_CLICK, EVENT_ACTIONS_CLICK,
EVENT_LABEL_MODAL, EVENT_LABEL_MODAL,
...@@ -45,7 +46,6 @@ export default { ...@@ -45,7 +46,6 @@ export default {
anchor: 'advanced-installation', anchor: 'advanced-installation',
}), }),
enableKasPath: helpPagePath('administration/clusters/kas'), enableKasPath: helpPagePath('administration/clusters/kas'),
installAgentPath: helpPagePath('user/clusters/agent/install/index'),
registerAgentPath: helpPagePath('user/clusters/agent/install/index', { registerAgentPath: helpPagePath('user/clusters/agent/install/index', {
anchor: 'register-an-agent-with-gitlab', anchor: 'register-an-agent-with-gitlab',
}), }),
...@@ -109,10 +109,10 @@ export default { ...@@ -109,10 +109,10 @@ export default {
return !this.registering && this.agentName !== null; return !this.registering && this.agentName !== null;
}, },
canCancel() { canCancel() {
return !this.registered && !this.registering && this.isAgentRegistrationModal; return !this.registered && !this.registering && !this.kasDisabled;
}, },
canRegister() { canRegister() {
return !this.registered && this.isAgentRegistrationModal; return !this.registered && !this.kasDisabled;
}, },
agentRegistrationCommand() { agentRegistrationCommand() {
return generateAgentRegistrationCommand(this.agentToken, this.kasAddress); return generateAgentRegistrationCommand(this.agentToken, this.kasAddress);
...@@ -125,32 +125,20 @@ export default { ...@@ -125,32 +125,20 @@ export default {
projectPath: this.projectPath, projectPath: this.projectPath,
}; };
}, },
i18n() {
return I18N_AGENT_MODAL[this.modalType];
},
repositoryPath() { repositoryPath() {
return `/${this.projectPath}`; return `/${this.projectPath}`;
}, },
modalType() { modalType() {
return !this.availableAgents?.length && !this.registered return this.kasDisabled ? MODAL_TYPE_EMPTY : MODAL_TYPE_REGISTER;
? MODAL_TYPE_EMPTY
: MODAL_TYPE_REGISTER;
}, },
modalSize() { modalSize() {
return this.isEmptyStateModal ? 'sm' : 'md'; return this.kasDisabled ? 'sm' : 'md';
},
isEmptyStateModal() {
return this.modalType === MODAL_TYPE_EMPTY;
},
isAgentRegistrationModal() {
return this.modalType === MODAL_TYPE_REGISTER;
},
isKasEnabledInEmptyStateModal() {
return this.isEmptyStateModal && !this.kasDisabled;
}, },
}, },
methods: { methods: {
setAgentName(name) { setAgentName(name) {
this.error = null;
this.agentName = name; this.agentName = name;
this.track(EVENT_ACTIONS_SELECT); this.track(EVENT_ACTIONS_SELECT);
}, },
...@@ -244,7 +232,7 @@ export default { ...@@ -244,7 +232,7 @@ export default {
if (error) { if (error) {
this.error = error.message; this.error = error.message;
} else { } else {
this.error = this.i18n.unknownError; this.error = this.$options.i18n.unknownError;
} }
} finally { } finally {
this.registering = false; this.registering = false;
...@@ -258,22 +246,21 @@ export default { ...@@ -258,22 +246,21 @@ export default {
<gl-modal <gl-modal
ref="modal" ref="modal"
:modal-id="$options.modalId" :modal-id="$options.modalId"
:title="i18n.modalTitle" :title="$options.i18n.modalTitle"
:size="modalSize" :size="modalSize"
static static
lazy lazy
@hidden="resetModal" @hidden="resetModal"
@show="track($options.EVENT_ACTIONS_OPEN, { property: modalType })" @show="track($options.EVENT_ACTIONS_OPEN, { property: modalType })"
> >
<template v-if="isAgentRegistrationModal"> <template v-if="!kasDisabled">
<template v-if="!registered"> <template v-if="!registered">
<p> <p class="gl-mb-0">
<strong>{{ i18n.selectAgentTitle }}</strong> <gl-sprintf :message="$options.i18n.modalBody">
</p> <template #link="{ content }">
<gl-link :href="repositoryPath">{{ content }}</gl-link>
<p class="gl-mb-0">{{ i18n.selectAgentBody }}</p> </template>
<p> </gl-sprintf>
<gl-link :href="$options.registerAgentPath"> {{ i18n.learnMoreLink }}</gl-link>
</p> </p>
<form> <form>
...@@ -287,8 +274,16 @@ export default { ...@@ -287,8 +274,16 @@ export default {
</gl-form-group> </gl-form-group>
</form> </form>
<p>
<gl-link :href="$options.registerAgentPath"> {{ $options.i18n.learnMoreLink }}</gl-link>
</p>
<p v-if="error"> <p v-if="error">
<gl-alert :title="i18n.registrationErrorTitle" variant="danger" :dismissible="false"> <gl-alert
:title="$options.i18n.registrationErrorTitle"
variant="danger"
:dismissible="false"
>
{{ error }} {{ error }}
</gl-alert> </gl-alert>
</p> </p>
...@@ -296,11 +291,11 @@ export default { ...@@ -296,11 +291,11 @@ export default {
<template v-else> <template v-else>
<p> <p>
<strong>{{ i18n.tokenTitle }}</strong> <strong>{{ $options.i18n.tokenTitle }}</strong>
</p> </p>
<p> <p>
<gl-sprintf :message="i18n.tokenBody"> <gl-sprintf :message="$options.i18n.tokenBody">
<template #link="{ content }"> <template #link="{ content }">
<gl-link :href="$options.basicInstallPath" target="_blank"> {{ content }}</gl-link> <gl-link :href="$options.basicInstallPath" target="_blank"> {{ content }}</gl-link>
</template> </template>
...@@ -308,8 +303,12 @@ export default { ...@@ -308,8 +303,12 @@ export default {
</p> </p>
<p> <p>
<gl-alert :title="i18n.tokenSingleUseWarningTitle" variant="warning" :dismissible="false"> <gl-alert
{{ i18n.tokenSingleUseWarningBody }} :title="$options.i18n.tokenSingleUseWarningTitle"
variant="warning"
:dismissible="false"
>
{{ $options.i18n.tokenSingleUseWarningBody }}
</gl-alert> </gl-alert>
</p> </p>
...@@ -318,7 +317,7 @@ export default { ...@@ -318,7 +317,7 @@ export default {
<template #append> <template #append>
<modal-copy-button <modal-copy-button
:text="agentToken" :text="agentToken"
:title="i18n.copyToken" :title="$options.i18n.copyToken"
:modal-id="$options.modalId" :modal-id="$options.modalId"
/> />
</template> </template>
...@@ -326,11 +325,11 @@ export default { ...@@ -326,11 +325,11 @@ export default {
</p> </p>
<p> <p>
<strong>{{ i18n.basicInstallTitle }}</strong> <strong>{{ $options.i18n.basicInstallTitle }}</strong>
</p> </p>
<p> <p>
{{ i18n.basicInstallBody }} {{ $options.i18n.basicInstallBody }}
</p> </p>
<p> <p>
...@@ -338,11 +337,11 @@ export default { ...@@ -338,11 +337,11 @@ export default {
</p> </p>
<p> <p>
<strong>{{ i18n.advancedInstallTitle }}</strong> <strong>{{ $options.i18n.advancedInstallTitle }}</strong>
</p> </p>
<p> <p>
<gl-sprintf :message="i18n.advancedInstallBody"> <gl-sprintf :message="$options.i18n.advancedInstallBody">
<template #link="{ content }"> <template #link="{ content }">
<gl-link :href="$options.advancedInstallPath" target="_blank"> {{ content }}</gl-link> <gl-link :href="$options.advancedInstallPath" target="_blank"> {{ content }}</gl-link>
</template> </template>
...@@ -353,24 +352,16 @@ export default { ...@@ -353,24 +352,16 @@ export default {
<template v-else> <template v-else>
<div class="gl-text-center gl-mb-5"> <div class="gl-text-center gl-mb-5">
<img :alt="i18n.altText" :src="emptyStateImage" height="100" /> <img :alt="$options.i18n.altText" :src="emptyStateImage" height="100" />
</div> </div>
<p v-if="kasDisabled"> <p v-if="kasDisabled">
<gl-sprintf :message="i18n.enableKasText"> <gl-sprintf :message="$options.i18n.enableKasText">
<template #link="{ content }"> <template #link="{ content }">
<gl-link :href="$options.enableKasPath">{{ content }}</gl-link> <gl-link :href="$options.enableKasPath">{{ content }}</gl-link>
</template> </template>
</gl-sprintf> </gl-sprintf>
</p> </p>
<p v-else>
<gl-sprintf :message="i18n.modalBody">
<template #link="{ content }">
<gl-link :href="$options.installAgentPath">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
</template> </template>
<template #modal-footer> <template #modal-footer>
...@@ -382,7 +373,7 @@ export default { ...@@ -382,7 +373,7 @@ export default {
:data-track-label="$options.EVENT_LABEL_MODAL" :data-track-label="$options.EVENT_LABEL_MODAL"
data-track-property="close" data-track-property="close"
@click="closeModal" @click="closeModal"
>{{ i18n.close }} >{{ $options.i18n.close }}
</gl-button> </gl-button>
<gl-button <gl-button
...@@ -391,7 +382,7 @@ export default { ...@@ -391,7 +382,7 @@ export default {
:data-track-label="$options.EVENT_LABEL_MODAL" :data-track-label="$options.EVENT_LABEL_MODAL"
data-track-property="cancel" data-track-property="cancel"
@click="closeModal" @click="closeModal"
>{{ i18n.cancel }} >{{ $options.i18n.cancel }}
</gl-button> </gl-button>
<gl-button <gl-button
...@@ -403,25 +394,16 @@ export default { ...@@ -403,25 +394,16 @@ export default {
:data-track-label="$options.EVENT_LABEL_MODAL" :data-track-label="$options.EVENT_LABEL_MODAL"
data-track-property="register" data-track-property="register"
@click="registerAgent" @click="registerAgent"
>{{ i18n.registerAgentButton }} >{{ $options.i18n.registerAgentButton }}
</gl-button> </gl-button>
<gl-button <gl-button
v-if="isEmptyStateModal" v-if="kasDisabled"
:data-track-action="$options.EVENT_ACTIONS_CLICK" :data-track-action="$options.EVENT_ACTIONS_CLICK"
:data-track-label="$options.EVENT_LABEL_MODAL" :data-track-label="$options.EVENT_LABEL_MODAL"
data-track-property="done" data-track-property="done"
@click="closeModal" @click="closeModal"
>{{ i18n.done }} >{{ $options.i18n.close }}
</gl-button>
<gl-button
v-if="isKasEnabledInEmptyStateModal"
:href="repositoryPath"
variant="confirm"
category="primary"
data-testid="agent-primary-button"
>{{ i18n.primaryButton }}
</gl-button> </gl-button>
</template> </template>
</gl-modal> </gl-modal>
......
...@@ -83,21 +83,24 @@ export const I18N_AGENT_TABLE = { ...@@ -83,21 +83,24 @@ export const I18N_AGENT_TABLE = {
), ),
versionMismatchOutdatedTitle: s__('ClusterAgents|Agent version mismatch and update'), versionMismatchOutdatedTitle: s__('ClusterAgents|Agent version mismatch and update'),
viewDocsText: s__('ClusterAgents|How to update an agent?'), viewDocsText: s__('ClusterAgents|How to update an agent?'),
defaultConfigText: s__('ClusterAgents|Default configuration'),
defaultConfigTooltip: s__('ClusterAgents|What is default configuration?'),
}; };
export const I18N_AGENT_MODAL = { export const I18N_AGENT_MODAL = {
agent_registration: {
registerAgentButton: s__('ClusterAgents|Register'), registerAgentButton: s__('ClusterAgents|Register'),
close: __('Close'), close: __('Close'),
cancel: __('Cancel'), cancel: __('Cancel'),
modalTitle: s__('ClusterAgents|Connect a cluster through an agent'), modalTitle: s__('ClusterAgents|Connect a cluster through an agent'),
selectAgentTitle: s__('ClusterAgents|Select an agent to register with GitLab'), modalBody: s__(
selectAgentBody: s__( 'ClusterAgents|Add an agent configuration file to %{linkStart}this repository%{linkEnd} and select it, or create a new one to register with GitLab:',
'ClusterAgents|Register an agent to generate a token that will be used to install the agent on your cluster in the next step.',
), ),
learnMoreLink: s__('ClusterAgents|How to register an agent?'), enableKasText: s__(
"ClusterAgents|Your instance doesn't have the %{linkStart}GitLab Agent Server (KAS)%{linkEnd} set up. Ask a GitLab Administrator to install it.",
),
altText: s__('ClusterAgents|GitLab Agent for Kubernetes'),
learnMoreLink: s__('ClusterAgents|How do I register an agent?'),
copyToken: s__('ClusterAgents|Copy token'), copyToken: s__('ClusterAgents|Copy token'),
tokenTitle: s__('ClusterAgents|Registration token'), tokenTitle: s__('ClusterAgents|Registration token'),
tokenBody: s__( tokenBody: s__(
...@@ -123,26 +126,15 @@ export const I18N_AGENT_MODAL = { ...@@ -123,26 +126,15 @@ export const I18N_AGENT_MODAL = {
registrationErrorTitle: s__('ClusterAgents|Failed to register an agent'), registrationErrorTitle: s__('ClusterAgents|Failed to register an agent'),
unknownError: s__('ClusterAgents|An unknown error occurred. Please try again.'), unknownError: s__('ClusterAgents|An unknown error occurred. Please try again.'),
},
empty_state: {
modalTitle: s__('ClusterAgents|Connect your cluster through an agent'),
modalBody: s__(
"ClusterAgents|To install a new agent, first add the agent's configuration file to this repository. %{linkStart}Learn more about installing GitLab Agent.%{linkEnd}",
),
enableKasText: s__(
"ClusterAgents|Your instance doesn't have the %{linkStart}GitLab Agent Server (KAS)%{linkEnd} set up. Ask a GitLab Administrator to install it.",
),
altText: s__('ClusterAgents|GitLab Agent for Kubernetes'),
primaryButton: s__('ClusterAgents|Go to the repository files'),
done: __('Cancel'),
},
}; };
export const KAS_DISABLED_ERROR = 'Gitlab::Kas::Client::ConfigurationError'; export const KAS_DISABLED_ERROR = 'Gitlab::Kas::Client::ConfigurationError';
export const I18N_AVAILABLE_AGENTS_DROPDOWN = { export const I18N_AVAILABLE_AGENTS_DROPDOWN = {
selectAgent: s__('ClusterAgents|Select an agent'), selectAgent: s__('ClusterAgents|Select an agent or enter a name to create new'),
registeringAgent: s__('ClusterAgents|Registering Agent'), registeringAgent: s__('ClusterAgents|Registering agent'),
noResults: __('No matching results'),
createButton: s__('ClusterAgents|Create agent: %{searchTerm}'),
}; };
export const AGENT_STATUSES = { export const AGENT_STATUSES = {
......
import produce from 'immer'; import produce from 'immer';
import { getAgentConfigPath } from '../clusters_util';
export const hasErrors = ({ errors = [] }) => errors?.length; export const hasErrors = ({ errors = [] }) => errors?.length;
...@@ -12,17 +11,8 @@ export function addAgentToStore(store, createClusterAgent, query, variables) { ...@@ -12,17 +11,8 @@ export function addAgentToStore(store, createClusterAgent, query, variables) {
}); });
const data = produce(sourceData, (draftData) => { const data = produce(sourceData, (draftData) => {
const configuration = {
id: clusterAgent.id,
name: clusterAgent.name,
path: getAgentConfigPath(clusterAgent.name),
webPath: clusterAgent.webPath,
__typename: 'TreeEntry',
};
draftData.project.clusterAgents.nodes.push(clusterAgent); draftData.project.clusterAgents.nodes.push(clusterAgent);
draftData.project.clusterAgents.count += 1; draftData.project.clusterAgents.count += 1;
draftData.project.repository.tree.trees.nodes.push(configuration);
}); });
store.writeQuery({ store.writeQuery({
......
...@@ -7,9 +7,7 @@ query getAgents( ...@@ -7,9 +7,7 @@ query getAgents(
$first: Int $first: Int
$last: Int $last: Int
$afterAgent: String $afterAgent: String
$afterTree: String
$beforeAgent: String $beforeAgent: String
$beforeTree: String
) { ) {
project(fullPath: $projectPath) { project(fullPath: $projectPath) {
id id
...@@ -27,17 +25,13 @@ query getAgents( ...@@ -27,17 +25,13 @@ query getAgents(
repository { repository {
tree(path: ".gitlab/agents", ref: $defaultBranchName) { tree(path: ".gitlab/agents", ref: $defaultBranchName) {
trees(first: $first, last: $last, after: $afterTree, before: $beforeTree) { trees {
nodes { nodes {
id id
name name
path path
webPath webPath
} }
pageInfo {
...PageInfo
}
} }
} }
} }
......
...@@ -27,18 +27,47 @@ Before you can install the agent in your cluster, you need: ...@@ -27,18 +27,47 @@ Before you can install the agent in your cluster, you need:
To install the agent in your cluster: To install the agent in your cluster:
1. [Create an agent configuration file called `config.yaml`](#create-an-agent-configuration-file).
1. [Register the agent with GitLab](#register-the-agent-with-gitlab). 1. [Register the agent with GitLab](#register-the-agent-with-gitlab).
1. [Install the agent in your cluster](#install-the-agent-in-the-cluster). 1. [Install the agent in your cluster](#install-the-agent-in-the-cluster).
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> Watch a GitLab 14.2 [walk-through of this process](https://www.youtube.com/watch?v=XuBpKtsgGkE). <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> Watch a GitLab 14.2 [walk-through of this process](https://www.youtube.com/watch?v=XuBpKtsgGkE).
### Register the agent with GitLab
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5786) in GitLab 14.1, you can create a new agent record directly from the GitLab UI.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/347240) in GitLab 14.9, the agent can be registered without creating an agent configuration file.
You must register an agent with GitLab.
Prerequisites:
- For a [GitLab CI/CD workflow](../ci_cd_tunnel.md), ensure that
[GitLab CI/CD is enabled](../../../../ci/enable_or_disable_ci.md#enable-cicd-in-a-project).
To register an agent with GitLab:
1. On the top bar, select **Menu > Projects** and find your project.
1. From the left sidebar, select **Infrastructure > Kubernetes clusters**.
1. Select **Actions**.
1. From the **Select an agent** dropdown list:
- If you want to create a configuration with CI/CD defaults, type a name for the agent.
- If you already have an [agent configuration file](#create-an-agent-configuration-file), select it from the list.
1. Select **Register an agent**.
1. GitLab generates a registration token for this agent. Securely store this secret token. You need it to install the agent in 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 when you use the one-liner installation method to install the agent in your cluster.
### Create an agent configuration file ### Create an agent configuration file
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/259669) in GitLab 13.7, the agent configuration file can be added to multiple directories (or subdirectories) of the repository. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/259669) in GitLab 13.7, the agent configuration file can be added to multiple directories (or subdirectories) of the repository.
> - Group authorization was [introduced](https://gitlab.com/groups/gitlab-org/-/epics/5784) in GitLab 14.3. > - Group authorization was [introduced](https://gitlab.com/groups/gitlab-org/-/epics/5784) in GitLab 14.3.
In a GitLab project, in the repository, create a file called `config.yaml` at this path: You can use an agent configuration file to specify details about your implementation.
Creating a file is optional but is needed if:
- You use [a GitOps workflow](../gitops.md#gitops-configuration-reference) and you want a more advanced configuration.
- You use a GitLab CI/CD workflow. In that workflow, you must [authorize the agent](../ci_cd_tunnel.md#authorize-the-agent).
To create an agent configuration file, go to the GitLab project. In the repository, create a file called `config.yaml` at this path:
```plaintext ```plaintext
.gitlab/agents/<agent-name>/config.yaml .gitlab/agents/<agent-name>/config.yaml
...@@ -54,28 +83,6 @@ The agent bootstraps with the GitLab installation URL and an authentication toke ...@@ -54,28 +83,6 @@ The agent bootstraps with the GitLab installation URL and an authentication toke
and you provide the rest of the configuration in your repository, following and you provide the rest of the configuration in your repository, following
Infrastructure as Code (IaaC) best practices. Infrastructure as Code (IaaC) best practices.
### Register the agent with GitLab
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5786) in GitLab 14.1, you can create a new agent record directly from the GitLab UI.
Now that you've created your agent configuration file, register it
with GitLab.
When you register the agent, GitLab generates a token that you need to
install the agent in your cluster.
Prerequisite when using a [GitLab CI/CD workflow](../ci_cd_tunnel.md):
- In the project that has the agent configuration file, ensure that [GitLab CI/CD is enabled](../../../../ci/enable_or_disable_ci.md#enable-cicd-in-a-project).
To register the agent with GitLab:
1. On the top bar, select **Menu > Projects** and find the project that has your agent configuration file.
1. From the left sidebar, select **Infrastructure > Kubernetes clusters**.
1. Select **Actions**.
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. You need it to install the agent in 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 when you use the one-liner installation method to install the agent in your cluster.
### Install the agent in the cluster ### Install the agent in the cluster
To connect your cluster to GitLab, install the registered agent To connect your cluster to GitLab, install the registered agent
......
...@@ -7670,6 +7670,9 @@ msgstr "" ...@@ -7670,6 +7670,9 @@ msgstr ""
msgid "ClusterAgents|Actions" msgid "ClusterAgents|Actions"
msgstr "" msgstr ""
msgid "ClusterAgents|Add an agent configuration file to %{linkStart}this repository%{linkEnd} and select it, or create a new one to register with GitLab:"
msgstr ""
msgid "ClusterAgents|Advanced installation methods" msgid "ClusterAgents|Advanced installation methods"
msgstr "" msgstr ""
...@@ -7736,9 +7739,6 @@ msgstr "" ...@@ -7736,9 +7739,6 @@ msgstr ""
msgid "ClusterAgents|Connect with the GitLab Agent" msgid "ClusterAgents|Connect with the GitLab Agent"
msgstr "" msgstr ""
msgid "ClusterAgents|Connect your cluster through an agent"
msgstr ""
msgid "ClusterAgents|Connected" msgid "ClusterAgents|Connected"
msgstr "" msgstr ""
...@@ -7751,6 +7751,9 @@ msgstr "" ...@@ -7751,6 +7751,9 @@ msgstr ""
msgid "ClusterAgents|Create a new cluster" msgid "ClusterAgents|Create a new cluster"
msgstr "" msgstr ""
msgid "ClusterAgents|Create agent: %{searchTerm}"
msgstr ""
msgid "ClusterAgents|Created by" msgid "ClusterAgents|Created by"
msgstr "" msgstr ""
...@@ -7760,6 +7763,9 @@ msgstr "" ...@@ -7760,6 +7763,9 @@ msgstr ""
msgid "ClusterAgents|Date created" msgid "ClusterAgents|Date created"
msgstr "" msgstr ""
msgid "ClusterAgents|Default configuration"
msgstr ""
msgid "ClusterAgents|Delete" msgid "ClusterAgents|Delete"
msgstr "" msgstr ""
...@@ -7790,10 +7796,7 @@ msgstr "" ...@@ -7790,10 +7796,7 @@ msgstr ""
msgid "ClusterAgents|Give feedback" msgid "ClusterAgents|Give feedback"
msgstr "" msgstr ""
msgid "ClusterAgents|Go to the repository files" msgid "ClusterAgents|How do I register an agent?"
msgstr ""
msgid "ClusterAgents|How to register an agent?"
msgstr "" msgstr ""
msgid "ClusterAgents|How to update an agent?" msgid "ClusterAgents|How to update an agent?"
...@@ -7844,10 +7847,7 @@ msgstr "" ...@@ -7844,10 +7847,7 @@ msgstr ""
msgid "ClusterAgents|Register" msgid "ClusterAgents|Register"
msgstr "" msgstr ""
msgid "ClusterAgents|Register an agent to generate a token that will be used to install the agent on your cluster in the next step." msgid "ClusterAgents|Registering agent"
msgstr ""
msgid "ClusterAgents|Registering Agent"
msgstr "" msgstr ""
msgid "ClusterAgents|Registration token" msgid "ClusterAgents|Registration token"
...@@ -7871,10 +7871,7 @@ msgstr "" ...@@ -7871,10 +7871,7 @@ msgstr ""
msgid "ClusterAgents|See Agent activity updates such as tokens created or revoked and clusters connected or not connected." msgid "ClusterAgents|See Agent activity updates such as tokens created or revoked and clusters connected or not connected."
msgstr "" msgstr ""
msgid "ClusterAgents|Select an agent" msgid "ClusterAgents|Select an agent or enter a name to create new"
msgstr ""
msgid "ClusterAgents|Select an agent to register with GitLab"
msgstr "" msgstr ""
msgid "ClusterAgents|Tell us what you think" msgid "ClusterAgents|Tell us what you think"
...@@ -7906,9 +7903,6 @@ msgstr "" ...@@ -7906,9 +7903,6 @@ msgstr ""
msgid "ClusterAgents|To delete the agent, type %{name} to confirm:" msgid "ClusterAgents|To delete the agent, type %{name} to confirm:"
msgstr "" msgstr ""
msgid "ClusterAgents|To install a new agent, first add the agent's configuration file to this repository. %{linkStart}Learn more about installing GitLab Agent.%{linkEnd}"
msgstr ""
msgid "ClusterAgents|Token created by %{userName}" msgid "ClusterAgents|Token created by %{userName}"
msgstr "" msgstr ""
...@@ -7930,6 +7924,9 @@ msgstr "" ...@@ -7930,6 +7924,9 @@ msgstr ""
msgid "ClusterAgents|What is GitLab Agent activity?" msgid "ClusterAgents|What is GitLab Agent activity?"
msgstr "" msgstr ""
msgid "ClusterAgents|What is default configuration?"
msgstr ""
msgid "ClusterAgents|You cannot see this token again after you close this window." msgid "ClusterAgents|You cannot see this token again after you close this window."
msgstr "" msgstr ""
......
...@@ -8,6 +8,9 @@ import { stubComponent } from 'helpers/stub_component'; ...@@ -8,6 +8,9 @@ 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'; import { clusterAgents, connectedTimeNow, connectedTimeInactive } from './mock_data';
const defaultConfigHelpUrl =
'/help/user/clusters/agent/install/index#create-an-agent-without-configuration-file';
const provideData = { const provideData = {
gitlabVersion: '14.8', gitlabVersion: '14.8',
}; };
...@@ -31,8 +34,8 @@ describe('AgentTable', () => { ...@@ -31,8 +34,8 @@ describe('AgentTable', () => {
let wrapper; let wrapper;
const findAgentLink = (at) => wrapper.findAllByTestId('cluster-agent-name-link').at(at); const findAgentLink = (at) => wrapper.findAllByTestId('cluster-agent-name-link').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 findStatusIcon = (at) => findStatusText(at).find(GlIcon);
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 findVersionText = (at) => wrapper.findAllByTestId('cluster-agent-version').at(at);
const findConfiguration = (at) => const findConfiguration = (at) =>
...@@ -141,16 +144,16 @@ describe('AgentTable', () => { ...@@ -141,16 +144,16 @@ describe('AgentTable', () => {
); );
it.each` it.each`
agentPath | hasLink | lineNumber agentConfig | link | lineNumber
${'.gitlab/agents/agent-1'} | ${true} | ${0} ${'.gitlab/agents/agent-1'} | ${'/agent/full/path'} | ${0}
${'.gitlab/agents/agent-2'} | ${false} | ${1} ${'Default configuration'} | ${defaultConfigHelpUrl} | ${1}
`( `(
'displays config file path as "$agentPath" at line $lineNumber', 'displays config file path as "$agentPath" at line $lineNumber',
({ agentPath, hasLink, lineNumber }) => { ({ agentConfig, link, lineNumber }) => {
const findLink = findConfiguration(lineNumber).find(GlLink); const findLink = findConfiguration(lineNumber).find(GlLink);
expect(findLink.exists()).toBe(hasLink); expect(findLink.attributes('href')).toBe(link);
expect(findConfiguration(lineNumber).text()).toBe(agentPath); expect(findConfiguration(lineNumber).text()).toBe(agentConfig);
}, },
); );
......
import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import AvailableAgentsDropdown from '~/clusters_list/components/available_agents_dropdown.vue'; import AvailableAgentsDropdown from '~/clusters_list/components/available_agents_dropdown.vue';
import { I18N_AVAILABLE_AGENTS_DROPDOWN } from '~/clusters_list/constants'; import { I18N_AVAILABLE_AGENTS_DROPDOWN } from '~/clusters_list/constants';
...@@ -9,11 +9,14 @@ describe('AvailableAgentsDropdown', () => { ...@@ -9,11 +9,14 @@ describe('AvailableAgentsDropdown', () => {
const i18n = I18N_AVAILABLE_AGENTS_DROPDOWN; const i18n = I18N_AVAILABLE_AGENTS_DROPDOWN;
const findDropdown = () => wrapper.findComponent(GlDropdown); const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findConfiguredAgentItem = () => findDropdownItems().at(0); const findFirstAgentItem = () => findDropdownItems().at(0);
const findSearchInput = () => wrapper.findComponent(GlSearchBoxByType);
const findCreateButton = () => wrapper.findByTestId('create-config-button');
const createWrapper = ({ propsData }) => { const createWrapper = ({ propsData }) => {
wrapper = shallowMount(AvailableAgentsDropdown, { wrapper = shallowMountExtended(AvailableAgentsDropdown, {
propsData, propsData,
stubs: { GlDropdown },
}); });
}; };
...@@ -23,7 +26,7 @@ describe('AvailableAgentsDropdown', () => { ...@@ -23,7 +26,7 @@ describe('AvailableAgentsDropdown', () => {
describe('there are agents available', () => { describe('there are agents available', () => {
const propsData = { const propsData = {
availableAgents: ['configured-agent'], availableAgents: ['configured-agent', 'search-agent', 'test-agent'],
isRegistering: false, isRegistering: false,
}; };
...@@ -35,9 +38,38 @@ describe('AvailableAgentsDropdown', () => { ...@@ -35,9 +38,38 @@ describe('AvailableAgentsDropdown', () => {
expect(findDropdown().props('text')).toBe(i18n.selectAgent); expect(findDropdown().props('text')).toBe(i18n.selectAgent);
}); });
describe('click events', () => { describe('search agent', () => {
it('renders search button', () => {
expect(findSearchInput().exists()).toBe(true);
});
it('renders all agents when search term is empty', () => {
expect(findDropdownItems()).toHaveLength(3);
});
it('renders only the agent searched for when the search item exists', async () => {
await findSearchInput().vm.$emit('input', 'search-agent');
expect(findDropdownItems()).toHaveLength(1);
expect(findFirstAgentItem().text()).toBe('search-agent');
});
it('renders create button when search started', async () => {
await findSearchInput().vm.$emit('input', 'new-agent');
expect(findCreateButton().exists()).toBe(true);
});
it("doesn't render create button when search item is found", async () => {
await findSearchInput().vm.$emit('input', 'search-agent');
expect(findCreateButton().exists()).toBe(false);
});
});
describe('select existing agent configuration', () => {
beforeEach(() => { beforeEach(() => {
findConfiguredAgentItem().vm.$emit('click'); findFirstAgentItem().vm.$emit('click');
}); });
it('emits agentSelected with the name of the clicked agent', () => { it('emits agentSelected with the name of the clicked agent', () => {
...@@ -46,7 +78,22 @@ describe('AvailableAgentsDropdown', () => { ...@@ -46,7 +78,22 @@ describe('AvailableAgentsDropdown', () => {
it('marks the clicked item as selected', () => { it('marks the clicked item as selected', () => {
expect(findDropdown().props('text')).toBe('configured-agent'); expect(findDropdown().props('text')).toBe('configured-agent');
expect(findConfiguredAgentItem().props('isChecked')).toBe(true); expect(findFirstAgentItem().props('isChecked')).toBe(true);
});
});
describe('create new agent configuration', () => {
beforeEach(async () => {
await findSearchInput().vm.$emit('input', 'new-agent');
findCreateButton().vm.$emit('click');
});
it('emits agentSelected with the name of the clicked agent', () => {
expect(wrapper.emitted('agentSelected')).toEqual([['new-agent']]);
});
it('marks the clicked item as selected', () => {
expect(findDropdown().props('text')).toBe('new-agent');
}); });
}); });
}); });
......
...@@ -39,6 +39,7 @@ const kasAddress = 'kas.example.com'; ...@@ -39,6 +39,7 @@ const kasAddress = 'kas.example.com';
const emptyStateImage = 'path/to/image'; const emptyStateImage = 'path/to/image';
const defaultBranchName = 'default'; const defaultBranchName = 'default';
const maxAgents = MAX_LIST_COUNT; const maxAgents = MAX_LIST_COUNT;
const i18n = I18N_AGENT_MODAL;
describe('InstallAgentModal', () => { describe('InstallAgentModal', () => {
let wrapper; let wrapper;
...@@ -67,7 +68,7 @@ describe('InstallAgentModal', () => { ...@@ -67,7 +68,7 @@ describe('InstallAgentModal', () => {
const findActionButton = () => findButtonByVariant('confirm'); const findActionButton = () => findButtonByVariant('confirm');
const findCancelButton = () => findButtonByVariant('default'); const findCancelButton = () => findButtonByVariant('default');
const findPrimaryButton = () => wrapper.findByTestId('agent-primary-button'); const findPrimaryButton = () => wrapper.findByTestId('agent-primary-button');
const findImage = () => wrapper.findByRole('img', { alt: I18N_AGENT_MODAL.empty_state.altText }); const findImage = () => wrapper.findByRole('img', { alt: i18n.altText });
const expectDisabledAttribute = (element, disabled) => { const expectDisabledAttribute = (element, disabled) => {
if (disabled) { if (disabled) {
...@@ -140,12 +141,13 @@ describe('InstallAgentModal', () => { ...@@ -140,12 +141,13 @@ describe('InstallAgentModal', () => {
apolloProvider = null; apolloProvider = null;
}); });
describe('when agent configurations are present', () => { describe('when KAS is enabled', () => {
const i18n = I18N_AGENT_MODAL.agent_registration;
describe('initial state', () => { describe('initial state', () => {
it('renders the dropdown for available agents', () => { it('renders the dropdown for available agents', () => {
expect(findAgentDropdown().isVisible()).toBe(true); expect(findAgentDropdown().isVisible()).toBe(true);
});
it("doesn't render agent installation instructions", () => {
expect(findModal().text()).not.toContain(i18n.basicInstallTitle); expect(findModal().text()).not.toContain(i18n.basicInstallTitle);
expect(findModal().findComponent(GlFormInputGroup).exists()).toBe(false); expect(findModal().findComponent(GlFormInputGroup).exists()).toBe(false);
expect(findModal().findComponent(GlAlert).exists()).toBe(false); expect(findModal().findComponent(GlAlert).exists()).toBe(false);
...@@ -272,44 +274,7 @@ describe('InstallAgentModal', () => { ...@@ -272,44 +274,7 @@ describe('InstallAgentModal', () => {
}); });
}); });
describe('when there are no agent configurations present', () => {
const i18n = I18N_AGENT_MODAL.empty_state;
const apolloQueryEmptyResponse = {
data: {
project: {
clusterAgents: { nodes: [] },
agentConfigurations: { nodes: [] },
},
},
};
beforeEach(() => {
apolloProvider = createMockApollo([
[getAgentConfigurations, jest.fn().mockResolvedValue(apolloQueryEmptyResponse)],
]);
createWrapper();
});
it('renders empty state image', () => {
expect(findImage().attributes('src')).toBe(emptyStateImage);
});
it('renders a primary button', () => {
expect(findPrimaryButton().isVisible()).toBe(true);
expect(findPrimaryButton().text()).toBe(i18n.primaryButton);
});
it('sends the event with the modalType', () => {
findModal().vm.$emit('show');
expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTIONS_OPEN, {
label: EVENT_LABEL_MODAL,
property: MODAL_TYPE_EMPTY,
});
});
});
describe('when KAS is disabled', () => { describe('when KAS is disabled', () => {
const i18n = I18N_AGENT_MODAL.empty_state;
beforeEach(async () => { beforeEach(async () => {
apolloProvider = createMockApollo([ apolloProvider = createMockApollo([
[getAgentConfigurations, jest.fn().mockResolvedValue(kasDisabledErrorResponse)], [getAgentConfigurations, jest.fn().mockResolvedValue(kasDisabledErrorResponse)],
...@@ -331,11 +296,19 @@ describe('InstallAgentModal', () => { ...@@ -331,11 +296,19 @@ describe('InstallAgentModal', () => {
it('renders a cancel button', () => { it('renders a cancel button', () => {
expect(findCancelButton().isVisible()).toBe(true); expect(findCancelButton().isVisible()).toBe(true);
expect(findCancelButton().text()).toBe(i18n.done); expect(findCancelButton().text()).toBe(i18n.close);
}); });
it("doesn't render a secondary button", () => { it("doesn't render a secondary button", () => {
expect(findPrimaryButton().exists()).toBe(false); expect(findPrimaryButton().exists()).toBe(false);
}); });
it('sends the event with the modalType', () => {
findModal().vm.$emit('show');
expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTIONS_OPEN, {
label: EVENT_LABEL_MODAL,
property: MODAL_TYPE_EMPTY,
});
});
}); });
}); });
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