Commit dffb7edd authored by Tiger Watson's avatar Tiger Watson Committed by Heinrich Lee Yu

Expose :certificate_based_clusters feature flag to frontend

Hide Kubernetes sidebar entry for admin and groups
Show 404  on cluster pages
Add feature flag to data helper
Load feature flag status from attributes
Split constants for future re-use
Only show agent tab if cluster are disabled
Hide dropdown and show button if cluster disabled
Co-authored-by: default avatarTiger Watson <twatson@gitlab.com>
parent 69710c13
<script> <script>
import { import {
GlButton,
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
GlModalDirective, GlModalDirective,
...@@ -14,6 +15,7 @@ export default { ...@@ -14,6 +15,7 @@ export default {
i18n: CLUSTERS_ACTIONS, i18n: CLUSTERS_ACTIONS,
INSTALL_AGENT_MODAL_ID, INSTALL_AGENT_MODAL_ID,
components: { components: {
GlButton,
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
GlDropdownDivider, GlDropdownDivider,
...@@ -23,7 +25,13 @@ export default { ...@@ -23,7 +25,13 @@ export default {
GlModalDirective, GlModalDirective,
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
inject: ['newClusterPath', 'addClusterPath', 'canAddCluster', 'displayClusterAgents'], inject: [
'newClusterPath',
'addClusterPath',
'canAddCluster',
'displayClusterAgents',
'certificateBasedClustersEnabled',
],
computed: { computed: {
tooltip() { tooltip() {
const { connectWithAgent, connectExistingCluster, dropdownDisabledHint } = this.$options.i18n; const { connectWithAgent, connectExistingCluster, dropdownDisabledHint } = this.$options.i18n;
...@@ -46,6 +54,7 @@ export default { ...@@ -46,6 +54,7 @@ export default {
<template> <template>
<div class="nav-controls gl-ml-auto"> <div class="nav-controls gl-ml-auto">
<gl-dropdown <gl-dropdown
v-if="certificateBasedClustersEnabled"
ref="dropdown" ref="dropdown"
v-gl-modal-directive="shouldTriggerModal && $options.INSTALL_AGENT_MODAL_ID" v-gl-modal-directive="shouldTriggerModal && $options.INSTALL_AGENT_MODAL_ID"
v-gl-tooltip="tooltip" v-gl-tooltip="tooltip"
...@@ -75,5 +84,15 @@ export default { ...@@ -75,5 +84,15 @@ export default {
{{ $options.i18n.connectExistingCluster }} {{ $options.i18n.connectExistingCluster }}
</gl-dropdown-item> </gl-dropdown-item>
</gl-dropdown> </gl-dropdown>
<gl-button
v-else
v-gl-modal-directive="$options.INSTALL_AGENT_MODAL_ID"
v-gl-tooltip="tooltip"
:disabled="!canAddCluster"
category="primary"
variant="confirm"
>
{{ $options.i18n.connectWithAgent }}
</gl-button>
</div> </div>
</template> </template>
...@@ -9,6 +9,7 @@ import { ...@@ -9,6 +9,7 @@ import {
AGENT, AGENT,
EVENT_LABEL_TABS, EVENT_LABEL_TABS,
EVENT_ACTIONS_CHANGE, EVENT_ACTIONS_CHANGE,
AGENT_TAB,
} from '../constants'; } from '../constants';
import Agents from './agents.vue'; import Agents from './agents.vue';
import InstallAgentModal from './install_agent_modal.vue'; import InstallAgentModal from './install_agent_modal.vue';
...@@ -28,9 +29,8 @@ export default { ...@@ -28,9 +29,8 @@ export default {
Agents, Agents,
InstallAgentModal, InstallAgentModal,
}, },
CLUSTERS_TABS,
mixins: [trackingMixin], mixins: [trackingMixin],
inject: ['displayClusterAgents'], inject: ['displayClusterAgents', 'certificateBasedClustersEnabled'],
props: { props: {
defaultBranchName: { defaultBranchName: {
default: '.noBranch', default: '.noBranch',
...@@ -45,21 +45,27 @@ export default { ...@@ -45,21 +45,27 @@ export default {
}; };
}, },
computed: { computed: {
clusterTabs() { availableTabs() {
return this.displayClusterAgents ? CLUSTERS_TABS : [CERTIFICATE_TAB]; const clusterTabs = this.displayClusterAgents ? CLUSTERS_TABS : [CERTIFICATE_TAB];
return this.certificateBasedClustersEnabled ? clusterTabs : [AGENT_TAB];
}, },
}, },
watch: { watch: {
selectedTabIndex(val) { selectedTabIndex: {
handler(val) {
this.onTabChange(val); this.onTabChange(val);
}, },
immediate: true,
},
}, },
methods: { methods: {
setSelectedTab(tabName) { setSelectedTab(tabName) {
this.selectedTabIndex = this.clusterTabs.findIndex((tab) => tab.queryParamValue === tabName); this.selectedTabIndex = this.availableTabs.findIndex(
(tab) => tab.queryParamValue === tabName,
);
}, },
onTabChange(tab) { onTabChange(tab) {
const tabName = this.clusterTabs[tab].queryParamValue; const tabName = this.availableTabs[tab].queryParamValue;
this.maxAgents = tabName === AGENT ? MAX_LIST_COUNT : MAX_CLUSTERS_LIST; this.maxAgents = tabName === AGENT ? MAX_LIST_COUNT : MAX_CLUSTERS_LIST;
this.track(EVENT_ACTIONS_CHANGE, { property: tabName }); this.track(EVENT_ACTIONS_CHANGE, { property: tabName });
...@@ -76,7 +82,7 @@ export default { ...@@ -76,7 +82,7 @@ export default {
lazy lazy
> >
<gl-tab <gl-tab
v-for="(tab, idx) in clusterTabs" v-for="(tab, idx) in availableTabs"
:key="idx" :key="idx"
:title="tab.title" :title="tab.title"
:query-param-value="tab.queryParamValue" :query-param-value="tab.queryParamValue"
......
...@@ -232,25 +232,24 @@ export const CERTIFICATE_BASED_CARD_INFO = { ...@@ -232,25 +232,24 @@ export const CERTIFICATE_BASED_CARD_INFO = {
export const MAX_CLUSTERS_LIST = 6; export const MAX_CLUSTERS_LIST = 6;
export const CERTIFICATE_TAB = { export const ALL_TAB = {
title: s__('ClusterAgents|Certificate'),
component: 'clusters',
queryParamValue: 'certificate_based',
};
export const CLUSTERS_TABS = [
{
title: s__('ClusterAgents|All'), title: s__('ClusterAgents|All'),
component: 'ClustersViewAll', component: 'ClustersViewAll',
queryParamValue: 'all', queryParamValue: 'all',
}, };
{
export const AGENT_TAB = {
title: s__('ClusterAgents|Agent'), title: s__('ClusterAgents|Agent'),
component: 'agents', component: 'agents',
queryParamValue: 'agent', queryParamValue: 'agent',
}, };
CERTIFICATE_TAB, export const CERTIFICATE_TAB = {
]; title: s__('ClusterAgents|Certificate'),
component: 'clusters',
queryParamValue: 'certificate_based',
};
export const CLUSTERS_TABS = [ALL_TAB, AGENT_TAB, CERTIFICATE_TAB];
export const CLUSTERS_ACTIONS = { export const CLUSTERS_ACTIONS = {
actionsButton: s__('ClusterAgents|Actions'), actionsButton: s__('ClusterAgents|Actions'),
......
...@@ -31,6 +31,7 @@ export default () => { ...@@ -31,6 +31,7 @@ export default () => {
canAdminCluster, canAdminCluster,
gitlabVersion, gitlabVersion,
displayClusterAgents, displayClusterAgents,
certificateBasedClustersEnabled,
} = el.dataset; } = el.dataset;
return new Vue({ return new Vue({
...@@ -48,6 +49,7 @@ export default () => { ...@@ -48,6 +49,7 @@ export default () => {
canAdminCluster: parseBoolean(canAdminCluster), canAdminCluster: parseBoolean(canAdminCluster),
gitlabVersion, gitlabVersion,
displayClusterAgents: parseBoolean(displayClusterAgents), displayClusterAgents: parseBoolean(displayClusterAgents),
certificateBasedClustersEnabled: parseBoolean(certificateBasedClustersEnabled),
}, },
store: createStore(el.dataset), store: createStore(el.dataset),
render(createElement) { render(createElement) {
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
class Admin::ClustersController < Clusters::ClustersController class Admin::ClustersController < Clusters::ClustersController
include EnforcesAdminAuthentication include EnforcesAdminAuthentication
before_action :ensure_feature_enabled!
layout 'admin' layout 'admin'
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
class Groups::ClustersController < Clusters::ClustersController class Groups::ClustersController < Clusters::ClustersController
include ControllerWithCrossProjectAccessCheck include ControllerWithCrossProjectAccessCheck
before_action :ensure_feature_enabled!
prepend_before_action :group prepend_before_action :group
requires_cross_project_access requires_cross_project_access
......
...@@ -464,7 +464,10 @@ module ApplicationSettingsHelper ...@@ -464,7 +464,10 @@ module ApplicationSettingsHelper
end end
def instance_clusters_enabled? def instance_clusters_enabled?
can?(current_user, :read_cluster, Clusters::Instance.new) clusterable = Clusters::Instance.new
Feature.enabled?(:certificate_based_clusters, clusterable, default_enabled: :yaml, type: :ops) &&
can?(current_user, :read_cluster, clusterable)
end end
def omnibus_protected_paths_throttle? def omnibus_protected_paths_throttle?
......
...@@ -31,7 +31,8 @@ module ClustersHelper ...@@ -31,7 +31,8 @@ module ClustersHelper
add_cluster_path: clusterable.new_path(tab: 'add'), add_cluster_path: clusterable.new_path(tab: 'add'),
can_add_cluster: clusterable.can_add_cluster?.to_s, can_add_cluster: clusterable.can_add_cluster?.to_s,
can_admin_cluster: clusterable.can_admin_cluster?.to_s, can_admin_cluster: clusterable.can_admin_cluster?.to_s,
display_cluster_agents: display_cluster_agents?(clusterable).to_s display_cluster_agents: display_cluster_agents?(clusterable).to_s,
certificate_based_clusters_enabled: Feature.enabled?(:certificate_based_clusters, clusterable, default_enabled: :yaml, type: :ops).to_s
} }
end end
......
...@@ -21,7 +21,10 @@ module Sidebars ...@@ -21,7 +21,10 @@ module Sidebars
override :render? override :render?
def render? def render?
can?(context.current_user, :read_cluster, context.group) clusterable = context.group
Feature.enabled?(:certificate_based_clusters, clusterable, default_enabled: :yaml, type: :ops) &&
can?(context.current_user, :read_cluster, clusterable)
end end
override :extra_container_html_options override :extra_container_html_options
......
...@@ -27,7 +27,7 @@ RSpec.describe Admin::ClustersController do ...@@ -27,7 +27,7 @@ RSpec.describe Admin::ClustersController do
create(:cluster, :disabled, :provided_by_gcp, :production_environment, :instance) create(:cluster, :disabled, :provided_by_gcp, :production_environment, :instance)
end end
include_examples ':certificate_based_clusters feature flag index responses' do include_examples ':certificate_based_clusters feature flag controller responses' do
let(:subject) { get_index } let(:subject) { get_index }
end end
......
...@@ -32,7 +32,7 @@ RSpec.describe Groups::ClustersController do ...@@ -32,7 +32,7 @@ RSpec.describe Groups::ClustersController do
create(:cluster, :disabled, :provided_by_gcp, :production_environment, cluster_type: :group_type, groups: [group]) create(:cluster, :disabled, :provided_by_gcp, :production_environment, cluster_type: :group_type, groups: [group])
end end
include_examples ':certificate_based_clusters feature flag index responses' do include_examples ':certificate_based_clusters feature flag controller responses' do
let(:subject) { go } let(:subject) { go }
end end
......
import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { GlDropdown, GlDropdownItem, GlButton } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ClustersActions from '~/clusters_list/components/clusters_actions.vue'; import ClustersActions from '~/clusters_list/components/clusters_actions.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
...@@ -15,6 +15,7 @@ describe('ClustersActionsComponent', () => { ...@@ -15,6 +15,7 @@ describe('ClustersActionsComponent', () => {
addClusterPath, addClusterPath,
canAddCluster: true, canAddCluster: true,
displayClusterAgents: true, displayClusterAgents: true,
certificateBasedClustersEnabled: true,
}; };
const findDropdown = () => wrapper.findComponent(GlDropdown); const findDropdown = () => wrapper.findComponent(GlDropdown);
...@@ -24,6 +25,7 @@ describe('ClustersActionsComponent', () => { ...@@ -24,6 +25,7 @@ describe('ClustersActionsComponent', () => {
const findNewClusterLink = () => wrapper.findByTestId('new-cluster-link'); const findNewClusterLink = () => wrapper.findByTestId('new-cluster-link');
const findConnectClusterLink = () => wrapper.findByTestId('connect-cluster-link'); const findConnectClusterLink = () => wrapper.findByTestId('connect-cluster-link');
const findConnectNewAgentLink = () => wrapper.findByTestId('connect-new-agent-link'); const findConnectNewAgentLink = () => wrapper.findByTestId('connect-new-agent-link');
const findConnectWithAgentButton = () => wrapper.findComponent(GlButton);
const createWrapper = (provideData = {}) => { const createWrapper = (provideData = {}) => {
wrapper = shallowMountExtended(ClustersActions, { wrapper = shallowMountExtended(ClustersActions, {
...@@ -45,7 +47,7 @@ describe('ClustersActionsComponent', () => { ...@@ -45,7 +47,7 @@ describe('ClustersActionsComponent', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
describe('when the certificate based clusters are enabled', () => {
it('renders actions menu', () => { it('renders actions menu', () => {
expect(findDropdown().props('text')).toBe(CLUSTERS_ACTIONS.actionsButton); expect(findDropdown().props('text')).toBe(CLUSTERS_ACTIONS.actionsButton);
}); });
...@@ -131,4 +133,24 @@ describe('ClustersActionsComponent', () => { ...@@ -131,4 +133,24 @@ describe('ClustersActionsComponent', () => {
expect(binding.value).toBe(false); expect(binding.value).toBe(false);
}); });
}); });
});
describe('when the certificate based clusters not enabled', () => {
beforeEach(() => {
createWrapper({ certificateBasedClustersEnabled: false });
});
it('it does not show the the dropdown', () => {
expect(findDropdown().exists()).toBe(false);
});
it('shows the connect with agent button', () => {
expect(findConnectWithAgentButton().props()).toMatchObject({
disabled: !defaultProvide.canAddCluster,
category: 'primary',
variant: 'confirm',
});
expect(findConnectWithAgentButton().text()).toBe(CLUSTERS_ACTIONS.connectWithAgent);
});
});
}); });
...@@ -6,6 +6,7 @@ import InstallAgentModal from '~/clusters_list/components/install_agent_modal.vu ...@@ -6,6 +6,7 @@ import InstallAgentModal from '~/clusters_list/components/install_agent_modal.vu
import { import {
AGENT, AGENT,
CERTIFICATE_BASED, CERTIFICATE_BASED,
AGENT_TAB,
CLUSTERS_TABS, CLUSTERS_TABS,
CERTIFICATE_TAB, CERTIFICATE_TAB,
MAX_CLUSTERS_LIST, MAX_CLUSTERS_LIST,
...@@ -24,10 +25,18 @@ describe('ClustersMainViewComponent', () => { ...@@ -24,10 +25,18 @@ describe('ClustersMainViewComponent', () => {
defaultBranchName, defaultBranchName,
}; };
const createWrapper = ({ displayClusterAgents }) => { const defaultProvide = {
certificateBasedClustersEnabled: true,
displayClusterAgents: true,
};
const createWrapper = (extendedProvide = {}) => {
wrapper = shallowMountExtended(ClustersMainView, { wrapper = shallowMountExtended(ClustersMainView, {
propsData, propsData,
provide: { displayClusterAgents }, provide: {
...defaultProvide,
...extendedProvide,
},
}); });
}; };
...@@ -40,7 +49,7 @@ describe('ClustersMainViewComponent', () => { ...@@ -40,7 +49,7 @@ describe('ClustersMainViewComponent', () => {
const findGlTabAtIndex = (index) => findAllTabs().at(index); const findGlTabAtIndex = (index) => findAllTabs().at(index);
const findComponent = () => wrapper.findByTestId('clusters-tab-component'); const findComponent = () => wrapper.findByTestId('clusters-tab-component');
const findModal = () => wrapper.findComponent(InstallAgentModal); const findModal = () => wrapper.findComponent(InstallAgentModal);
describe('when the certificate based clusters are enabled', () => {
describe('when on project level', () => { describe('when on project level', () => {
beforeEach(() => { beforeEach(() => {
createWrapper({ displayClusterAgents: true }); createWrapper({ displayClusterAgents: true });
...@@ -127,4 +136,24 @@ describe('ClustersMainViewComponent', () => { ...@@ -127,4 +136,24 @@ describe('ClustersMainViewComponent', () => {
expect(findGlTabAtIndex(0).attributes('title')).toBe(CERTIFICATE_TAB.title); expect(findGlTabAtIndex(0).attributes('title')).toBe(CERTIFICATE_TAB.title);
}); });
}); });
describe('when the certificate based clusters not enabled', () => {
beforeEach(() => {
createWrapper({ certificateBasedClustersEnabled: false });
});
it('it displays only the Agent tab', () => {
expect(findAllTabs()).toHaveLength(1);
const agentTab = findGlTabAtIndex(0);
expect(agentTab.props()).toMatchObject({
queryParamValue: AGENT_TAB.queryParamValue,
titleLinkClass: '',
});
expect(agentTab.attributes()).toMatchObject({
title: AGENT_TAB.title,
});
});
});
});
}); });
...@@ -293,4 +293,25 @@ RSpec.describe ApplicationSettingsHelper do ...@@ -293,4 +293,25 @@ RSpec.describe ApplicationSettingsHelper do
it { is_expected.to eq([%w(Track track), %w(Compress compress)]) } it { is_expected.to eq([%w(Track track), %w(Compress compress)]) }
end end
describe '#instance_clusters_enabled?' do
let_it_be(:user) { create(:user) }
subject { helper.instance_clusters_enabled? }
before do
allow(helper).to receive(:current_user).and_return(user)
allow(helper).to receive(:can?).with(user, :read_cluster, instance_of(Clusters::Instance)).and_return(true)
end
it { is_expected.to be_truthy}
context ':certificate_based_clusters feature flag is disabled' do
before do
stub_feature_flags(certificate_based_clusters: false)
end
it { is_expected.to be_falsey }
end
end
end end
...@@ -136,6 +136,28 @@ RSpec.describe ClustersHelper do ...@@ -136,6 +136,28 @@ RSpec.describe ClustersHelper do
expect(subject[:display_cluster_agents]).to eq("false") expect(subject[:display_cluster_agents]).to eq("false")
end end
end end
describe 'certificate based clusters enabled' do
before do
stub_feature_flags(certificate_based_clusters: flag_enabled)
end
context 'feature flag is enabled' do
let(:flag_enabled) { true }
it do
expect(subject[:certificate_based_clusters_enabled]).to eq('true')
end
end
context 'feature flag is disabled' do
let(:flag_enabled) { false }
it do
expect(subject[:certificate_based_clusters_enabled]).to eq('false')
end
end
end
end end
describe '#js_clusters_data' do describe '#js_clusters_data' do
......
...@@ -28,5 +28,15 @@ RSpec.describe Sidebars::Groups::Menus::KubernetesMenu do ...@@ -28,5 +28,15 @@ RSpec.describe Sidebars::Groups::Menus::KubernetesMenu do
expect(menu.render?).to eq false expect(menu.render?).to eq false
end end
end end
context ':certificate_based_clusters feature flag is disabled' do
before do
stub_feature_flags(certificate_based_clusters: false)
end
it 'returns false' do
expect(menu.render?).to eq false
end
end
end end
end end
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