From 7a27bec0ed76a2a6e9b51f0419e8ac663c6136d2 Mon Sep 17 00:00:00 2001
From: Miguel Rincon <mrincon@gitlab.com>
Date: Wed, 26 Apr 2023 16:46:50 +0000
Subject: [PATCH] Support old registration token reset in projects

This change adds the registration reset in a dropdown to the
project CI/CD settings, to allow user to reset the runner registration
token for their project.
---
 .../registration/registration_dropdown.vue    | 30 +++++++++-----
 app/assets/javascripts/ci/runner/constants.js |  6 +++
 .../runner/project_runners/register/index.js  | 39 +++++++++++++++++++
 .../projects/settings/ci_cd/show/index.js     |  2 +
 .../projects/settings/ci_cd_controller.rb     |  1 +
 .../runners/_project_runners.html.haml        |  1 +
 spec/features/admin/admin_runners_spec.rb     | 27 ++++++++++---
 spec/features/runners_spec.rb                 | 17 ++++++--
 .../registration_dropdown_spec.js             | 37 +++++++++++++-----
 9 files changed, 131 insertions(+), 29 deletions(-)
 create mode 100644 app/assets/javascripts/ci/runner/project_runners/register/index.js

diff --git a/app/assets/javascripts/ci/runner/components/registration/registration_dropdown.vue b/app/assets/javascripts/ci/runner/components/registration/registration_dropdown.vue
index c1e862f6fa88..2fdf8456615a 100644
--- a/app/assets/javascripts/ci/runner/components/registration/registration_dropdown.vue
+++ b/app/assets/javascripts/ci/runner/components/registration/registration_dropdown.vue
@@ -3,7 +3,15 @@ import { GlDropdown, GlDropdownForm, GlDropdownItem, GlDropdownDivider, GlIcon }
 import { s__ } from '~/locale';
 import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
 import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
-import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../../constants';
+import {
+  INSTANCE_TYPE,
+  GROUP_TYPE,
+  PROJECT_TYPE,
+  I18N_REGISTER_INSTANCE_TYPE,
+  I18N_REGISTER_GROUP_TYPE,
+  I18N_REGISTER_PROJECT_TYPE,
+  I18N_REGISTER_RUNNER,
+} from '../../constants';
 import RegistrationToken from './registration_token.vue';
 import RegistrationTokenResetDropdownItem from './registration_token_reset_dropdown_item.vue';
 
@@ -51,20 +59,23 @@ export default {
         this.glFeatures?.createRunnerWorkflowForNamespace
       );
     },
