Commit 3fb2793e authored by Jose Ivan Vargas's avatar Jose Ivan Vargas Committed by Olena Horal-Koretska

Add install GitLab runner pop

This contains the frontend code to add
the installation instructions modal for
runners in all categories (admin/group/project)
parent 06a3af64
import initFilteredSearch from '~/pages/search/init_filtered_search';
import AdminRunnersFilteredSearchTokenKeys from '~/filtered_search/admin_runners_filtered_search_token_keys';
import { FILTERED_SEARCH } from '~/pages/constants';
import { initInstallRunner } from '~/pages/shared/mount_runner_instructions';
document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch({
......@@ -8,4 +9,8 @@ document.addEventListener('DOMContentLoaded', () => {
filteredSearchTokenKeys: AdminRunnersFilteredSearchTokenKeys,
useDefaultState: true,
});
if (gon?.features?.runnerInstructions) {
initInstallRunner();
}
});
......@@ -4,6 +4,7 @@ import initFilteredSearch from '~/pages/search/init_filtered_search';
import GroupRunnersFilteredSearchTokenKeys from '~/filtered_search/group_runners_filtered_search_token_keys';
import { FILTERED_SEARCH } from '~/pages/constants';
import initSharedRunnersForm from '~/group_settings/mount_shared_runners';
import { initInstallRunner } from '~/pages/shared/mount_runner_instructions';
document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels
......@@ -18,4 +19,8 @@ document.addEventListener('DOMContentLoaded', () => {
initSharedRunnersForm();
initVariableList();
if (gon?.features?.runnerInstructions) {
initInstallRunner();
}
});
......@@ -6,6 +6,7 @@ import initDeployFreeze from '~/deploy_freeze';
import initSettingsPipelinesTriggers from '~/ci_settings_pipeline_triggers';
import initSharedRunnersToggle from '~/projects/settings/mount_shared_runners_toggle';
import initArtifactsSettings from '~/artifacts_settings';
import { initInstallRunner } from '~/pages/shared/mount_runner_instructions';
document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels
......@@ -39,4 +40,8 @@ document.addEventListener('DOMContentLoaded', () => {
if (gon?.features?.vueifySharedRunnersToggle) {
initSharedRunnersToggle();
}
if (gon?.features?.runnerInstructions) {
initInstallRunner();
}
});
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import InstallRunnerInstructions from '~/vue_shared/components/runner_instructions/runner_instructions.vue';
Vue.use(VueApollo);
export function initInstallRunner(componentId = 'js-install-runner') {
const installRunnerEl = document.getElementById(componentId);
if (installRunnerEl) {
const defaultClient = createDefaultClient();
const { projectPath, groupPath } = installRunnerEl.dataset;
const apolloProvider = new VueApollo({
defaultClient,
});
// eslint-disable-next-line no-new
new Vue({
el: installRunnerEl,
apolloProvider,
provide: {
projectPath,
groupPath,
},
render(createElement) {
return createElement(InstallRunnerInstructions);
},
});
}
}
import { s__ } from '~/locale';
export const PLATFORMS_WITHOUT_ARCHITECTURES = ['docker', 'kubernetes'];
export const INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES = {
docker: {
instructions: s__(
'Runners|To install Runner in a container follow the instructions described in the GitLab documentation',
),
link: 'https://docs.gitlab.com/runner/install/docker.html',
},
kubernetes: {
instructions: s__(
'Runners|To install Runner in Kubernetes follow the instructions described in the GitLab documentation.',
),
link: 'https://docs.gitlab.com/runner/install/kubernetes.html',
},
};
query getRunnerPlatforms($projectPath: ID!, $groupPath: ID!) {
runnerPlatforms {
nodes {
name
humanReadableName
architectures {
nodes {
name
downloadLocation
}
}
}
}
project(fullPath: $projectPath) {
id
}
group(fullPath: $groupPath) {
id
}
}
query runnerSetupInstructions(
$platform: String!
$architecture: String!
$projectId: ID!
$groupId: ID!
) {
runnerSetup(
platform: $platform
architecture: $architecture
projectId: $projectId
groupId: $groupId
) {
installInstructions
registerInstructions
}
}
<script>
import {
GlAlert,
GlButton,
GlModal,
GlModalDirective,
GlButtonGroup,
GlDropdown,
GlDropdownItem,
GlIcon,
} from '@gitlab/ui';
import { __, s__ } from '~/locale';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
import {
PLATFORMS_WITHOUT_ARCHITECTURES,
INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES,
} from './constants';
import getRunnerPlatforms from './graphql/queries/get_runner_platforms.query.graphql';
import getRunnerSetupInstructions from './graphql/queries/get_runner_setup.query.graphql';
export default {
components: {
GlAlert,
GlButton,
GlButtonGroup,
GlDropdown,
GlDropdownItem,
GlModal,
GlIcon,
ModalCopyButton,
},
directives: {
GlModalDirective,
},
inject: {
projectPath: {
default: '',
},
groupPath: {
default: '',
},
},
apollo: {
runnerPlatforms: {
query: getRunnerPlatforms,
variables() {
return {
projectPath: this.projectPath,
groupPath: this.groupPath,
};
},
error() {
this.showAlert = true;
},
result({ data }) {
this.project = data?.project;
this.group = data?.group;
this.selectPlatform(this.platforms[0].name);
},
},
},
data() {
return {
showAlert: false,
selectedPlatformArchitectures: [],
selectedPlatform: {
name: '',
},
selectedArchitecture: {},
runnerPlatforms: {},
instructions: {},
project: {},
group: {},
};
},
computed: {
isPlatformSelected() {
return Object.keys(this.selectedPlatform).length > 0;
},
instructionsEmpty() {
return Object.keys(this.instructions).length === 0;
},
groupId() {
return this.group?.id ?? '';
},
projectId() {
return this.project?.id ?? '';
},
platforms() {
return this.runnerPlatforms?.nodes;
},
hasArchitecureList() {
return !PLATFORMS_WITHOUT_ARCHITECTURES.includes(this.selectedPlatform?.name);
},
instructionsWithoutArchitecture() {
return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatform.name]?.instructions;
},
runnerInstallationLink() {
return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatform.name]?.link;
},
},
methods: {
selectPlatform(name) {
this.selectedPlatform = this.platforms.find((platform) => platform.name === name);
if (this.hasArchitecureList) {
this.selectedPlatformArchitectures = this.selectedPlatform?.architectures?.nodes;
[this.selectedArchitecture] = this.selectedPlatformArchitectures;
this.selectArchitecture(this.selectedArchitecture);
}
},
selectArchitecture(architecture) {
this.selectedArchitecture = architecture;
this.$apollo.addSmartQuery('instructions', {
variables() {
return {
platform: this.selectedPlatform.name,
architecture: this.selectedArchitecture.name,
projectId: this.projectId,
groupId: this.groupId,
};
},
query: getRunnerSetupInstructions,
update(data) {
return data?.runnerSetup;
},
error() {
this.showAlert = true;
},
});
},
toggleAlert(state) {
this.showAlert = state;
},
},
modalId: 'installation-instructions-modal',
i18n: {
installARunner: s__('Runners|Install a Runner'),
architecture: s__('Runners|Architecture'),
downloadInstallBinary: s__('Runners|Download and Install Binary'),
downloadLatestBinary: s__('Runners|Download Latest Binary'),
registerRunner: s__('Runners|Register Runner'),
method: __('Method'),
fetchError: s__('Runners|An error has occurred fetching instructions'),
instructions: s__('Runners|Show Runner installation instructions'),
copyInstructions: s__('Runners|Copy instructions'),
},
closeButton: {
text: __('Close'),
attributes: [{ variant: 'default' }],
},
};
</script>
<template>
<div>
<gl-button
v-gl-modal-directive="$options.modalId"
class="gl-mt-4"
data-testid="show-modal-button"
>
{{ $options.i18n.instructions }}
</gl-button>
<gl-modal
:modal-id="$options.modalId"
:title="$options.i18n.installARunner"
:action-secondary="$options.closeButton"
>
<gl-alert v-if="showAlert" variant="danger" @dismiss="toggleAlert(false)">
{{ $options.i18n.fetchError }}
</gl-alert>
<h5>{{ __('Environment') }}</h5>
<gl-button-group class="gl-mb-5">
<gl-button
v-for="platform in platforms"
:key="platform.name"
data-testid="platform-button"
@click="selectPlatform(platform.name)"
>
{{ platform.humanReadableName }}
</gl-button>
</gl-button-group>
<template v-if="hasArchitecureList">
<template v-if="isPlatformSelected">
<h5>
{{ $options.i18n.architecture }}
</h5>
<gl-dropdown class="gl-mb-5" :text="selectedArchitecture.name">
<gl-dropdown-item
v-for="architecture in selectedPlatformArchitectures"
:key="architecture.name"
data-testid="architecture-dropdown-item"
@click="selectArchitecture(architecture)"
>
{{ architecture.name }}
</gl-dropdown-item>
</gl-dropdown>
<div class="gl-display-flex gl-align-items-center gl-mb-5">
<h5>{{ $options.i18n.downloadInstallBinary }}</h5>
<gl-button
class="gl-ml-auto"
:href="selectedArchitecture.downloadLocation"
download
data-testid="binary-download-button"
>
{{ $options.i18n.downloadLatestBinary }}
</gl-button>
</div>
</template>
<template v-if="!instructionsEmpty">
<div class="gl-display-flex">
<pre
class="gl-bg-gray gl-flex-fill-1 gl-white-space-pre-line"
data-testid="binary-instructions"
>
{{ instructions.installInstructions }}
</pre
>
<modal-copy-button
:title="$options.i18n.copyInstructions"
:text="instructions.installInstructions"
:modal-id="$options.modalId"
css-classes="gl-align-self-start gl-ml-2 gl-mt-2"
category="tertiary"
/>
</div>
<hr />
<h5 class="gl-mb-5">{{ $options.i18n.registerRunner }}</h5>
<h5 class="gl-mb-5">{{ $options.i18n.method }}</h5>
<div class="gl-display-flex">
<pre
class="gl-bg-gray gl-flex-fill-1 gl-white-space-pre-line"
data-testid="runner-instructions"
>
{{ instructions.registerInstructions }}
</pre
>
<modal-copy-button
:title="$options.i18n.copyInstructions"
:text="instructions.registerInstructions"
:modal-id="$options.modalId"
css-classes="gl-align-self-start gl-ml-2 gl-mt-2"
category="tertiary"
/>
</div>
</template>
</template>
<template v-else>
<div>
<p>{{ instructionsWithoutArchitecture }}</p>
<gl-button :href="runnerInstallationLink">
<gl-icon name="external-link" />
{{ s__('Runners|View installation instructions') }}
</gl-button>
</div>
</template>
</gl-modal>
</div>
</template>
......@@ -4,6 +4,9 @@ class Admin::RunnersController < Admin::ApplicationController
include RunnerSetupScripts
before_action :runner, except: [:index, :tag_list, :runner_setup_scripts]
before_action do
push_frontend_feature_flag(:runner_instructions, default_enabled: :yaml)
end
feature_category :continuous_integration
......
......@@ -9,6 +9,9 @@ module Groups
before_action :authorize_admin_group!
before_action :authorize_update_max_artifacts_size!, only: [:update]
before_action :define_variables, only: [:show]
before_action do
push_frontend_feature_flag(:runner_instructions, @group, default_enabled: :yaml)
end
feature_category :continuous_integration
......
......@@ -12,6 +12,7 @@ module Projects
before_action do
push_frontend_feature_flag(:ajax_new_deploy_token, @project)
push_frontend_feature_flag(:vueify_shared_runners_toggle, @project)
push_frontend_feature_flag(:runner_instructions, @project, default_enabled: :yaml)
end
helper_method :highlight_badge
......
......@@ -39,7 +39,9 @@
= render partial: 'ci/runner/how_to_setup_runner',
locals: { registration_token: Gitlab::CurrentSettings.runners_registration_token,
type: 'shared',
reset_token_url: reset_registration_token_admin_application_settings_path }
reset_token_url: reset_registration_token_admin_application_settings_path,
project_path: '',
group_path: '' }
.row
.col-sm-9
......
......@@ -21,3 +21,5 @@
= button_to _("Reset registration token"), reset_token_url,
method: :put, class: 'gl-button btn btn-default',
data: { confirm: _("Are you sure you want to reset the registration token?") }
#js-install-runner{ data: { project_path: project_path, group_path: group_path } }
......@@ -17,5 +17,7 @@
= render partial: 'ci/runner/how_to_setup_runner',
locals: { registration_token: @group.runners_token,
type: 'group',
reset_token_url: reset_registration_token_group_settings_ci_cd_path }
reset_token_url: reset_registration_token_group_settings_ci_cd_path,
project_path: '',
group_path: @group.path }
%br
......@@ -9,9 +9,11 @@
clusters_path: project_clusters_path(@project) }
%hr
= render partial: 'ci/runner/how_to_setup_runner',
locals: { registration_token: @project.runners_token,
type: 'specific',
reset_token_url: reset_registration_token_namespace_project_settings_ci_cd_path }
locals: { registration_token: @project.runners_token,
type: 'specific',
reset_token_url: reset_registration_token_namespace_project_settings_ci_cd_path,
project_path: @project.path_with_namespace,
group_path: '' }
%hr
......
---
name: runner_instructions
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/51014
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/296818
milestone: '13.9'
type: development
group: group::"continuous integration"
default_enabled: false
......@@ -24606,21 +24606,36 @@ msgstr ""
msgid "Runners|Active"
msgstr ""
msgid "Runners|An error has occurred fetching instructions"
msgstr ""
msgid "Runners|Architecture"
msgstr ""
msgid "Runners|Can run untagged jobs"
msgstr ""
msgid "Runners|Copy instructions"
msgstr ""
msgid "Runners|Description"
msgstr ""
msgid "Runners|Download Latest Binary"
msgstr ""
msgid "Runners|Download and Install Binary"
msgstr ""
msgid "Runners|Group"
msgstr ""
msgid "Runners|IP Address"
msgstr ""
msgid "Runners|Install a Runner"
msgstr ""
msgid "Runners|Last contact"
msgstr ""
......@@ -24642,24 +24657,39 @@ msgstr ""
msgid "Runners|Protected"
msgstr ""
msgid "Runners|Register Runner"
msgstr ""
msgid "Runners|Revision"
msgstr ""
msgid "Runners|Shared"
msgstr ""
msgid "Runners|Show Runner installation instructions"
msgstr ""
msgid "Runners|Specific"
msgstr ""
msgid "Runners|Tags"
msgstr ""
msgid "Runners|To install Runner in Kubernetes follow the instructions described in the GitLab documentation."
msgstr ""
msgid "Runners|To install Runner in a container follow the instructions described in the GitLab documentation"
msgstr ""
msgid "Runners|Value"
msgstr ""
msgid "Runners|Version"
msgstr ""
msgid "Runners|View installation instructions"
msgstr ""
msgid "Runners|You have used %{quotaUsed} out of %{quotaLimit} of your shared Runners pipeline minutes."
msgstr ""
......
export const mockGraphqlRunnerPlatforms = {
data: {
runnerPlatforms: {
nodes: [
{
name: 'linux',
humanReadableName: 'Linux',
architectures: {
nodes: [
{
name: 'amd64',
downloadLocation:
'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64',
__typename: 'RunnerArchitecture',
},
{
name: '386',
downloadLocation:
'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-386',
__typename: 'RunnerArchitecture',
},
{
name: 'arm',
downloadLocation:
'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-arm',
__typename: 'RunnerArchitecture',
},
{
name: 'arm64',
downloadLocation:
'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-arm64',
__typename: 'RunnerArchitecture',
},
],
__typename: 'RunnerArchitectureConnection',
},
__typename: 'RunnerPlatform',
},
{
name: 'osx',
humanReadableName: 'macOS',
architectures: {
nodes: [
{
name: 'amd64',
downloadLocation:
'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-darwin-amd64',
__typename: 'RunnerArchitecture',
},
],
__typename: 'RunnerArchitectureConnection',
},
__typename: 'RunnerPlatform',
},
{
name: 'windows',
humanReadableName: 'Windows',
architectures: {
nodes: [
{
name: 'amd64',
downloadLocation:
'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-windows-amd64.exe',
__typename: 'RunnerArchitecture',
},
{
name: '386',
downloadLocation:
'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-windows-386.exe',
__typename: 'RunnerArchitecture',
},
],
__typename: 'RunnerArchitectureConnection',
},
__typename: 'RunnerPlatform',
},
{
name: 'docker',
humanReadableName: 'Docker',
architectures: null,
__typename: 'RunnerPlatform',
},
{
name: 'kubernetes',
humanReadableName: 'Kubernetes',
architectures: null,
__typename: 'RunnerPlatform',
},
],
__typename: 'RunnerPlatformConnection',
},
project: { id: 'gid://gitlab/Project/1', __typename: 'Project' },
group: null,
},
};
export const mockGraphqlInstructions = {
data: {
runnerSetup: {
installInstructions:
"# Download the binary for your system\nsudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64\n\n# Give it permissions to execute\nsudo chmod +x /usr/local/bin/gitlab-runner\n\n# Create a GitLab CI user\nsudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash\n\n# Install and run as service\nsudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner\nsudo gitlab-runner start\n",
registerInstructions:
'sudo gitlab-runner register --url http://192.168.1.81:3000/ --registration-token GE5gsjeep_HAtBf9s3Yz',
__typename: 'RunnerSetup',
},
},
};
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import RunnerInstructions from '~/vue_shared/components/runner_instructions/runner_instructions.vue';
import getRunnerPlatforms from '~/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql';
import getRunnerSetupInstructions from '~/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql';
import { mockGraphqlRunnerPlatforms, mockGraphqlInstructions } from './mock_data';
const projectPath = 'gitlab-org/gitlab';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('RunnerInstructions component', () => {
let wrapper;
let fakeApollo;
const findModalButton = () => wrapper.find('[data-testid="show-modal-button"]');
const findPlatformButtons = () => wrapper.findAll('[data-testid="platform-button"]');
const findArchitectureDropdownItems = () =>
wrapper.findAll('[data-testid="architecture-dropdown-item"]');
const findBinaryInstructionsSection = () => wrapper.find('[data-testid="binary-instructions"]');
const findRunnerInstructionsSection = () => wrapper.find('[data-testid="runner-instructions"]');
beforeEach(async () => {
const requestHandlers = [
[getRunnerPlatforms, jest.fn().mockResolvedValue(mockGraphqlRunnerPlatforms)],
[getRunnerSetupInstructions, jest.fn().mockResolvedValue(mockGraphqlInstructions)],
];
fakeApollo = createMockApollo(requestHandlers);
wrapper = shallowMount(RunnerInstructions, {
provide: {
projectPath,
},
localVue,
apolloProvider: fakeApollo,
});
await wrapper.vm.$nextTick();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('should show the "Show Runner installation instructions" button', () => {
const button = findModalButton();
expect(button.exists()).toBe(true);
expect(button.text()).toBe('Show Runner installation instructions');
});
it('should contain a number of platforms buttons', () => {
const buttons = findPlatformButtons();
expect(buttons).toHaveLength(mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes.length);
});
it('should contain a number of dropdown items for the architecture options', () => {
const platformButton = findPlatformButtons().at(0);
platformButton.vm.$emit('click');
return wrapper.vm.$nextTick(() => {
const dropdownItems = findArchitectureDropdownItems();
expect(dropdownItems).toHaveLength(
mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes[0].architectures.nodes.length,
);
});
});
it('should display the binary installation instructions for a selected architecture', async () => {
const platformButton = findPlatformButtons().at(0);
platformButton.vm.$emit('click');
await wrapper.vm.$nextTick();
const dropdownItem = findArchitectureDropdownItems().at(0);
dropdownItem.vm.$emit('click');
await wrapper.vm.$nextTick();
const runner = findBinaryInstructionsSection();
expect(runner.text()).toMatch('sudo chmod +x /usr/local/bin/gitlab-runner');
expect(runner.text()).toMatch(
`sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash`,
);
expect(runner.text()).toMatch(
'sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner',
);
expect(runner.text()).toMatch('sudo gitlab-runner start');
});
it('should display the runner register instructions for a selected architecture', async () => {
const platformButton = findPlatformButtons().at(0);
platformButton.vm.$emit('click');
await wrapper.vm.$nextTick();
const dropdownItem = findArchitectureDropdownItems().at(0);
dropdownItem.vm.$emit('click');
await wrapper.vm.$nextTick();
const runner = findRunnerInstructionsSection();
expect(runner.text()).toMatch(mockGraphqlInstructions.data.runnerSetup.registerInstructions);
});
});
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