Commit f9da3a1c authored by Enrique Alcantara's avatar Enrique Alcantara

Validate that users selects at least two subnets

When a user is creating a EKS Cluster, they should
select at least two subnets.
parent 4c5be4df
<script> <script>
import { createNamespacedHelpers, mapState, mapActions } from 'vuex'; import { createNamespacedHelpers, mapState, mapActions, mapGetters } from 'vuex';
import { escape as esc } from 'lodash'; import { escape as esc } from 'lodash';
import { GlFormInput, GlFormCheckbox } from '@gitlab/ui'; import { GlFormInput, GlFormCheckbox } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale'; import { sprintf, s__ } from '~/locale';
...@@ -61,6 +61,7 @@ export default { ...@@ -61,6 +61,7 @@ export default {
'gitlabManagedCluster', 'gitlabManagedCluster',
'isCreatingCluster', 'isCreatingCluster',
]), ]),
...mapGetters(['subnetValid']),
...mapRolesState({ ...mapRolesState({
roles: 'items', roles: 'items',
isLoadingRoles: 'isLoadingItems', isLoadingRoles: 'isLoadingItems',
...@@ -119,7 +120,7 @@ export default { ...@@ -119,7 +120,7 @@ export default {
!this.selectedRegion || !this.selectedRegion ||
!this.selectedKeyPair || !this.selectedKeyPair ||
!this.selectedVpc || !this.selectedVpc ||
!this.selectedSubnet || !this.subnetValid ||
!this.selectedRole || !this.selectedRole ||
!this.selectedSecurityGroup || !this.selectedSecurityGroup ||
!this.selectedInstanceType || !this.selectedInstanceType ||
...@@ -127,6 +128,9 @@ export default { ...@@ -127,6 +128,9 @@ export default {
this.isCreatingCluster this.isCreatingCluster
); );
}, },
displaySubnetError() {
return Boolean(this.loadingSubnetsError) || this.selectedSubnet?.length === 1;
},
createClusterButtonLabel() { createClusterButtonLabel() {
return this.isCreatingCluster return this.isCreatingCluster
? s__('ClusterIntegration|Creating Kubernetes cluster') ? s__('ClusterIntegration|Creating Kubernetes cluster')
...@@ -216,6 +220,13 @@ export default { ...@@ -216,6 +220,13 @@ export default {
false, false,
); );
}, },
subnetValidationErrorText() {
if (this.loadingSubnetsError) {
return s__('ClusterIntegration|Could not load subnets for the selected VPC');
}
return s__('ClusterIntegration|You should select at least two subnets');
},
securityGroupDropdownHelpText() { securityGroupDropdownHelpText() {
return sprintf( return sprintf(
s__( s__(
...@@ -289,14 +300,14 @@ export default { ...@@ -289,14 +300,14 @@ export default {
this.setRegion({ region }); this.setRegion({ region });
this.setVpc({ vpc: null }); this.setVpc({ vpc: null });
this.setKeyPair({ keyPair: null }); this.setKeyPair({ keyPair: null });
this.setSubnet({ subnet: null }); this.setSubnet({ subnet: [] });
this.setSecurityGroup({ securityGroup: null }); this.setSecurityGroup({ securityGroup: null });
this.fetchVpcs({ region }); this.fetchVpcs({ region });
this.fetchKeyPairs({ region }); this.fetchKeyPairs({ region });
}, },
setVpcAndFetchSubnets(vpc) { setVpcAndFetchSubnets(vpc) {
this.setVpc({ vpc }); this.setVpc({ vpc });
this.setSubnet({ subnet: null }); this.setSubnet({ subnet: [] });
this.setSecurityGroup({ securityGroup: null }); this.setSecurityGroup({ securityGroup: null });
this.fetchSubnets({ vpc, region: this.selectedRegion }); this.fetchSubnets({ vpc, region: this.selectedRegion });
this.fetchSecurityGroups({ vpc, region: this.selectedRegion }); this.fetchSecurityGroups({ vpc, region: this.selectedRegion });
...@@ -436,8 +447,8 @@ export default { ...@@ -436,8 +447,8 @@ export default {
:placeholder="s__('ClusterIntergation|Select a subnet')" :placeholder="s__('ClusterIntergation|Select a subnet')"
:search-field-placeholder="s__('ClusterIntegration|Search subnets')" :search-field-placeholder="s__('ClusterIntegration|Search subnets')"
:empty-text="s__('ClusterIntegration|No subnet found')" :empty-text="s__('ClusterIntegration|No subnet found')"
:has-errors="Boolean(loadingSubnetsError)" :has-errors="displaySubnetError"
:error-message="s__('ClusterIntegration|Could not load subnets for the selected VPC')" :error-message="subnetValidationErrorText"
@input="setSubnet({ subnet: $event })" @input="setSubnet({ subnet: $event })"
/> />
<p class="form-text text-muted" v-html="subnetDropdownHelpText"></p> <p class="form-text text-muted" v-html="subnetDropdownHelpText"></p>
......
// eslint-disable-next-line import/prefer-default-export
export const subnetValid = ({ selectedSubnet }) =>
Array.isArray(selectedSubnet) && selectedSubnet.length >= 2;
...@@ -21,7 +21,7 @@ export default () => ({ ...@@ -21,7 +21,7 @@ export default () => ({
selectedRole: '', selectedRole: '',
selectedKeyPair: '', selectedKeyPair: '',
selectedVpc: '', selectedVpc: '',
selectedSubnet: '', selectedSubnet: [],
selectedSecurityGroup: '', selectedSecurityGroup: '',
selectedInstanceType: 'm5.large', selectedInstanceType: 'm5.large',
nodeCount: '3', nodeCount: '3',
......
...@@ -4822,6 +4822,9 @@ msgstr "" ...@@ -4822,6 +4822,9 @@ msgstr ""
msgid "ClusterIntegration|You must have an RBAC-enabled cluster to install Knative." msgid "ClusterIntegration|You must have an RBAC-enabled cluster to install Knative."
msgstr "" msgstr ""
msgid "ClusterIntegration|You should select at least two subnets"
msgstr ""
msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}" msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}"
msgstr "" msgstr ""
......
...@@ -13,6 +13,7 @@ localVue.use(Vuex); ...@@ -13,6 +13,7 @@ localVue.use(Vuex);
describe('EksClusterConfigurationForm', () => { describe('EksClusterConfigurationForm', () => {
let store; let store;
let actions; let actions;
let getters;
let state; let state;
let rolesState; let rolesState;
let regionsState; let regionsState;
...@@ -29,8 +30,7 @@ describe('EksClusterConfigurationForm', () => { ...@@ -29,8 +30,7 @@ describe('EksClusterConfigurationForm', () => {
let securityGroupsActions; let securityGroupsActions;
let vm; let vm;
beforeEach(() => { const createStore = (config = {}) => {
state = eksClusterFormState();
actions = { actions = {
createCluster: jest.fn(), createCluster: jest.fn(),
setClusterName: jest.fn(), setClusterName: jest.fn(),
...@@ -64,29 +64,44 @@ describe('EksClusterConfigurationForm', () => { ...@@ -64,29 +64,44 @@ describe('EksClusterConfigurationForm', () => {
securityGroupsActions = { securityGroupsActions = {
fetchItems: jest.fn(), fetchItems: jest.fn(),
}; };
state = {
...eksClusterFormState(),
...config.initialState,
};
rolesState = { rolesState = {
...clusterDropdownStoreState(), ...clusterDropdownStoreState(),
...config.rolesState,
}; };
regionsState = { regionsState = {
...clusterDropdownStoreState(), ...clusterDropdownStoreState(),
...config.regionsState,
}; };
vpcsState = { vpcsState = {
...clusterDropdownStoreState(), ...clusterDropdownStoreState(),
...config.vpcsState,
}; };
subnetsState = { subnetsState = {
...clusterDropdownStoreState(), ...clusterDropdownStoreState(),
...config.subnetsState,
}; };
keyPairsState = { keyPairsState = {
...clusterDropdownStoreState(), ...clusterDropdownStoreState(),
...config.keyPairsState,
}; };
securityGroupsState = { securityGroupsState = {
...clusterDropdownStoreState(), ...clusterDropdownStoreState(),
...config.securityGroupsState,
}; };
instanceTypesState = { instanceTypesState = {
...clusterDropdownStoreState(), ...clusterDropdownStoreState(),
...config.instanceTypesState,
};
getters = {
subnetValid: config?.getters?.subnetValid || (() => false),
}; };
store = new Vuex.Store({ store = new Vuex.Store({
state, state,
getters,
actions, actions,
modules: { modules: {
vpcs: { vpcs: {
...@@ -125,9 +140,29 @@ describe('EksClusterConfigurationForm', () => { ...@@ -125,9 +140,29 @@ describe('EksClusterConfigurationForm', () => {
}, },
}, },
}); });
}); };
beforeEach(() => { const createValidStateStore = initialState => {
createStore({
initialState: {
clusterName: 'cluster name',
environmentScope: '*',
selectedRegion: 'region',
selectedRole: 'role',
selectedKeyPair: 'key pair',
selectedVpc: 'vpc',
selectedSubnet: ['subnet 1', 'subnet 2'],
selectedSecurityGroup: 'group',
selectedInstanceType: 'small-1',
...initialState,
},
getters: {
subnetValid: () => true,
},
});
};
const buildWrapper = () => {
vm = shallowMount(EksClusterConfigurationForm, { vm = shallowMount(EksClusterConfigurationForm, {
localVue, localVue,
store, store,
...@@ -137,27 +172,17 @@ describe('EksClusterConfigurationForm', () => { ...@@ -137,27 +172,17 @@ describe('EksClusterConfigurationForm', () => {
externalLinkIcon: '', externalLinkIcon: '',
}, },
}); });
};
beforeEach(() => {
createStore();
buildWrapper();
}); });
afterEach(() => { afterEach(() => {
vm.destroy(); vm.destroy();
}); });
const setAllConfigurationFields = () => {
store.replaceState({
...state,
clusterName: 'cluster name',
environmentScope: '*',
selectedRegion: 'region',
selectedRole: 'role',
selectedKeyPair: 'key pair',
selectedVpc: 'vpc',
selectedSubnet: 'subnet',
selectedSecurityGroup: 'group',
selectedInstanceType: 'small-1',
});
};
const findCreateClusterButton = () => vm.find('.js-create-cluster'); const findCreateClusterButton = () => vm.find('.js-create-cluster');
const findClusterNameInput = () => vm.find('[id=eks-cluster-name]'); const findClusterNameInput = () => vm.find('[id=eks-cluster-name]');
const findEnvironmentScopeInput = () => vm.find('[id=eks-environment-scope]'); const findEnvironmentScopeInput = () => vm.find('[id=eks-environment-scope]');
...@@ -310,12 +335,29 @@ describe('EksClusterConfigurationForm', () => { ...@@ -310,12 +335,29 @@ describe('EksClusterConfigurationForm', () => {
expect(findSubnetDropdown().props('items')).toBe(subnetsState.items); expect(findSubnetDropdown().props('items')).toBe(subnetsState.items);
}); });
it('sets SubnetDropdown hasErrors to true when loading subnets fails', () => { it('displays a validation error in the subnet dropdown when loading subnets fails', () => {
subnetsState.loadingItemsError = new Error(); createStore({
subnetsState: {
loadingItemsError: new Error(),
},
});
buildWrapper();
return Vue.nextTick().then(() => { expect(findSubnetDropdown().props('hasErrors')).toEqual(true);
expect(findSubnetDropdown().props('hasErrors')).toEqual(true); });
it('displays a validation error in the subnet dropdown when a single subnet is selected', () => {
createStore({
initialState: {
selectedSubnet: ['subnet 1'],
},
}); });
buildWrapper();
expect(findSubnetDropdown().props('hasErrors')).toEqual(true);
expect(findSubnetDropdown().props('errorMessage')).toEqual(
'You should select at least two subnets',
);
}); });
it('disables SecurityGroupDropdown when no vpc is selected', () => { it('disables SecurityGroupDropdown when no vpc is selected', () => {
...@@ -386,11 +428,7 @@ describe('EksClusterConfigurationForm', () => { ...@@ -386,11 +428,7 @@ describe('EksClusterConfigurationForm', () => {
}); });
it('cleans selected subnet', () => { it('cleans selected subnet', () => {
expect(actions.setSubnet).toHaveBeenCalledWith( expect(actions.setSubnet).toHaveBeenCalledWith(expect.anything(), { subnet: [] }, undefined);
expect.anything(),
{ subnet: null },
undefined,
);
}); });
it('cleans selected security group', () => { it('cleans selected security group', () => {
...@@ -464,11 +502,7 @@ describe('EksClusterConfigurationForm', () => { ...@@ -464,11 +502,7 @@ describe('EksClusterConfigurationForm', () => {
}); });
it('cleans selected subnet', () => { it('cleans selected subnet', () => {
expect(actions.setSubnet).toHaveBeenCalledWith( expect(actions.setSubnet).toHaveBeenCalledWith(expect.anything(), { subnet: [] }, undefined);
expect.anything(),
{ subnet: null },
undefined,
);
}); });
it('cleans selected security group', () => { it('cleans selected security group', () => {
...@@ -573,22 +607,19 @@ describe('EksClusterConfigurationForm', () => { ...@@ -573,22 +607,19 @@ describe('EksClusterConfigurationForm', () => {
}); });
describe('when all cluster configuration fields are set', () => { describe('when all cluster configuration fields are set', () => {
beforeEach(() => {
setAllConfigurationFields();
});
it('enables create cluster button', () => { it('enables create cluster button', () => {
createValidStateStore();
buildWrapper();
expect(findCreateClusterButton().props('disabled')).toBe(false); expect(findCreateClusterButton().props('disabled')).toBe(false);
}); });
}); });
describe('when at least one cluster configuration field is not set', () => { describe('when at least one cluster configuration field is not set', () => {
beforeEach(() => { beforeEach(() => {
setAllConfigurationFields(); createValidStateStore({
store.replaceState({ clusterName: null,
...state,
clusterName: '',
}); });
buildWrapper();
}); });
it('disables create cluster button', () => { it('disables create cluster button', () => {
...@@ -596,13 +627,12 @@ describe('EksClusterConfigurationForm', () => { ...@@ -596,13 +627,12 @@ describe('EksClusterConfigurationForm', () => {
}); });
}); });
describe('when isCreatingCluster', () => { describe('when is creating cluster', () => {
beforeEach(() => { beforeEach(() => {
setAllConfigurationFields(); createValidStateStore({
store.replaceState({
...state,
isCreatingCluster: true, isCreatingCluster: true,
}); });
buildWrapper();
}); });
it('sets create cluster button as loading', () => { it('sets create cluster button as loading', () => {
......
import { subnetValid } from '~/create_cluster/eks_cluster/store/getters';
describe('EKS Cluster Store Getters', () => {
describe('subnetValid', () => {
it('returns true if there are 2 or more selected subnets', () => {
expect(subnetValid({ selectedSubnet: [1, 2] })).toBe(true);
});
it.each([[[], [1]]])('returns false if there are 1 or less selected subnets', subnets => {
expect(subnetValid({ selectedSubnet: subnets })).toBe(false);
});
});
});
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