-    dropdownText() {
-      if (this.isDeprecated) {
-        return '';
-      }
+    actionText() {
       switch (this.type) {
         case INSTANCE_TYPE:
-          return s__('Runners|Register an instance runner');
+          return I18N_REGISTER_INSTANCE_TYPE;
         case GROUP_TYPE:
-          return s__('Runners|Register a group runner');
+          return I18N_REGISTER_GROUP_TYPE;
         case PROJECT_TYPE:
-          return s__('Runners|Register a project runner');
+          return I18N_REGISTER_PROJECT_TYPE;
         default:
-          return s__('Runners|Register a runner');
+          return I18N_REGISTER_RUNNER;
+      }
+    },
+    dropdownText() {
+      if (this.isDeprecated) {
+        return '';
       }
+      return this.actionText;
     },
     dropdownToggleClass() {
       if (this.isDeprecated) {
@@ -109,6 +120,7 @@ export default {
     v-bind="$attrs"
   >
     <template v-if="isDeprecated" #button-content>
+      <span class="gl-sr-only">{{ actionText }}</span>
       <gl-icon name="ellipsis_v" />
     </template>
     <gl-dropdown-form class="gl-p-4!">
diff --git a/app/assets/javascripts/ci/runner/constants.js b/app/assets/javascripts/ci/runner/constants.js
index 84b2ed010e61..1225c0d75836 100644
--- a/app/assets/javascripts/ci/runner/constants.js
+++ b/app/assets/javascripts/ci/runner/constants.js
@@ -71,6 +71,12 @@ export const I18N_STALE_NEVER_CONTACTED_TOOLTIP = s__(
   'Runners|Runner is stale; it has never contacted this instance',
 );
 
+// Registration dropdown
+export const I18N_REGISTER_INSTANCE_TYPE = s__('Runners|Register an instance runner');
+export const I18N_REGISTER_GROUP_TYPE = s__('Runners|Register a group runner');
+export const I18N_REGISTER_PROJECT_TYPE = s__('Runners|Register a project runner');
+export const I18N_REGISTER_RUNNER = s__('Runners|Register a runner');
+
 // Actions
 export const I18N_EDIT = __('Edit');
 
diff --git a/app/assets/javascripts/ci/runner/project_runners/register/index.js b/app/assets/javascripts/ci/runner/project_runners/register/index.js
new file mode 100644
index 000000000000..9986c93c9181
--- /dev/null
+++ b/app/assets/javascripts/ci/runner/project_runners/register/index.js
@@ -0,0 +1,39 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import RegistrationDropdown from '~/ci/runner/components/registration/registration_dropdown.vue';
+import { PROJECT_TYPE } from '~/ci/runner/constants';
+
+Vue.use(VueApollo);
+
+export const initProjectRunnersRegistrationDropdown = (
+  selector = '#js-project-runner-registration-dropdown',
+) => {
+  const el = document.querySelector(selector);
+
+  if (!el) {
+    return null;
+  }
+
+  const { registrationToken, projectId } = el.dataset;
+
+  const apolloProvider = new VueApollo({
+    defaultClient: createDefaultClient(),
+  });
+
+  return new Vue({
+    el,
+    apolloProvider,
+    provide: {
+      projectId,
+    },
+    render(h) {
+      return h(RegistrationDropdown, {
+        props: {
+          registrationToken,
+          type: PROJECT_TYPE,
+        },
+      });
+    },
+  });
+};
diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
index 9ec560154054..731b1373987b 100644
--- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
@@ -12,6 +12,7 @@ import { initTokenAccess } from '~/token_access';
 import { initCiSecureFiles } from '~/ci_secure_files';
 import initDeployTokens from '~/deploy_tokens';
 import { initProjectRunners } from '~/ci/runner/project_runners';
+import { initProjectRunnersRegistrationDropdown } from '~/ci/runner/project_runners/register';
 
 // Initialize expandable settings panels
 initSettingsPanels();
@@ -42,6 +43,7 @@ initSettingsPipelinesTriggers();
 initArtifactsSettings();
 
 initProjectRunners();
+initProjectRunnersRegistrationDropdown();
 initSharedRunnersToggle();
 initRefSwitcherBadges();
 initInstallRunner();
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index 28ae730eda58..626587deb710 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -15,6 +15,7 @@ class CiCdController < Projects::ApplicationController
       before_action do
         push_frontend_feature_flag(:ci_variables_pages, current_user)
         push_frontend_feature_flag(:ci_limit_environment_scope, @project)
+        push_frontend_feature_flag(:create_runner_workflow_for_namespace, @project.namespace)
       end
 
       helper_method :highlight_badge
diff --git a/app/views/projects/runners/_project_runners.html.haml b/app/views/projects/runners/_project_runners.html.haml
index 9b401711ec5a..1d4e45c71b5b 100644
--- a/app/views/projects/runners/_project_runners.html.haml
+++ b/app/views/projects/runners/_project_runners.html.haml
@@ -7,6 +7,7 @@
     - if can?(current_user, :create_runner, @project)
       = render Pajamas::ButtonComponent.new(href: new_project_runner_path(@project), variant: :confirm) do
         = s_('Runners|New project runner')
+      #js-project-runner-registration-dropdown{ data: { registration_token: @project.runners_token, project_id: @project.id } }
     - else
       = _('Please contact an admin to create runners.')
       = link_to _('Learn more.'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'restrict-runner-registration-by-all-users-in-an-instance'), target: '_blank', rel: 'noopener noreferrer'
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index d82f9acdd074..582535790bdd 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -32,15 +32,30 @@
     end
 
     describe "runners registration" do
-      before do
-        stub_feature_flags(create_runner_workflow_for_admin: false)
+      context 'when create_runner_workflow_for_namespace is enabled' do
+        before do
+          stub_feature_flags(create_runner_workflow_for_admin: true)
 
-        visit admin_runners_path
+          visit admin_runners_path
+        end
+
+        it_behaves_like "shows and resets runner registration token" do
+          let(:dropdown_text) { s_('Runners|Register an instance runner') }
+          let(:registration_token) { Gitlab::CurrentSettings.runners_registration_token }
+        end
       end
 
-      it_behaves_like "shows and resets runner registration token" do
-        let(:dropdown_text) { s_('Runners|Register an instance runner') }
-        let(:registration_token) { Gitlab::CurrentSettings.runners_registration_token }
+      context 'when create_runner_workflow_for_namespace is disabled' do
+        before do
+          stub_feature_flags(create_runner_workflow_for_admin: false)
+
+          visit admin_runners_path
+        end
+
+        it_behaves_like "shows and resets runner registration token" do
+          let(:dropdown_text) { s_('Runners|Register an instance runner') }
+          let(:registration_token) { Gitlab::CurrentSettings.runners_registration_token }
+        end
       end
     end
 
diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb
index 54482d141c71..2de95c21003c 100644
--- a/spec/features/runners_spec.rb
+++ b/spec/features/runners_spec.rb
@@ -21,19 +21,28 @@
         project.add_maintainer(user)
       end
 
-      context 'when create_runner_workflow_for_namespace is enabled' do
+      context 'when create_runner_workflow_for_namespace is enabled', :js do
         before do
           stub_feature_flags(create_runner_workflow_for_namespace: [project.namespace])
-        end
 
-        it 'user can see a link with instructions on how to install GitLab Runner' do
           visit project_runners_path(project)
+        end
 
+        it 'user can see a link with instructions on how to install GitLab Runner' do
           expect(page).to have_link(s_('Runners|New project runner'), href: new_project_runner_path(project))
         end
 
-        describe 'runner registration', :js do
+        it_behaves_like "shows and resets runner registration token" do
+          let(:dropdown_text) { s_('Runners|Register a project runner') }
+          let(:registration_token) { project.runners_token }
+        end
+      end
+
+      context 'when user views new runner page' do
+        context 'when create_runner_workflow_for_namespace is enabled', :js do
           before do
+            stub_feature_flags(create_runner_workflow_for_namespace: [project.namespace])
+
             visit new_project_runner_path(project)
           end
 
diff --git a/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
index 9df7a974af38..e564cf49ca0d 100644
--- a/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
@@ -12,7 +12,14 @@ import RegistrationDropdown from '~/ci/runner/components/registration/registrati
 import RegistrationToken from '~/ci/runner/components/registration/registration_token.vue';
 import RegistrationTokenResetDropdownItem from '~/ci/runner/components/registration/registration_token_reset_dropdown_item.vue';
 
-import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/ci/runner/constants';
+import {
+  INSTANCE_TYPE,
+  GROUP_TYPE,
+  PROJECT_TYPE,
+  I18N_REGISTER_INSTANCE_TYPE,
+  I18N_REGISTER_GROUP_TYPE,
+  I18N_REGISTER_PROJECT_TYPE,
+} from '~/ci/runner/constants';
 
 import getRunnerPlatformsQuery from '~/vue_shared/components/runner_instructions/graphql/get_runner_platforms.query.graphql';
 import getRunnerSetupInstructionsQuery from '~/vue_shared/components/runner_instructions/graphql/get_runner_setup.query.graphql';
@@ -81,13 +88,13 @@ describe('RegistrationDropdown', () => {
 
   it.each`
     type             | text
-    ${INSTANCE_TYPE} | ${s__('Runners|Register an instance runner')}
-    ${GROUP_TYPE}    | ${s__('Runners|Register a group runner')}
-    ${PROJECT_TYPE}  | ${s__('Runners|Register a project runner')}
-  `('Dropdown text for type $type is "$text"', () => {
-    createComponent({ props: { type: INSTANCE_TYPE } }, mountExtended);
+    ${INSTANCE_TYPE} | ${I18N_REGISTER_INSTANCE_TYPE}
+    ${GROUP_TYPE}    | ${I18N_REGISTER_GROUP_TYPE}
+    ${PROJECT_TYPE}  | ${I18N_REGISTER_PROJECT_TYPE}
+  `('Dropdown text for type $type is "$text"', ({ type, text }) => {
+    createComponent({ props: { type } }, mountExtended);
 
-    expect(wrapper.text()).toContain('Register an instance runner');
+    expect(wrapper.text()).toContain(text);
   });
 
   it('Passes attributes to dropdown', () => {
@@ -214,7 +221,7 @@ describe('RegistrationDropdown', () => {
     { createRunnerWorkflowForAdmin: true },
     { createRunnerWorkflowForNamespace: true },
   ])('When showing a "deprecated" warning', (glFeatures) => {
-    it('Passes deprecated variant props and attributes to dropdown', () => {
+    it('passes deprecated variant props and attributes to dropdown', () => {
       createComponent({
         provide: { glFeatures },
       });
@@ -230,6 +237,17 @@ describe('RegistrationDropdown', () => {
       });
     });
 
+    it.each`
+      type             | text
+      ${INSTANCE_TYPE} | ${I18N_REGISTER_INSTANCE_TYPE}
+      ${GROUP_TYPE}    | ${I18N_REGISTER_GROUP_TYPE}
+      ${PROJECT_TYPE}  | ${I18N_REGISTER_PROJECT_TYPE}
+    `('dropdown text for type $type is "$text"', ({ type, text }) => {
+      createComponent({ props: { type } }, mountExtended);
+
+      expect(wrapper.text()).toContain(text);
+    });
+
     it('shows warning text', () => {
       createComponent(
         {
@@ -243,7 +261,7 @@ describe('RegistrationDropdown', () => {
       expect(text.exists()).toBe(true);
     });
 
-    it('button shows only ellipsis icon', () => {
+    it('button shows ellipsis icon', () => {
       createComponent(
         {
           provide: { glFeatures },
@@ -251,7 +269,6 @@ describe('RegistrationDropdown', () => {
         mountExtended,
       );
 
-      expect(findDropdownBtn().text()).toBe('');
       expect(findDropdownBtn().findComponent(GlIcon).props('name')).toBe('ellipsis_v');
       expect(findDropdownBtn().findAllComponents(GlIcon)).toHaveLength(1);
     });
-- 
GitLab