Commit eed996ac authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent b42f312d
...@@ -2,14 +2,19 @@ ...@@ -2,14 +2,19 @@
import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue'; import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue'; import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue'; import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
import { GlIcon } from '@gitlab/ui';
const findItem = (items, valueProp, value) => items.find(item => item[valueProp] === value); const toArray = value => [].concat(value);
const itemsProp = (items, prop) => items.map(item => item[prop]);
const defaultSearchFn = (searchQuery, labelProp) => item =>
item[labelProp].toLowerCase().indexOf(searchQuery) > -1;
export default { export default {
components: { components: {
DropdownButton, DropdownButton,
DropdownSearchInput, DropdownSearchInput,
DropdownHiddenInput, DropdownHiddenInput,
GlIcon,
}, },
props: { props: {
fieldName: { fieldName: {
...@@ -28,7 +33,7 @@ export default { ...@@ -28,7 +33,7 @@ export default {
default: '', default: '',
}, },
value: { value: {
type: [Object, String], type: [Object, Array, String],
required: false, required: false,
default: () => null, default: () => null,
}, },
...@@ -72,6 +77,11 @@ export default { ...@@ -72,6 +77,11 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
multiple: {
type: Boolean,
required: false,
default: false,
},
errorMessage: { errorMessage: {
type: String, type: String,
required: false, required: false,
...@@ -90,12 +100,11 @@ export default { ...@@ -90,12 +100,11 @@ export default {
searchFn: { searchFn: {
type: Function, type: Function,
required: false, required: false,
default: searchQuery => item => item.name.toLowerCase().indexOf(searchQuery) > -1, default: defaultSearchFn,
}, },
}, },
data() { data() {
return { return {
selectedItem: findItem(this.items, this.value),
searchQuery: '', searchQuery: '',
}; };
}, },
...@@ -109,36 +118,52 @@ export default { ...@@ -109,36 +118,52 @@ export default {
return this.disabledText; return this.disabledText;
} }
if (!this.selectedItem) { if (!this.selectedItems.length) {
return this.placeholder; return this.placeholder;
} }
return this.selectedItemLabel; return this.selectedItemsLabels;
}, },
results() { results() {
if (!this.items) { return this.getItemsOrEmptyList().filter(this.searchFn(this.searchQuery, this.labelProperty));
return [];
}
return this.items.filter(this.searchFn(this.searchQuery));
}, },
selectedItemLabel() { selectedItems() {
return this.selectedItem && this.selectedItem[this.labelProperty]; const valueProp = this.valueProperty;
const valueList = toArray(this.value);
const items = this.getItemsOrEmptyList();
return items.filter(item => valueList.some(value => item[valueProp] === value));
}, },
selectedItemValue() { selectedItemsLabels() {
return (this.selectedItem && this.selectedItem[this.valueProperty]) || ''; return itemsProp(this.selectedItems, this.labelProperty).join(', ');
}, },
}, selectedItemsValues() {
watch: { return itemsProp(this.selectedItems, this.valueProperty).join(', ');
value(value) {
this.selectedItem = findItem(this.items, this.valueProperty, value);
}, },
}, },
methods: { methods: {
select(item) { getItemsOrEmptyList() {
this.selectedItem = item; return this.items || [];
},
selectSingle(item) {
this.$emit('input', item[this.valueProperty]); this.$emit('input', item[this.valueProperty]);
}, },
selectMultiple(item) {
const value = toArray(this.value);
const itemValue = item[this.valueProperty];
const itemValueIndex = value.indexOf(itemValue);
if (itemValueIndex > -1) {
value.splice(itemValueIndex, 1);
} else {
value.push(itemValue);
}
this.$emit('input', value);
},
isSelected(item) {
return this.selectedItems.includes(item);
},
}, },
}; };
</script> </script>
...@@ -146,7 +171,7 @@ export default { ...@@ -146,7 +171,7 @@ export default {
<template> <template>
<div> <div>
<div class="js-gcp-machine-type-dropdown dropdown"> <div class="js-gcp-machine-type-dropdown dropdown">
<dropdown-hidden-input :name="fieldName" :value="selectedItemValue" /> <dropdown-hidden-input :name="fieldName" :value="selectedItemsValues" />
<dropdown-button <dropdown-button
:class="{ 'border-danger': hasErrors }" :class="{ 'border-danger': hasErrors }"
:is-disabled="disabled" :is-disabled="disabled"
...@@ -158,15 +183,28 @@ export default { ...@@ -158,15 +183,28 @@ export default {
<div class="dropdown-content"> <div class="dropdown-content">
<ul> <ul>
<li v-if="!results.length"> <li v-if="!results.length">
<span class="js-empty-text menu-item"> <span class="js-empty-text menu-item">{{ emptyText }}</span>
{{ emptyText }}
</span>
</li> </li>
<li v-for="item in results" :key="item.id"> <li v-for="item in results" :key="item.id">
<button class="js-dropdown-item" type="button" @click.prevent="select(item)"> <button
<slot name="item" :item="item"> v-if="multiple"
{{ item.name }} class="js-dropdown-item d-flex align-items-center"
</slot> type="button"
@click.stop.prevent="selectMultiple(item)"
>
<gl-icon
:class="[{ invisible: !isSelected(item) }, 'mr-1']"
name="mobile-issue-close"
/>
<slot name="item" :item="item">{{ item.name }}</slot>
</button>
<button
v-else
class="js-dropdown-item"
type="button"
@click.prevent="selectSingle(item)"
>
<slot name="item" :item="item">{{ item.name }}</slot>
</button> </button>
</li> </li>
</ul> </ul>
...@@ -182,8 +220,7 @@ export default { ...@@ -182,8 +220,7 @@ export default {
'text-muted': !hasErrors, 'text-muted': !hasErrors,
}, },
]" ]"
>{{ errorMessage }}</span
> >
{{ errorMessage }}
</span>
</div> </div>
</template> </template>
...@@ -41,6 +41,7 @@ export default { ...@@ -41,6 +41,7 @@ export default {
v-if="hasCredentials" v-if="hasCredentials"
:gitlab-managed-cluster-help-path="gitlabManagedClusterHelpPath" :gitlab-managed-cluster-help-path="gitlabManagedClusterHelpPath"
:kubernetes-integration-help-path="kubernetesIntegrationHelpPath" :kubernetes-integration-help-path="kubernetesIntegrationHelpPath"
:external-link-icon="externalLinkIcon"
/> />
<service-credentials-form <service-credentials-form
v-else v-else
......
<script>
import { sprintf, s__ } from '~/locale';
import ClusterFormDropdown from './cluster_form_dropdown.vue';
export default {
components: {
ClusterFormDropdown,
},
props: {
regions: {
type: Array,
required: false,
default: () => [],
},
loading: {
type: Boolean,
required: false,
default: false,
},
error: {
type: Object,
required: false,
default: null,
},
},
computed: {
hasErrors() {
return Boolean(this.error);
},
helpText() {
return sprintf(
s__('ClusterIntegration|Learn more about %{startLink}Regions%{endLink}.'),
{
startLink:
'<a href="https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services/" target="_blank" rel="noopener noreferrer">',
endLink: '</a>',
},
false,
);
},
},
};
</script>
<template>
<div>
<cluster-form-dropdown
field-id="eks-region"
field-name="eks-region"
:items="regions"
:loading="loading"
:loading-text="s__('ClusterIntegration|Loading Regions')"
:placeholder="s__('ClusterIntergation|Select a region')"
:search-field-placeholder="s__('ClusterIntegration|Search regions')"
:empty-text="s__('ClusterIntegration|No region found')"
:has-errors="hasErrors"
:error-message="s__('ClusterIntegration|Could not load regions from your AWS account')"
v-bind="$attrs"
v-on="$listeners"
/>
<p class="form-text text-muted" v-html="helpText"></p>
</div>
</template>
...@@ -131,7 +131,7 @@ export default { ...@@ -131,7 +131,7 @@ export default {
<p class="form-text text-muted" v-html="provisionRoleArnHelpText"></p> <p class="form-text text-muted" v-html="provisionRoleArnHelpText"></p>
</div> </div>
<loading-button <loading-button
class="js-submit-service-credentials" class="js-submit-service-credentials btn-success"
type="submit" type="submit"
:disabled="submitButtonDisabled" :disabled="submitButtonDisabled"
:loading="isCreatingRole" :loading="isCreatingRole"
......
// eslint-disable-next-line import/prefer-default-export // eslint-disable-next-line import/prefer-default-export
export const KUBERNETES_VERSIONS = [ export const KUBERNETES_VERSIONS = [{ name: '1.14', value: '1.14' }];
{ name: '1.14', value: '1.14' },
{ name: '1.13', value: '1.13' },
{ name: '1.12', value: '1.12' },
{ name: '1.11', value: '1.11' },
];
...@@ -12,10 +12,19 @@ export default el => { ...@@ -12,10 +12,19 @@ export default el => {
kubernetesIntegrationHelpPath, kubernetesIntegrationHelpPath,
accountAndExternalIdsHelpPath, accountAndExternalIdsHelpPath,
createRoleArnHelpPath, createRoleArnHelpPath,
getRolesPath,
getRegionsPath,
getKeyPairsPath,
getVpcsPath,
getSubnetsPath,
getSecurityGroupsPath,
getInstanceTypesPath,
externalId, externalId,
accountId, accountId,
hasCredentials, hasCredentials,
createRolePath, createRolePath,
createClusterPath,
signOutPath,
externalLinkIcon, externalLinkIcon,
} = el.dataset; } = el.dataset;
...@@ -27,6 +36,17 @@ export default el => { ...@@ -27,6 +36,17 @@ export default el => {
externalId, externalId,
accountId, accountId,
createRolePath, createRolePath,
createClusterPath,
signOutPath,
},
apiPaths: {
getRolesPath,
getRegionsPath,
getKeyPairsPath,
getVpcsPath,
getSubnetsPath,
getSecurityGroupsPath,
getInstanceTypesPath,
}, },
}), }),
components: { components: {
......
import EC2 from 'aws-sdk/clients/ec2'; import axios from '~/lib/utils/axios_utils';
import IAM from 'aws-sdk/clients/iam';
export default apiPaths => ({
export const fetchRoles = () => { fetchRoles() {
const iam = new IAM(); return axios
.get(apiPaths.getRolesPath)
return iam .then(({ data: { roles } }) =>
.listRoles() roles.map(({ role_name: name, arn: value }) => ({ name, value })),
.promise() );
.then(({ Roles: roles }) => roles.map(({ RoleName: name }) => ({ name }))); },
}; fetchKeyPairs({ region }) {
return axios
export const fetchKeyPairs = () => { .get(apiPaths.getKeyPairsPath, { params: { region } })
const ec2 = new EC2(); .then(({ data: { key_pairs: keyPairs } }) =>
keyPairs.map(({ key_name }) => ({ name: key_name, value: key_name })),
return ec2 );
.describeKeyPairs() },
.promise() fetchRegions() {
.then(({ KeyPairs: keyPairs }) => keyPairs.map(({ RegionName: name }) => ({ name }))); return axios.get(apiPaths.getRegionsPath).then(({ data: { regions } }) =>
}; regions.map(({ region_name }) => ({
name: region_name,
export const fetchRegions = () => { value: region_name,
const ec2 = new EC2();
return ec2
.describeRegions()
.promise()
.then(({ Regions: regions }) =>
regions.map(({ RegionName: name }) => ({
name,
value: name,
})), })),
); );
}; },
fetchVpcs({ region }) {
export const fetchVpcs = () => { return axios.get(apiPaths.getVpcsPath, { params: { region } }).then(({ data: { vpcs } }) =>
const ec2 = new EC2(); vpcs.map(({ vpc_id }) => ({
value: vpc_id,
return ec2 name: vpc_id,
.describeVpcs()
.promise()
.then(({ Vpcs: vpcs }) =>
vpcs.map(({ VpcId: id }) => ({
value: id,
name: id,
})), })),
); );
}; },
fetchSubnets({ vpc, region }) {
export const fetchSubnets = ({ vpc }) => { return axios
const ec2 = new EC2(); .get(apiPaths.getSubnetsPath, { params: { vpc_id: vpc, region } })
.then(({ data: { subnets } }) =>
return ec2 subnets.map(({ subnet_id }) => ({ name: subnet_id, value: subnet_id })),
.describeSubnets({ );
Filters: [ },
{ fetchSecurityGroups({ vpc, region }) {
Name: 'vpc-id', return axios
Values: [vpc], .get(apiPaths.getSecurityGroupsPath, { params: { vpc_id: vpc, region } })
}, .then(({ data: { security_groups: securityGroups } }) =>
], securityGroups.map(({ group_name: name, group_id: value }) => ({ name, value })),
}) );
.promise() },
.then(({ Subnets: subnets }) => subnets.map(({ SubnetId: id }) => ({ id, name: id }))); fetchInstanceTypes() {
}; return axios
.get(apiPaths.getInstanceTypesPath)
export const fetchSecurityGroups = ({ vpc }) => { .then(({ data: { instance_types: instanceTypes } }) =>
const ec2 = new EC2(); instanceTypes.map(({ instance_type_name }) => ({
name: instance_type_name,
return ec2 value: instance_type_name,
.describeSecurityGroups({ })),
Filters: [ );
{ },
Name: 'vpc-id', });
Values: [vpc],
},
],
})
.promise()
.then(({ SecurityGroups: securityGroups }) =>
securityGroups.map(({ GroupName: name, GroupId: value }) => ({ name, value })),
);
};
export default () => {};
import * as types from './mutation_types'; import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
const getErrorMessage = data => {
const errorKey = Object.keys(data)[0];
return data[errorKey][0];
};
export const setClusterName = ({ commit }, payload) => { export const setClusterName = ({ commit }, payload) => {
commit(types.SET_CLUSTER_NAME, payload); commit(types.SET_CLUSTER_NAME, payload);
...@@ -37,6 +44,44 @@ export const createRoleError = ({ commit }, payload) => { ...@@ -37,6 +44,44 @@ export const createRoleError = ({ commit }, payload) => {
commit(types.CREATE_ROLE_ERROR, payload); commit(types.CREATE_ROLE_ERROR, payload);
}; };
export const createCluster = ({ dispatch, state }) => {
dispatch('requestCreateCluster');
return axios
.post(state.createClusterPath, {
name: state.clusterName,
environment_scope: state.environmentScope,
managed: state.gitlabManagedCluster,
provider_aws_attributes: {
region: state.selectedRegion,
vpc_id: state.selectedVpc,
subnet_ids: state.selectedSubnet,
role_arn: state.selectedRole,
key_name: state.selectedKeyPair,
security_group_id: state.selectedSecurityGroup,
instance_type: state.selectedInstanceType,
num_nodes: state.nodeCount,
},
})
.then(({ headers: { location } }) => dispatch('createClusterSuccess', location))
.catch(({ response: { data } }) => {
dispatch('createClusterError', data);
});
};
export const requestCreateCluster = ({ commit }) => {
commit(types.REQUEST_CREATE_CLUSTER);
};
export const createClusterSuccess = (_, location) => {
window.location.assign(location);
};
export const createClusterError = ({ commit }, error) => {
commit(types.CREATE_CLUSTER_ERROR, error);
createFlash(getErrorMessage(error));
};
export const setRegion = ({ commit }, payload) => { export const setRegion = ({ commit }, payload) => {
commit(types.SET_REGION, payload); commit(types.SET_REGION, payload);
}; };
...@@ -64,3 +109,17 @@ export const setSecurityGroup = ({ commit }, payload) => { ...@@ -64,3 +109,17 @@ export const setSecurityGroup = ({ commit }, payload) => {
export const setGitlabManagedCluster = ({ commit }, payload) => { export const setGitlabManagedCluster = ({ commit }, payload) => {
commit(types.SET_GITLAB_MANAGED_CLUSTER, payload); commit(types.SET_GITLAB_MANAGED_CLUSTER, payload);
}; };
export const setInstanceType = ({ commit }, payload) => {
commit(types.SET_INSTANCE_TYPE, payload);
};
export const setNodeCount = ({ commit }, payload) => {
commit(types.SET_NODE_COUNT, payload);
};
export const signOut = ({ commit, state: { signOutPath } }) =>
axios
.delete(signOutPath)
.then(() => commit(types.SIGN_OUT))
.catch(({ response: { data } }) => createFlash(getErrorMessage(data)));
...@@ -6,10 +6,12 @@ import state from './state'; ...@@ -6,10 +6,12 @@ import state from './state';
import clusterDropdownStore from './cluster_dropdown'; import clusterDropdownStore from './cluster_dropdown';
import * as awsServices from '../services/aws_services_facade'; import awsServicesFactory from '../services/aws_services_facade';
const createStore = ({ initialState }) => const createStore = ({ initialState, apiPaths }) => {
new Vuex.Store({ const awsServices = awsServicesFactory(apiPaths);
return new Vuex.Store({
actions, actions,
getters, getters,
mutations, mutations,
...@@ -39,7 +41,12 @@ const createStore = ({ initialState }) => ...@@ -39,7 +41,12 @@ const createStore = ({ initialState }) =>
namespaced: true, namespaced: true,
...clusterDropdownStore(awsServices.fetchSecurityGroups), ...clusterDropdownStore(awsServices.fetchSecurityGroups),
}, },
instanceTypes: {
namespaced: true,
...clusterDropdownStore(awsServices.fetchInstanceTypes),
},
}, },
}); });
};
export default createStore; export default createStore;
...@@ -7,7 +7,13 @@ export const SET_KEY_PAIR = 'SET_KEY_PAIR'; ...@@ -7,7 +7,13 @@ export const SET_KEY_PAIR = 'SET_KEY_PAIR';
export const SET_SUBNET = 'SET_SUBNET'; export const SET_SUBNET = 'SET_SUBNET';
export const SET_ROLE = 'SET_ROLE'; export const SET_ROLE = 'SET_ROLE';
export const SET_SECURITY_GROUP = 'SET_SECURITY_GROUP'; export const SET_SECURITY_GROUP = 'SET_SECURITY_GROUP';
export const SET_INSTANCE_TYPE = 'SET_INSTANCE_TYPE';
export const SET_NODE_COUNT = 'SET_NODE_COUNT';
export const SET_GITLAB_MANAGED_CLUSTER = 'SET_GITLAB_MANAGED_CLUSTER'; export const SET_GITLAB_MANAGED_CLUSTER = 'SET_GITLAB_MANAGED_CLUSTER';
export const REQUEST_CREATE_ROLE = 'REQUEST_CREATE_ROLE'; export const REQUEST_CREATE_ROLE = 'REQUEST_CREATE_ROLE';
export const CREATE_ROLE_SUCCESS = 'CREATE_ROLE_SUCCESS'; export const CREATE_ROLE_SUCCESS = 'CREATE_ROLE_SUCCESS';
export const CREATE_ROLE_ERROR = 'CREATE_ROLE_ERROR'; export const CREATE_ROLE_ERROR = 'CREATE_ROLE_ERROR';
export const SIGN_OUT = 'SIGN_OUT';
export const REQUEST_CREATE_CLUSTER = 'REQUEST_CREATE_CLUSTER';
export const CREATE_CLUSTER_SUCCESS = 'CREATE_CLUSTER_SUCCESS';
export const CREATE_CLUSTER_ERROR = 'CREATE_CLUSTER_ERROR';
...@@ -28,6 +28,12 @@ export default { ...@@ -28,6 +28,12 @@ export default {
[types.SET_SECURITY_GROUP](state, { securityGroup }) { [types.SET_SECURITY_GROUP](state, { securityGroup }) {
state.selectedSecurityGroup = securityGroup; state.selectedSecurityGroup = securityGroup;
}, },
[types.SET_INSTANCE_TYPE](state, { instanceType }) {
state.selectedInstanceType = instanceType;
},
[types.SET_NODE_COUNT](state, { nodeCount }) {
state.nodeCount = nodeCount;
},
[types.SET_GITLAB_MANAGED_CLUSTER](state, { gitlabManagedCluster }) { [types.SET_GITLAB_MANAGED_CLUSTER](state, { gitlabManagedCluster }) {
state.gitlabManagedCluster = gitlabManagedCluster; state.gitlabManagedCluster = gitlabManagedCluster;
}, },
...@@ -46,4 +52,15 @@ export default { ...@@ -46,4 +52,15 @@ export default {
state.createRoleError = error; state.createRoleError = error;
state.hasCredentials = false; state.hasCredentials = false;
}, },
[types.REQUEST_CREATE_CLUSTER](state) {
state.isCreatingCluster = true;
state.createClusterError = null;
},
[types.CREATE_CLUSTER_ERROR](state, { error }) {
state.isCreatingCluster = false;
state.createClusterError = error;
},
[types.SIGN_OUT](state) {
state.hasCredentials = false;
},
}; };
import { KUBERNETES_VERSIONS } from '../constants'; import { KUBERNETES_VERSIONS } from '../constants';
const [{ value: kubernetesVersion }] = KUBERNETES_VERSIONS;
export default () => ({ export default () => ({
createRolePath: null, createRolePath: null,
...@@ -12,13 +14,18 @@ export default () => ({ ...@@ -12,13 +14,18 @@ export default () => ({
clusterName: '', clusterName: '',
environmentScope: '*', environmentScope: '*',
kubernetesVersion: [KUBERNETES_VERSIONS].value, kubernetesVersion,
selectedRegion: '', selectedRegion: '',
selectedRole: '', selectedRole: '',
selectedKeyPair: '', selectedKeyPair: '',
selectedVpc: '', selectedVpc: '',
selectedSubnet: '', selectedSubnet: '',
selectedSecurityGroup: '', selectedSecurityGroup: '',
selectedInstanceType: 'm5.large',
nodeCount: '3',
isCreatingCluster: false,
createClusterError: false,
gitlabManagedCluster: true, gitlabManagedCluster: true,
}); });
...@@ -94,8 +94,7 @@ ...@@ -94,8 +94,7 @@
- else - else
= f.text_field :name, label: s_('Profiles|Full name'), required: true, title: s_("Profiles|Using emojis in names seems fun, but please try to set a status message instead"), wrapper: { class: 'col-md-9 qa-full-name rspec-full-name' }, help: s_("Profiles|Enter your name, so people you know can recognize you") = f.text_field :name, label: s_('Profiles|Full name'), required: true, title: s_("Profiles|Using emojis in names seems fun, but please try to set a status message instead"), wrapper: { class: 'col-md-9 qa-full-name rspec-full-name' }, help: s_("Profiles|Enter your name, so people you know can recognize you")
= f.text_field :id, readonly: true, label: s_('Profiles|User ID'), wrapper: { class: 'col-md-3' } = f.text_field :id, readonly: true, label: s_('Profiles|User ID'), wrapper: { class: 'col-md-3' }
- if experiment_enabled?(:signup_flow) = f.select :role, ::User.roles.keys.map { |role| [role.titleize, role] }, { prompt: _('Select your role') }, required: true, class: 'input-md'
= f.select :role, ::User.roles.keys.map { |role| [role.titleize, role] }, { prompt: _('Select your role') }, class: 'input-md'
= render_if_exists 'profiles/email_settings', form: f = render_if_exists 'profiles/email_settings', form: f
= f.text_field :skype, class: 'input-md', placeholder: s_("Profiles|username") = f.text_field :skype, class: 'input-md', placeholder: s_("Profiles|username")
......
---
title: Build CI cache key from commit SHAs that changed given files
merge_request: 19392
author:
type: added
---
title: Create AWS EKS cluster
merge_request: 19578
author:
type: added
---
title: Make role required when editing profile
merge_request: 19636
author:
type: changed
...@@ -61,7 +61,7 @@ GET /groups/:id/packages ...@@ -61,7 +61,7 @@ GET /groups/:id/packages
| `exclude_subgroups` | boolean | false | If the param is included as true, packages from projects from subgroups are not listed. Default is `false`. | | `exclude_subgroups` | boolean | false | If the param is included as true, packages from projects from subgroups are not listed. Default is `false`. |
```bash ```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/group/:id/packages?exclude_subgroups=true curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/:id/packages?exclude_subgroups=true
``` ```
Example response: Example response:
......
...@@ -1535,6 +1535,50 @@ cache: ...@@ -1535,6 +1535,50 @@ cache:
- binaries/ - binaries/
``` ```
##### `cache:key:files`
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/18986) in GitLab v12.5.
If `cache:key:files` is added, the cache `key` will use the SHA of the most recent commit
that changed either of the given files. If neither file was changed in any commits, the key will be `default`.
A maximum of two files are allowed.
```yaml
cache:
key:
files:
- Gemfile.lock
- package.json
paths:
- vendor/ruby
- node_modules
```
##### `cache:key:prefix`
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/18986) in GitLab v12.5.
The `prefix` parameter adds extra functionality to `key:files` by allowing the key to
be composed of the given `prefix` combined with the SHA of the most recent commit
that changed either of the files. For example, adding a `prefix` of `rspec`, will
cause keys to look like: `rspec-feef9576d21ee9b6a32e30c5c79d0a0ceb68d1e5`. If neither
file was changed in any commits, the prefix is added to `default`, so the key in the
example would be `rspec-default`.
`prefix` follows the same restrictions as `key`, so it can use any of the
[predefined variables](../variables/README.md). Similarly, the `/` character or the
equivalent URI-encoded `%2F`, or a value made only of `.` or `%2E`, is not allowed.
```yaml
cache:
key:
files:
- Gemfile.lock
prefix: ${CI_JOB_NAME}
paths:
- vendor/ruby
```
#### `cache:untracked` #### `cache:untracked`
Set `untracked: true` to cache all files that are untracked in your Git Set `untracked: true` to cache all files that are untracked in your Git
......
...@@ -52,20 +52,16 @@ isn't gated by a License or Plan. ...@@ -52,20 +52,16 @@ isn't gated by a License or Plan.
[namespace-fa]: https://gitlab.com/gitlab-org/gitlab/blob/4cc1c62918aa4c31750cb21dfb1a6c3492d71080/ee/app/models/ee/namespace.rb#L71-85 [namespace-fa]: https://gitlab.com/gitlab-org/gitlab/blob/4cc1c62918aa4c31750cb21dfb1a6c3492d71080/ee/app/models/ee/namespace.rb#L71-85
[license-fa]: https://gitlab.com/gitlab-org/gitlab/blob/4cc1c62918aa4c31750cb21dfb1a6c3492d71080/ee/app/models/license.rb#L293-300 [license-fa]: https://gitlab.com/gitlab-org/gitlab/blob/4cc1c62918aa4c31750cb21dfb1a6c3492d71080/ee/app/models/license.rb#L293-300
An important side-effect of the implicit feature flags mentioned above is that **An important side-effect of the implicit feature flags mentioned above is that
unless the feature is explicitly disabled or limited to a percentage of users, unless the feature is explicitly disabled or limited to a percentage of users,
the feature flag check will default to `true`. the feature flag check will default to `true`.**
As an example, if you were to ship the backend half of a feature behind a flag, As an example, if you were to ship the backend half of a feature behind a flag,
you'd want to explicitly disable that flag until the frontend half is also ready you'd want to explicitly disable that flag until the frontend half is also ready
to be shipped. [You can do this via Chatops](controls.md): to be shipped. To make sure this feature is disabled for both GitLab.com and
self-managed instances you'd need to explicitly call `Feature.enabled?` method
``` before the `feature_available` method. This ensures the feature_flag is defaulting
/chatops run feature set some_feature 0 to `true`.
```
Note that you can do this at any time, even before the merge request using the
flag has been merged!
## Feature groups ## Feature groups
......
# frozen_string_literal: true
module Gitlab
module Ci
class Config
module Entry
##
# Entry that represents an array of file paths.
#
class Files < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, array_of_strings: true
validates :config, length: {
minimum: 1,
maximum: 2,
too_short: 'requires at least %{count} item',
too_long: 'has too many items (maximum is %{count})'
}
end
end
end
end
end
end
...@@ -7,11 +7,48 @@ module Gitlab ...@@ -7,11 +7,48 @@ module Gitlab
## ##
# Entry that represents a key. # Entry that represents a key.
# #
class Key < ::Gitlab::Config::Entry::Node class Key < ::Gitlab::Config::Entry::Simplifiable
include ::Gitlab::Config::Entry::Validatable strategy :SimpleKey, if: -> (config) { config.is_a?(String) || config.is_a?(Symbol) }
strategy :ComplexKey, if: -> (config) { config.is_a?(Hash) }
validations do class SimpleKey < ::Gitlab::Config::Entry::Node
validates :config, key: true include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, key: true
end
def self.default
'default'
end
def value
super.to_s
end
end
class ComplexKey < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Attributable
include ::Gitlab::Config::Entry::Configurable
ALLOWED_KEYS = %i[files prefix].freeze
REQUIRED_KEYS = %i[files].freeze
validations do
validates :config, allowed_keys: ALLOWED_KEYS
validates :config, required_keys: REQUIRED_KEYS
end
entry :files, Entry::Files,
description: 'Files that should be used to build the key'
entry :prefix, Entry::Prefix,
description: 'Prefix that is added to the final cache key'
end
class UnknownStrategy < ::Gitlab::Config::Entry::Node
def errors
["#{location} should be a hash, a string or a symbol"]
end
end end
def self.default def self.default
......
# frozen_string_literal: true
module Gitlab
module Ci
class Config
module Entry
##
# Entry that represents a key prefix.
#
class Prefix < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, key: true
end
end
end
end
end
end
...@@ -29,6 +29,8 @@ module Gitlab ...@@ -29,6 +29,8 @@ module Gitlab
.fabricate(attributes.delete(:except)) .fabricate(attributes.delete(:except))
@rules = Gitlab::Ci::Build::Rules @rules = Gitlab::Ci::Build::Rules
.new(attributes.delete(:rules)) .new(attributes.delete(:rules))
@cache = Seed::Build::Cache
.new(pipeline, attributes.delete(:cache))
end end
def name def name
...@@ -59,6 +61,7 @@ module Gitlab ...@@ -59,6 +61,7 @@ module Gitlab
@seed_attributes @seed_attributes
.deep_merge(pipeline_attributes) .deep_merge(pipeline_attributes)
.deep_merge(rules_attributes) .deep_merge(rules_attributes)
.deep_merge(cache_attributes)
end end
def bridge? def bridge?
...@@ -150,6 +153,12 @@ module Gitlab ...@@ -150,6 +153,12 @@ module Gitlab
@using_rules ? @rules.evaluate(@pipeline, self).build_attributes : {} @using_rules ? @rules.evaluate(@pipeline, self).build_attributes : {}
end end
end end
def cache_attributes
strong_memoize(:cache_attributes) do
@cache.build_attributes
end
end
end end
end end
end end
......
# frozen_string_literal: true
module Gitlab
module Ci
module Pipeline
module Seed
class Build
class Cache
def initialize(pipeline, cache)
@pipeline = pipeline
local_cache = cache.to_h.deep_dup
@key = local_cache.delete(:key)
@paths = local_cache.delete(:paths)
@policy = local_cache.delete(:policy)
@untracked = local_cache.delete(:untracked)
raise ArgumentError, "unknown cache keys: #{local_cache.keys}" if local_cache.any?
end
def build_attributes
{
options: {
cache: {
key: key_string,
paths: @paths,
policy: @policy,
untracked: @untracked
}.compact.presence
}.compact
}
end
private
def key_string
key_from_string || key_from_files
end
def key_from_string
@key.to_s if @key.is_a?(String) || @key.is_a?(Symbol)
end
def key_from_files
return unless @key.is_a?(Hash)
[@key[:prefix], files_digest].select(&:present?).join('-')
end
def files_digest
hash_of_the_latest_changes || 'default'
end
def hash_of_the_latest_changes
return unless Feature.enabled?(:ci_file_based_cache, @pipeline.project, default_enabled: true)
ids = files.map { |path| last_commit_id_for_path(path) }
ids = ids.compact.sort.uniq
Digest::SHA1.hexdigest(ids.join('-')) if ids.any?
end
def files
@key[:files]
.to_a
.select(&:present?)
.uniq
end
def last_commit_id_for_path(path)
@pipeline.project.repository.last_commit_id_for_path(@pipeline.sha, path)
end
end
end
end
end
end
end
...@@ -43,11 +43,11 @@ module Gitlab ...@@ -43,11 +43,11 @@ module Gitlab
needs_attributes: job.dig(:needs, :job), needs_attributes: job.dig(:needs, :job),
interruptible: job[:interruptible], interruptible: job[:interruptible],
rules: job[:rules], rules: job[:rules],
cache: job[:cache],
options: { options: {
image: job[:image], image: job[:image],
services: job[:services], services: job[:services],
artifacts: job[:artifacts], artifacts: job[:artifacts],
cache: job[:cache],
dependencies: job[:dependencies], dependencies: job[:dependencies],
job_timeout: job[:timeout], job_timeout: job[:timeout],
before_script: job[:before_script], before_script: job[:before_script],
......
...@@ -3547,10 +3547,13 @@ msgstr "" ...@@ -3547,10 +3547,13 @@ msgstr ""
msgid "ClusterIntegration|Choose a prefix to be used for your namespaces. Defaults to your project path." msgid "ClusterIntegration|Choose a prefix to be used for your namespaces. Defaults to your project path."
msgstr "" msgstr ""
msgid "ClusterIntegration|Choose the %{startLink}security groups%{endLink} to apply to the EKS-managed Elastic Network Interfaces that are created in your worker node subnets." msgid "ClusterIntegration|Choose the %{startLink}security group %{externalLinkIcon} %{endLink} to apply to the EKS-managed Elastic Network Interfaces that are created in your worker node subnets."
msgstr "" msgstr ""
msgid "ClusterIntegration|Choose the %{startLink}subnets%{endLink} in your VPC where your worker nodes will run." msgid "ClusterIntegration|Choose the %{startLink}subnets %{externalLinkIcon} %{endLink} in your VPC where your worker nodes will run."
msgstr ""
msgid "ClusterIntegration|Choose the worker node %{startLink}instance type %{externalLinkIcon} %{endLink}."
msgstr "" msgstr ""
msgid "ClusterIntegration|Choose which applications to install on your Kubernetes cluster. Helm Tiller is required to install any of the following applications." msgid "ClusterIntegration|Choose which applications to install on your Kubernetes cluster. Helm Tiller is required to install any of the following applications."
...@@ -3607,6 +3610,9 @@ msgstr "" ...@@ -3607,6 +3610,9 @@ msgstr ""
msgid "ClusterIntegration|Could not load VPCs for the selected region" msgid "ClusterIntegration|Could not load VPCs for the selected region"
msgstr "" msgstr ""
msgid "ClusterIntegration|Could not load instance types"
msgstr ""
msgid "ClusterIntegration|Could not load regions from your AWS account" msgid "ClusterIntegration|Could not load regions from your AWS account"
msgstr "" msgstr ""
...@@ -3634,6 +3640,9 @@ msgstr "" ...@@ -3634,6 +3640,9 @@ msgstr ""
msgid "ClusterIntegration|Create new Cluster on GKE" msgid "ClusterIntegration|Create new Cluster on GKE"
msgstr "" msgstr ""
msgid "ClusterIntegration|Creating Kubernetes cluster"
msgstr ""
msgid "ClusterIntegration|Did you know?" msgid "ClusterIntegration|Did you know?"
msgstr "" msgstr ""
...@@ -3742,6 +3751,9 @@ msgstr "" ...@@ -3742,6 +3751,9 @@ msgstr ""
msgid "ClusterIntegration|Instance cluster" msgid "ClusterIntegration|Instance cluster"
msgstr "" msgstr ""
msgid "ClusterIntegration|Instance type"
msgstr ""
msgid "ClusterIntegration|Integrate Kubernetes cluster automation" msgid "ClusterIntegration|Integrate Kubernetes cluster automation"
msgstr "" msgstr ""
...@@ -3817,7 +3829,7 @@ msgstr "" ...@@ -3817,7 +3829,7 @@ msgstr ""
msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}." msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}."
msgstr "" msgstr ""
msgid "ClusterIntegration|Learn more about %{startLink}Regions%{endLink}." msgid "ClusterIntegration|Learn more about %{startLink}Regions %{externalLinkIcon}%{endLink}."
msgstr "" msgstr ""
msgid "ClusterIntegration|Learn more about Kubernetes" msgid "ClusterIntegration|Learn more about Kubernetes"
...@@ -3844,6 +3856,9 @@ msgstr "" ...@@ -3844,6 +3856,9 @@ msgstr ""
msgid "ClusterIntegration|Loading VPCs" msgid "ClusterIntegration|Loading VPCs"
msgstr "" msgstr ""
msgid "ClusterIntegration|Loading instance types"
msgstr ""
msgid "ClusterIntegration|Loading security groups" msgid "ClusterIntegration|Loading security groups"
msgstr "" msgstr ""
...@@ -3868,6 +3883,9 @@ msgstr "" ...@@ -3868,6 +3883,9 @@ msgstr ""
msgid "ClusterIntegration|No VPCs found" msgid "ClusterIntegration|No VPCs found"
msgstr "" msgstr ""
msgid "ClusterIntegration|No instance type found"
msgstr ""
msgid "ClusterIntegration|No machine types matched your search" msgid "ClusterIntegration|No machine types matched your search"
msgstr "" msgstr ""
...@@ -3964,6 +3982,9 @@ msgstr "" ...@@ -3964,6 +3982,9 @@ msgstr ""
msgid "ClusterIntegration|Search VPCs" msgid "ClusterIntegration|Search VPCs"
msgstr "" msgstr ""
msgid "ClusterIntegration|Search instance types"
msgstr ""
msgid "ClusterIntegration|Search machine types" msgid "ClusterIntegration|Search machine types"
msgstr "" msgstr ""
...@@ -3982,7 +4003,7 @@ msgstr "" ...@@ -3982,7 +4003,7 @@ msgstr ""
msgid "ClusterIntegration|Search zones" msgid "ClusterIntegration|Search zones"
msgstr "" msgstr ""
msgid "ClusterIntegration|Security groups" msgid "ClusterIntegration|Security group"
msgstr "" msgstr ""
msgid "ClusterIntegration|See and edit the details for your Kubernetes cluster" msgid "ClusterIntegration|See and edit the details for your Kubernetes cluster"
...@@ -3994,7 +4015,10 @@ msgstr "" ...@@ -3994,7 +4015,10 @@ msgstr ""
msgid "ClusterIntegration|Select a VPC to choose a subnet" msgid "ClusterIntegration|Select a VPC to choose a subnet"
msgstr "" msgstr ""
msgid "ClusterIntegration|Select a VPC to use for your EKS Cluster resources. To use a new VPC, first create one on %{startLink}Amazon Web Services%{endLink}." msgid "ClusterIntegration|Select a VPC to use for your EKS Cluster resources. To use a new VPC, first create one on %{startLink}Amazon Web Services %{externalLinkIcon} %{endLink}."
msgstr ""
msgid "ClusterIntegration|Select a different AWS role"
msgstr "" msgstr ""
msgid "ClusterIntegration|Select a region to choose a Key Pair" msgid "ClusterIntegration|Select a region to choose a Key Pair"
...@@ -4015,10 +4039,10 @@ msgstr "" ...@@ -4015,10 +4039,10 @@ msgstr ""
msgid "ClusterIntegration|Select project to choose zone" msgid "ClusterIntegration|Select project to choose zone"
msgstr "" msgstr ""
msgid "ClusterIntegration|Select the IAM Role to allow Amazon EKS and the Kubernetes control plane to manage AWS resources on your behalf. To use a new role name, first create one on %{startLink}Amazon Web Services%{endLink}." msgid "ClusterIntegration|Select the IAM Role to allow Amazon EKS and the Kubernetes control plane to manage AWS resources on your behalf. To use a new role name, first create one on %{startLink}Amazon Web Services %{externalLinkIcon} %{endLink}."
msgstr "" msgstr ""
msgid "ClusterIntegration|Select the key pair name that will be used to create EC2 nodes. To use a new key pair name, first create one on %{startLink}Amazon Web Services%{endLink}." msgid "ClusterIntegration|Select the key pair name that will be used to create EC2 nodes. To use a new key pair name, first create one on %{startLink}Amazon Web Services %{externalLinkIcon} %{endLink}."
msgstr "" msgstr ""
msgid "ClusterIntegration|Select zone" msgid "ClusterIntegration|Select zone"
...@@ -4054,7 +4078,7 @@ msgstr "" ...@@ -4054,7 +4078,7 @@ msgstr ""
msgid "ClusterIntegration|Specifying a domain will allow you to use Auto Review Apps and Auto Deploy stages for %{auto_devops_start}Auto DevOps%{auto_devops_end}. The domain should have a wildcard DNS configured matching the domain." msgid "ClusterIntegration|Specifying a domain will allow you to use Auto Review Apps and Auto Deploy stages for %{auto_devops_start}Auto DevOps%{auto_devops_end}. The domain should have a wildcard DNS configured matching the domain."
msgstr "" msgstr ""
msgid "ClusterIntegration|Subnet" msgid "ClusterIntegration|Subnets"
msgstr "" msgstr ""
msgid "ClusterIntegration|The Amazon Resource Name (ARN) associated with your role. If you do not have a provision role, first create one on %{startAwsLink}Amazon Web Services %{externalLinkIcon}%{endLink} using the above account and external IDs. %{startMoreInfoLink}More information%{endLink}" msgid "ClusterIntegration|The Amazon Resource Name (ARN) associated with your role. If you do not have a provision role, first create one on %{startAwsLink}Amazon Web Services %{externalLinkIcon}%{endLink} using the above account and external IDs. %{startMoreInfoLink}More information%{endLink}"
...@@ -4174,6 +4198,9 @@ msgstr "" ...@@ -4174,6 +4198,9 @@ msgstr ""
msgid "ClusterIntergation|Select a subnet" msgid "ClusterIntergation|Select a subnet"
msgstr "" msgstr ""
msgid "ClusterIntergation|Select an instance type"
msgstr ""
msgid "ClusterIntergation|Select key pair" msgid "ClusterIntergation|Select key pair"
msgstr "" msgstr ""
......
...@@ -23,6 +23,7 @@ describe 'User edit profile' do ...@@ -23,6 +23,7 @@ describe 'User edit profile' do
fill_in 'user_location', with: 'Ukraine' fill_in 'user_location', with: 'Ukraine'
fill_in 'user_bio', with: 'I <3 GitLab' fill_in 'user_bio', with: 'I <3 GitLab'
fill_in 'user_organization', with: 'GitLab' fill_in 'user_organization', with: 'GitLab'
select 'Data Analyst', from: 'user_role'
submit_settings submit_settings
expect(user.reload).to have_attributes( expect(user.reload).to have_attributes(
...@@ -31,7 +32,8 @@ describe 'User edit profile' do ...@@ -31,7 +32,8 @@ describe 'User edit profile' do
twitter: 'testtwitter', twitter: 'testtwitter',
website_url: 'testurl', website_url: 'testurl',
bio: 'I <3 GitLab', bio: 'I <3 GitLab',
organization: 'GitLab' organization: 'GitLab',
role: 'data_analyst'
) )
expect(find('#user_location').value).to eq 'Ukraine' expect(find('#user_location').value).to eq 'Ukraine'
...@@ -66,34 +68,6 @@ describe 'User edit profile' do ...@@ -66,34 +68,6 @@ describe 'User edit profile' do
end end
end end
describe 'when I change my role' do
context 'experiment enabled' do
before do
stub_experiment_for_user(signup_flow: true)
visit(profile_path)
end
it 'changes my role' do
expect(page).to have_content 'Role'
select 'Data Analyst', from: 'user_role'
submit_settings
user.reload
expect(user.role).to eq 'data_analyst'
end
end
context 'experiment disabled' do
before do
stub_experiment_for_user(signup_flow: false)
visit(profile_path)
end
it 'does not show the role picker' do
expect(page).not_to have_content 'Role'
end
end
end
context 'user avatar' do context 'user avatar' do
before do before do
attach_file(:user_avatar, Rails.root.join('spec', 'fixtures', 'banana_sample.gif')) attach_file(:user_avatar, Rails.root.join('spec', 'fixtures', 'banana_sample.gif'))
......
...@@ -3,7 +3,7 @@ import { shallowMount } from '@vue/test-utils'; ...@@ -3,7 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import ClusterFormDropdown from '~/create_cluster/eks_cluster/components/cluster_form_dropdown.vue'; import ClusterFormDropdown from '~/create_cluster/eks_cluster/components/cluster_form_dropdown.vue';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue'; import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue'; import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue'; import { GlIcon } from '@gitlab/ui';
describe('ClusterFormDropdown', () => { describe('ClusterFormDropdown', () => {
let vm; let vm;
...@@ -41,24 +41,50 @@ describe('ClusterFormDropdown', () => { ...@@ -41,24 +41,50 @@ describe('ClusterFormDropdown', () => {
.trigger('click'); .trigger('click');
}); });
it('displays selected item label', () => { it('emits input event with selected item', () => {
expect(vm.find(DropdownButton).props('toggleText')).toEqual(secondItem.name); expect(vm.emitted('input')[0]).toEqual([secondItem.value]);
});
});
describe('when multiple items are selected', () => {
const value = [1];
beforeEach(() => {
vm.setProps({ items, multiple: true, value });
vm.findAll('.js-dropdown-item')
.at(0)
.trigger('click');
vm.findAll('.js-dropdown-item')
.at(1)
.trigger('click');
});
it('emits input event with an array of selected items', () => {
expect(vm.emitted('input')[1]).toEqual([[firstItem.value, secondItem.value]]);
});
});
describe('when multiple items can be selected', () => {
beforeEach(() => {
vm.setProps({ items, multiple: true, value: firstItem.value });
}); });
it('sets selected value to dropdown hidden input', () => { it('displays a checked GlIcon next to the item', () => {
expect(vm.find(DropdownHiddenInput).props('value')).toEqual(secondItem.value); expect(vm.find(GlIcon).is('.invisible')).toBe(false);
expect(vm.find(GlIcon).props('name')).toBe('mobile-issue-close');
}); });
}); });
describe('when an item is selected and has a custom label property', () => { describe('when an item is selected and has a custom label property', () => {
it('displays selected item custom label', () => { it('displays selected item custom label', () => {
const labelProperty = 'customLabel'; const labelProperty = 'customLabel';
const selectedItem = { [labelProperty]: 'Name' }; const label = 'Name';
const currentValue = 1;
const customLabelItems = [{ [labelProperty]: label, value: currentValue }];
vm.setProps({ labelProperty }); vm.setProps({ labelProperty, items: customLabelItems, value: currentValue });
vm.setData({ selectedItem });
expect(vm.find(DropdownButton).props('toggleText')).toEqual(selectedItem[labelProperty]); expect(vm.find(DropdownButton).props('toggleText')).toEqual(label);
}); });
}); });
......
...@@ -4,7 +4,6 @@ import Vue from 'vue'; ...@@ -4,7 +4,6 @@ import Vue from 'vue';
import { GlFormCheckbox } from '@gitlab/ui'; import { GlFormCheckbox } from '@gitlab/ui';
import EksClusterConfigurationForm from '~/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue'; import EksClusterConfigurationForm from '~/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue';
import RegionDropdown from '~/create_cluster/eks_cluster/components/region_dropdown.vue';
import eksClusterFormState from '~/create_cluster/eks_cluster/store/state'; import eksClusterFormState from '~/create_cluster/eks_cluster/store/state';
import clusterDropdownStoreState from '~/create_cluster/eks_cluster/store/cluster_dropdown/state'; import clusterDropdownStoreState from '~/create_cluster/eks_cluster/store/cluster_dropdown/state';
...@@ -21,17 +20,21 @@ describe('EksClusterConfigurationForm', () => { ...@@ -21,17 +20,21 @@ describe('EksClusterConfigurationForm', () => {
let subnetsState; let subnetsState;
let keyPairsState; let keyPairsState;
let securityGroupsState; let securityGroupsState;
let instanceTypesState;
let vpcsActions; let vpcsActions;
let rolesActions; let rolesActions;
let regionsActions; let regionsActions;
let subnetsActions; let subnetsActions;
let keyPairsActions; let keyPairsActions;
let securityGroupsActions; let securityGroupsActions;
let instanceTypesActions;
let vm; let vm;
beforeEach(() => { beforeEach(() => {
state = eksClusterFormState(); state = eksClusterFormState();
actions = { actions = {
signOut: jest.fn(),
createCluster: jest.fn(),
setClusterName: jest.fn(), setClusterName: jest.fn(),
setEnvironmentScope: jest.fn(), setEnvironmentScope: jest.fn(),
setKubernetesVersion: jest.fn(), setKubernetesVersion: jest.fn(),
...@@ -41,6 +44,8 @@ describe('EksClusterConfigurationForm', () => { ...@@ -41,6 +44,8 @@ describe('EksClusterConfigurationForm', () => {
setRole: jest.fn(), setRole: jest.fn(),
setKeyPair: jest.fn(), setKeyPair: jest.fn(),
setSecurityGroup: jest.fn(), setSecurityGroup: jest.fn(),
setInstanceType: jest.fn(),
setNodeCount: jest.fn(),
setGitlabManagedCluster: jest.fn(), setGitlabManagedCluster: jest.fn(),
}; };
regionsActions = { regionsActions = {
...@@ -61,6 +66,9 @@ describe('EksClusterConfigurationForm', () => { ...@@ -61,6 +66,9 @@ describe('EksClusterConfigurationForm', () => {
securityGroupsActions = { securityGroupsActions = {
fetchItems: jest.fn(), fetchItems: jest.fn(),
}; };
instanceTypesActions = {
fetchItems: jest.fn(),
};
rolesState = { rolesState = {
...clusterDropdownStoreState(), ...clusterDropdownStoreState(),
}; };
...@@ -79,6 +87,9 @@ describe('EksClusterConfigurationForm', () => { ...@@ -79,6 +87,9 @@ describe('EksClusterConfigurationForm', () => {
securityGroupsState = { securityGroupsState = {
...clusterDropdownStoreState(), ...clusterDropdownStoreState(),
}; };
instanceTypesState = {
...clusterDropdownStoreState(),
};
store = new Vuex.Store({ store = new Vuex.Store({
state, state,
actions, actions,
...@@ -113,6 +124,11 @@ describe('EksClusterConfigurationForm', () => { ...@@ -113,6 +124,11 @@ describe('EksClusterConfigurationForm', () => {
state: securityGroupsState, state: securityGroupsState,
actions: securityGroupsActions, actions: securityGroupsActions,
}, },
instanceTypes: {
namespaced: true,
state: instanceTypesState,
actions: instanceTypesActions,
},
}, },
}); });
}); });
...@@ -124,6 +140,7 @@ describe('EksClusterConfigurationForm', () => { ...@@ -124,6 +140,7 @@ describe('EksClusterConfigurationForm', () => {
propsData: { propsData: {
gitlabManagedClusterHelpPath: '', gitlabManagedClusterHelpPath: '',
kubernetesIntegrationHelpPath: '', kubernetesIntegrationHelpPath: '',
externalLinkIcon: '',
}, },
}); });
}); });
...@@ -132,15 +149,34 @@ describe('EksClusterConfigurationForm', () => { ...@@ -132,15 +149,34 @@ describe('EksClusterConfigurationForm', () => {
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 findSignOutButton = () => vm.find('.js-sign-out');
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]');
const findKubernetesVersionDropdown = () => vm.find('[field-id="eks-kubernetes-version"]'); const findKubernetesVersionDropdown = () => vm.find('[field-id="eks-kubernetes-version"]');
const findRegionDropdown = () => vm.find(RegionDropdown); const findRegionDropdown = () => vm.find('[field-id="eks-region"]');
const findKeyPairDropdown = () => vm.find('[field-id="eks-key-pair"]'); const findKeyPairDropdown = () => vm.find('[field-id="eks-key-pair"]');
const findVpcDropdown = () => vm.find('[field-id="eks-vpc"]'); const findVpcDropdown = () => vm.find('[field-id="eks-vpc"]');
const findSubnetDropdown = () => vm.find('[field-id="eks-subnet"]'); const findSubnetDropdown = () => vm.find('[field-id="eks-subnet"]');
const findRoleDropdown = () => vm.find('[field-id="eks-role"]'); const findRoleDropdown = () => vm.find('[field-id="eks-role"]');
const findSecurityGroupDropdown = () => vm.find('[field-id="eks-security-group"]'); const findSecurityGroupDropdown = () => vm.find('[field-id="eks-security-group"]');
const findInstanceTypeDropdown = () => vm.find('[field-id="eks-instance-type"');
const findNodeCountInput = () => vm.find('[id="eks-node-count"]');
const findGitlabManagedClusterCheckbox = () => vm.find(GlFormCheckbox); const findGitlabManagedClusterCheckbox = () => vm.find(GlFormCheckbox);
describe('when mounted', () => { describe('when mounted', () => {
...@@ -151,6 +187,15 @@ describe('EksClusterConfigurationForm', () => { ...@@ -151,6 +187,15 @@ describe('EksClusterConfigurationForm', () => {
it('fetches available roles', () => { it('fetches available roles', () => {
expect(rolesActions.fetchItems).toHaveBeenCalled(); expect(rolesActions.fetchItems).toHaveBeenCalled();
}); });
it('fetches available instance types', () => {
expect(instanceTypesActions.fetchItems).toHaveBeenCalled();
});
});
it('dispatches signOut action when sign out button is clicked', () => {
findSignOutButton().trigger('click');
expect(actions.signOut).toHaveBeenCalled();
}); });
it('sets isLoadingRoles to RoleDropdown loading property', () => { it('sets isLoadingRoles to RoleDropdown loading property', () => {
...@@ -180,11 +225,13 @@ describe('EksClusterConfigurationForm', () => { ...@@ -180,11 +225,13 @@ describe('EksClusterConfigurationForm', () => {
}); });
it('sets regions to RegionDropdown regions property', () => { it('sets regions to RegionDropdown regions property', () => {
expect(findRegionDropdown().props('regions')).toBe(regionsState.items); expect(findRegionDropdown().props('items')).toBe(regionsState.items);
}); });
it('sets loadingRegionsError to RegionDropdown error property', () => { it('sets loadingRegionsError to RegionDropdown error property', () => {
expect(findRegionDropdown().props('error')).toBe(regionsState.loadingItemsError); regionsState.loadingItemsError = new Error();
expect(findRegionDropdown().props('hasErrors')).toEqual(true);
}); });
it('disables KeyPairDropdown when no region is selected', () => { it('disables KeyPairDropdown when no region is selected', () => {
...@@ -329,6 +376,34 @@ describe('EksClusterConfigurationForm', () => { ...@@ -329,6 +376,34 @@ describe('EksClusterConfigurationForm', () => {
undefined, undefined,
); );
}); });
it('cleans selected vpc', () => {
expect(actions.setVpc).toHaveBeenCalledWith(expect.anything(), { vpc: null }, undefined);
});
it('cleans selected key pair', () => {
expect(actions.setKeyPair).toHaveBeenCalledWith(
expect.anything(),
{ keyPair: null },
undefined,
);
});
it('cleans selected subnet', () => {
expect(actions.setSubnet).toHaveBeenCalledWith(
expect.anything(),
{ subnet: null },
undefined,
);
});
it('cleans selected security group', () => {
expect(actions.setSecurityGroup).toHaveBeenCalledWith(
expect.anything(),
{ securityGroup: null },
undefined,
);
});
}); });
it('dispatches setClusterName when cluster name input changes', () => { it('dispatches setClusterName when cluster name input changes', () => {
...@@ -381,8 +456,10 @@ describe('EksClusterConfigurationForm', () => { ...@@ -381,8 +456,10 @@ describe('EksClusterConfigurationForm', () => {
describe('when vpc is selected', () => { describe('when vpc is selected', () => {
const vpc = { name: 'vpc-1' }; const vpc = { name: 'vpc-1' };
const region = 'east-1';
beforeEach(() => { beforeEach(() => {
state.selectedRegion = region;
findVpcDropdown().vm.$emit('input', vpc); findVpcDropdown().vm.$emit('input', vpc);
}); });
...@@ -390,14 +467,34 @@ describe('EksClusterConfigurationForm', () => { ...@@ -390,14 +467,34 @@ describe('EksClusterConfigurationForm', () => {
expect(actions.setVpc).toHaveBeenCalledWith(expect.anything(), { vpc }, undefined); expect(actions.setVpc).toHaveBeenCalledWith(expect.anything(), { vpc }, undefined);
}); });
it('cleans selected subnet', () => {
expect(actions.setSubnet).toHaveBeenCalledWith(
expect.anything(),
{ subnet: null },
undefined,
);
});
it('cleans selected security group', () => {
expect(actions.setSecurityGroup).toHaveBeenCalledWith(
expect.anything(),
{ securityGroup: null },
undefined,
);
});
it('dispatches fetchSubnets action', () => { it('dispatches fetchSubnets action', () => {
expect(subnetsActions.fetchItems).toHaveBeenCalledWith(expect.anything(), { vpc }, undefined); expect(subnetsActions.fetchItems).toHaveBeenCalledWith(
expect.anything(),
{ vpc, region },
undefined,
);
}); });
it('dispatches fetchSecurityGroups action', () => { it('dispatches fetchSecurityGroups action', () => {
expect(securityGroupsActions.fetchItems).toHaveBeenCalledWith( expect(securityGroupsActions.fetchItems).toHaveBeenCalledWith(
expect.anything(), expect.anything(),
{ vpc }, { vpc, region },
undefined, undefined,
); );
}); });
...@@ -454,4 +551,76 @@ describe('EksClusterConfigurationForm', () => { ...@@ -454,4 +551,76 @@ describe('EksClusterConfigurationForm', () => {
); );
}); });
}); });
describe('when instance type is selected', () => {
const instanceType = 'small-1';
beforeEach(() => {
findInstanceTypeDropdown().vm.$emit('input', instanceType);
});
it('dispatches setInstanceType action', () => {
expect(actions.setInstanceType).toHaveBeenCalledWith(
expect.anything(),
{ instanceType },
undefined,
);
});
});
it('dispatches setNodeCount when node count input changes', () => {
const nodeCount = 5;
findNodeCountInput().vm.$emit('input', nodeCount);
expect(actions.setNodeCount).toHaveBeenCalledWith(expect.anything(), { nodeCount }, undefined);
});
describe('when all cluster configuration fields are set', () => {
beforeEach(() => {
setAllConfigurationFields();
});
it('enables create cluster button', () => {
expect(findCreateClusterButton().props('disabled')).toBe(false);
});
});
describe('when at least one cluster configuration field is not set', () => {
beforeEach(() => {
setAllConfigurationFields();
store.replaceState({
...state,
clusterName: '',
});
});
it('disables create cluster button', () => {
expect(findCreateClusterButton().props('disabled')).toBe(true);
});
});
describe('when isCreatingCluster', () => {
beforeEach(() => {
setAllConfigurationFields();
store.replaceState({
...state,
isCreatingCluster: true,
});
});
it('sets create cluster button as loading', () => {
expect(findCreateClusterButton().props('loading')).toBe(true);
});
});
describe('clicking create cluster button', () => {
beforeEach(() => {
findCreateClusterButton().vm.$emit('click');
});
it('dispatches createCluster action', () => {
expect(actions.createCluster).toHaveBeenCalled();
});
});
}); });
import { shallowMount } from '@vue/test-utils';
import ClusterFormDropdown from '~/create_cluster/eks_cluster/components/cluster_form_dropdown.vue';
import RegionDropdown from '~/create_cluster/eks_cluster/components/region_dropdown.vue';
describe('RegionDropdown', () => {
let vm;
const getClusterFormDropdown = () => vm.find(ClusterFormDropdown);
beforeEach(() => {
vm = shallowMount(RegionDropdown);
});
afterEach(() => vm.destroy());
it('renders a cluster-form-dropdown', () => {
expect(getClusterFormDropdown().exists()).toBe(true);
});
it('sets regions to cluster-form-dropdown items property', () => {
const regions = [{ name: 'basic' }];
vm.setProps({ regions });
expect(getClusterFormDropdown().props('items')).toEqual(regions);
});
it('sets a loading text', () => {
expect(getClusterFormDropdown().props('loadingText')).toEqual('Loading Regions');
});
it('sets a placeholder', () => {
expect(getClusterFormDropdown().props('placeholder')).toEqual('Select a region');
});
it('sets an empty results text', () => {
expect(getClusterFormDropdown().props('emptyText')).toEqual('No region found');
});
it('sets a search field placeholder', () => {
expect(getClusterFormDropdown().props('searchFieldPlaceholder')).toEqual('Search regions');
});
it('sets hasErrors property', () => {
vm.setProps({ error: {} });
expect(getClusterFormDropdown().props('hasErrors')).toEqual(true);
});
it('sets an error message', () => {
expect(getClusterFormDropdown().props('errorMessage')).toEqual(
'Could not load regions from your AWS account',
);
});
});
import awsServicesFacadeFactory from '~/create_cluster/eks_cluster/services/aws_services_facade';
import axios from '~/lib/utils/axios_utils';
import AxiosMockAdapter from 'axios-mock-adapter';
describe('awsServicesFacade', () => {
let apiPaths;
let axiosMock;
let awsServices;
let region;
let vpc;
beforeEach(() => {
apiPaths = {
getKeyPairsPath: '/clusters/aws/api/key_pairs',
getRegionsPath: '/clusters/aws/api/regions',
getRolesPath: '/clusters/aws/api/roles',
getSecurityGroupsPath: '/clusters/aws/api/security_groups',
getSubnetsPath: '/clusters/aws/api/subnets',
getVpcsPath: '/clusters/aws/api/vpcs',
getInstanceTypesPath: '/clusters/aws/api/instance_types',
};
region = 'west-1';
vpc = 'vpc-2';
awsServices = awsServicesFacadeFactory(apiPaths);
axiosMock = new AxiosMockAdapter(axios);
});
describe('when fetchRegions succeeds', () => {
let regions;
let regionsOutput;
beforeEach(() => {
regions = [{ region_name: 'east-1' }, { region_name: 'west-2' }];
regionsOutput = regions.map(({ region_name: name }) => ({ name, value: name }));
axiosMock.onGet(apiPaths.getRegionsPath).reply(200, { regions });
});
it('return list of roles where each item has a name and value', () => {
expect(awsServices.fetchRegions()).resolves.toEqual(regionsOutput);
});
});
describe('when fetchRoles succeeds', () => {
let roles;
let rolesOutput;
beforeEach(() => {
roles = [
{ role_name: 'admin', arn: 'aws::admin' },
{ role_name: 'read-only', arn: 'aws::read-only' },
];
rolesOutput = roles.map(({ role_name: name, arn: value }) => ({ name, value }));
axiosMock.onGet(apiPaths.getRolesPath).reply(200, { roles });
});
it('return list of regions where each item has a name and value', () => {
expect(awsServices.fetchRoles()).resolves.toEqual(rolesOutput);
});
});
describe('when fetchKeyPairs succeeds', () => {
let keyPairs;
let keyPairsOutput;
beforeEach(() => {
keyPairs = [{ key_pair: 'key-pair' }, { key_pair: 'key-pair-2' }];
keyPairsOutput = keyPairs.map(({ key_name: name }) => ({ name, value: name }));
axiosMock
.onGet(apiPaths.getKeyPairsPath, { params: { region } })
.reply(200, { key_pairs: keyPairs });
});
it('return list of key pairs where each item has a name and value', () => {
expect(awsServices.fetchKeyPairs({ region })).resolves.toEqual(keyPairsOutput);
});
});
describe('when fetchVpcs succeeds', () => {
let vpcs;
let vpcsOutput;
beforeEach(() => {
vpcs = [{ vpc_id: 'vpc-1' }, { vpc_id: 'vpc-2' }];
vpcsOutput = vpcs.map(({ vpc_id: name }) => ({ name, value: name }));
axiosMock.onGet(apiPaths.getVpcsPath, { params: { region } }).reply(200, { vpcs });
});
it('return list of vpcs where each item has a name and value', () => {
expect(awsServices.fetchVpcs({ region })).resolves.toEqual(vpcsOutput);
});
});
describe('when fetchSubnets succeeds', () => {
let subnets;
let subnetsOutput;
beforeEach(() => {
subnets = [{ subnet_id: 'vpc-1' }, { subnet_id: 'vpc-2' }];
subnetsOutput = subnets.map(({ subnet_id }) => ({ name: subnet_id, value: subnet_id }));
axiosMock
.onGet(apiPaths.getSubnetsPath, { params: { region, vpc_id: vpc } })
.reply(200, { subnets });
});
it('return list of subnets where each item has a name and value', () => {
expect(awsServices.fetchSubnets({ region, vpc })).resolves.toEqual(subnetsOutput);
});
});
describe('when fetchSecurityGroups succeeds', () => {
let securityGroups;
let securityGroupsOutput;
beforeEach(() => {
securityGroups = [
{ group_name: 'admin group', group_id: 'group-1' },
{ group_name: 'basic group', group_id: 'group-2' },
];
securityGroupsOutput = securityGroups.map(({ group_id: value, group_name: name }) => ({
name,
value,
}));
axiosMock
.onGet(apiPaths.getSecurityGroupsPath, { params: { region, vpc_id: vpc } })
.reply(200, { security_groups: securityGroups });
});
it('return list of security groups where each item has a name and value', () => {
expect(awsServices.fetchSecurityGroups({ region, vpc })).resolves.toEqual(
securityGroupsOutput,
);
});
});
describe('when fetchInstanceTypes succeeds', () => {
let instanceTypes;
let instanceTypesOutput;
beforeEach(() => {
instanceTypes = [{ instance_type_name: 't2.small' }, { instance_type_name: 't2.medium' }];
instanceTypesOutput = instanceTypes.map(({ instance_type_name }) => ({
name: instance_type_name,
value: instance_type_name,
}));
axiosMock.onGet(apiPaths.getInstanceTypesPath).reply(200, { instance_types: instanceTypes });
});
it('return list of instance types where each item has a name and value', () => {
expect(awsServices.fetchInstanceTypes()).resolves.toEqual(instanceTypesOutput);
});
});
});
...@@ -13,12 +13,20 @@ import { ...@@ -13,12 +13,20 @@ import {
SET_ROLE, SET_ROLE,
SET_SECURITY_GROUP, SET_SECURITY_GROUP,
SET_GITLAB_MANAGED_CLUSTER, SET_GITLAB_MANAGED_CLUSTER,
SET_INSTANCE_TYPE,
SET_NODE_COUNT,
REQUEST_CREATE_ROLE, REQUEST_CREATE_ROLE,
CREATE_ROLE_SUCCESS, CREATE_ROLE_SUCCESS,
CREATE_ROLE_ERROR, CREATE_ROLE_ERROR,
REQUEST_CREATE_CLUSTER,
CREATE_CLUSTER_ERROR,
SIGN_OUT,
} from '~/create_cluster/eks_cluster/store/mutation_types'; } from '~/create_cluster/eks_cluster/store/mutation_types';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import createFlash from '~/flash';
jest.mock('~/flash');
describe('EKS Cluster Store Actions', () => { describe('EKS Cluster Store Actions', () => {
let clusterName; let clusterName;
...@@ -30,25 +38,34 @@ describe('EKS Cluster Store Actions', () => { ...@@ -30,25 +38,34 @@ describe('EKS Cluster Store Actions', () => {
let role; let role;
let keyPair; let keyPair;
let securityGroup; let securityGroup;
let instanceType;
let nodeCount;
let gitlabManagedCluster; let gitlabManagedCluster;
let mock; let mock;
let state; let state;
let newClusterUrl;
beforeEach(() => { beforeEach(() => {
clusterName = 'my cluster'; clusterName = 'my cluster';
environmentScope = 'production'; environmentScope = 'production';
kubernetesVersion = '11.1'; kubernetesVersion = '11.1';
region = { name: 'regions-1' }; region = 'regions-1';
vpc = { name: 'vpc-1' }; vpc = 'vpc-1';
subnet = { name: 'subnet-1' }; subnet = 'subnet-1';
role = { name: 'role-1' }; role = 'role-1';
keyPair = { name: 'key-pair-1' }; keyPair = 'key-pair-1';
securityGroup = { name: 'default group' }; securityGroup = 'default group';
instanceType = 'small-1';
nodeCount = '5';
gitlabManagedCluster = true; gitlabManagedCluster = true;
newClusterUrl = '/clusters/1';
state = { state = {
...createState(), ...createState(),
createRolePath: '/clusters/roles/', createRolePath: '/clusters/roles/',
signOutPath: '/aws/signout',
createClusterPath: '/clusters/',
}; };
}); });
...@@ -71,6 +88,8 @@ describe('EKS Cluster Store Actions', () => { ...@@ -71,6 +88,8 @@ describe('EKS Cluster Store Actions', () => {
${'setVpc'} | ${SET_VPC} | ${{ vpc }} | ${'vpc'} ${'setVpc'} | ${SET_VPC} | ${{ vpc }} | ${'vpc'}
${'setSubnet'} | ${SET_SUBNET} | ${{ subnet }} | ${'subnet'} ${'setSubnet'} | ${SET_SUBNET} | ${{ subnet }} | ${'subnet'}
${'setSecurityGroup'} | ${SET_SECURITY_GROUP} | ${{ securityGroup }} | ${'securityGroup'} ${'setSecurityGroup'} | ${SET_SECURITY_GROUP} | ${{ securityGroup }} | ${'securityGroup'}
${'setInstanceType'} | ${SET_INSTANCE_TYPE} | ${{ instanceType }} | ${'instance type'}
${'setNodeCount'} | ${SET_NODE_COUNT} | ${{ nodeCount }} | ${'node count'}
${'setGitlabManagedCluster'} | ${SET_GITLAB_MANAGED_CLUSTER} | ${gitlabManagedCluster} | ${'gitlab managed cluster'} ${'setGitlabManagedCluster'} | ${SET_GITLAB_MANAGED_CLUSTER} | ${gitlabManagedCluster} | ${'gitlab managed cluster'}
`(`$action commits $mutation with $payloadDescription payload`, data => { `(`$action commits $mutation with $payloadDescription payload`, data => {
const { action, mutation, payload } = data; const { action, mutation, payload } = data;
...@@ -149,4 +168,127 @@ describe('EKS Cluster Store Actions', () => { ...@@ -149,4 +168,127 @@ describe('EKS Cluster Store Actions', () => {
testAction(actions.createRoleError, payload, state, [{ type: CREATE_ROLE_ERROR, payload }]); testAction(actions.createRoleError, payload, state, [{ type: CREATE_ROLE_ERROR, payload }]);
}); });
}); });
describe('createCluster', () => {
let requestPayload;
beforeEach(() => {
requestPayload = {
name: clusterName,
environment_scope: environmentScope,
managed: gitlabManagedCluster,
provider_aws_attributes: {
region,
vpc_id: vpc,
subnet_ids: subnet,
role_arn: role,
key_name: keyPair,
security_group_id: securityGroup,
instance_type: instanceType,
num_nodes: nodeCount,
},
};
state = Object.assign(createState(), {
clusterName,
environmentScope,
kubernetesVersion,
selectedRegion: region,
selectedVpc: vpc,
selectedSubnet: subnet,
selectedRole: role,
selectedKeyPair: keyPair,
selectedSecurityGroup: securityGroup,
selectedInstanceType: instanceType,
nodeCount,
gitlabManagedCluster,
});
});
describe('when request succeeds', () => {
beforeEach(() => {
mock.onPost(state.createClusterPath, requestPayload).reply(201, null, {
location: '/clusters/1',
});
});
it('dispatches createClusterSuccess action', () =>
testAction(
actions.createCluster,
null,
state,
[],
[
{ type: 'requestCreateCluster' },
{ type: 'createClusterSuccess', payload: newClusterUrl },
],
));
});
describe('when request fails', () => {
let response;
beforeEach(() => {
response = 'Request failed with status code 400';
mock.onPost(state.createClusterPath, requestPayload).reply(400, response);
});
it('dispatches createRoleError action', () =>
testAction(
actions.createCluster,
null,
state,
[],
[{ type: 'requestCreateCluster' }, { type: 'createClusterError', payload: response }],
));
});
});
describe('requestCreateCluster', () => {
it('commits requestCreateCluster mutation', () => {
testAction(actions.requestCreateCluster, null, state, [{ type: REQUEST_CREATE_CLUSTER }]);
});
});
describe('createClusterSuccess', () => {
beforeEach(() => {
jest.spyOn(window.location, 'assign').mockImplementation(() => {});
});
afterEach(() => {
window.location.assign.mockRestore();
});
it('redirects to the new cluster URL', () => {
actions.createClusterSuccess(null, newClusterUrl);
expect(window.location.assign).toHaveBeenCalledWith(newClusterUrl);
});
});
describe('createClusterError', () => {
let payload;
beforeEach(() => {
payload = { name: ['Create cluster failed'] };
});
it('commits createClusterError mutation', () => {
testAction(actions.createClusterError, payload, state, [
{ type: CREATE_CLUSTER_ERROR, payload },
]);
});
it('creates a flash that displays the create cluster error', () => {
expect(createFlash).toHaveBeenCalledWith(payload.name[0]);
});
});
describe('signOut', () => {
beforeEach(() => {
mock.onDelete(state.signOutPath).reply(200, null);
});
it('commits signOut mutation', () => {
testAction(actions.signOut, null, state, [{ type: SIGN_OUT }]);
});
});
}); });
...@@ -8,10 +8,15 @@ import { ...@@ -8,10 +8,15 @@ import {
SET_SUBNET, SET_SUBNET,
SET_ROLE, SET_ROLE,
SET_SECURITY_GROUP, SET_SECURITY_GROUP,
SET_INSTANCE_TYPE,
SET_NODE_COUNT,
SET_GITLAB_MANAGED_CLUSTER, SET_GITLAB_MANAGED_CLUSTER,
REQUEST_CREATE_ROLE, REQUEST_CREATE_ROLE,
CREATE_ROLE_SUCCESS, CREATE_ROLE_SUCCESS,
CREATE_ROLE_ERROR, CREATE_ROLE_ERROR,
REQUEST_CREATE_CLUSTER,
CREATE_CLUSTER_ERROR,
SIGN_OUT,
} from '~/create_cluster/eks_cluster/store/mutation_types'; } from '~/create_cluster/eks_cluster/store/mutation_types';
import createState from '~/create_cluster/eks_cluster/store/state'; import createState from '~/create_cluster/eks_cluster/store/state';
import mutations from '~/create_cluster/eks_cluster/store/mutations'; import mutations from '~/create_cluster/eks_cluster/store/mutations';
...@@ -27,6 +32,8 @@ describe('Create EKS cluster store mutations', () => { ...@@ -27,6 +32,8 @@ describe('Create EKS cluster store mutations', () => {
let role; let role;
let keyPair; let keyPair;
let securityGroup; let securityGroup;
let instanceType;
let nodeCount;
let gitlabManagedCluster; let gitlabManagedCluster;
beforeEach(() => { beforeEach(() => {
...@@ -39,6 +46,8 @@ describe('Create EKS cluster store mutations', () => { ...@@ -39,6 +46,8 @@ describe('Create EKS cluster store mutations', () => {
role = { name: 'role-1' }; role = { name: 'role-1' };
keyPair = { name: 'key pair' }; keyPair = { name: 'key pair' };
securityGroup = { name: 'default group' }; securityGroup = { name: 'default group' };
instanceType = 'small-1';
nodeCount = '5';
gitlabManagedCluster = false; gitlabManagedCluster = false;
state = createState(); state = createState();
...@@ -53,8 +62,10 @@ describe('Create EKS cluster store mutations', () => { ...@@ -53,8 +62,10 @@ describe('Create EKS cluster store mutations', () => {
${SET_REGION} | ${'selectedRegion'} | ${{ region }} | ${region} | ${'selected region payload'} ${SET_REGION} | ${'selectedRegion'} | ${{ region }} | ${region} | ${'selected region payload'}
${SET_KEY_PAIR} | ${'selectedKeyPair'} | ${{ keyPair }} | ${keyPair} | ${'selected key pair payload'} ${SET_KEY_PAIR} | ${'selectedKeyPair'} | ${{ keyPair }} | ${keyPair} | ${'selected key pair payload'}
${SET_VPC} | ${'selectedVpc'} | ${{ vpc }} | ${vpc} | ${'selected vpc payload'} ${SET_VPC} | ${'selectedVpc'} | ${{ vpc }} | ${vpc} | ${'selected vpc payload'}
${SET_SUBNET} | ${'selectedSubnet'} | ${{ subnet }} | ${subnet} | ${'selected sybnet payload'} ${SET_SUBNET} | ${'selectedSubnet'} | ${{ subnet }} | ${subnet} | ${'selected subnet payload'}
${SET_SECURITY_GROUP} | ${'selectedSecurityGroup'} | ${{ securityGroup }} | ${securityGroup} | ${'selected security group payload'} ${SET_SECURITY_GROUP} | ${'selectedSecurityGroup'} | ${{ securityGroup }} | ${securityGroup} | ${'selected security group payload'}
${SET_INSTANCE_TYPE} | ${'selectedInstanceType'} | ${{ instanceType }} | ${instanceType} | ${'selected instance type payload'}
${SET_NODE_COUNT} | ${'nodeCount'} | ${{ nodeCount }} | ${nodeCount} | ${'node count payload'}
${SET_GITLAB_MANAGED_CLUSTER} | ${'gitlabManagedCluster'} | ${{ gitlabManagedCluster }} | ${gitlabManagedCluster} | ${'gitlab managed cluster'} ${SET_GITLAB_MANAGED_CLUSTER} | ${'gitlabManagedCluster'} | ${{ gitlabManagedCluster }} | ${gitlabManagedCluster} | ${'gitlab managed cluster'}
`(`$mutation sets $mutatedProperty to $expectedValueDescription`, data => { `(`$mutation sets $mutatedProperty to $expectedValueDescription`, data => {
const { mutation, mutatedProperty, payload, expectedValue } = data; const { mutation, mutatedProperty, payload, expectedValue } = data;
...@@ -118,4 +129,45 @@ describe('Create EKS cluster store mutations', () => { ...@@ -118,4 +129,45 @@ describe('Create EKS cluster store mutations', () => {
expect(state.hasCredentials).toBe(false); expect(state.hasCredentials).toBe(false);
}); });
}); });
describe(`mutation ${REQUEST_CREATE_CLUSTER}`, () => {
beforeEach(() => {
mutations[REQUEST_CREATE_CLUSTER](state);
});
it('sets isCreatingCluster to true', () => {
expect(state.isCreatingCluster).toBe(true);
});
it('sets createClusterError to null', () => {
expect(state.createClusterError).toBe(null);
});
});
describe(`mutation ${CREATE_CLUSTER_ERROR}`, () => {
const error = new Error();
beforeEach(() => {
mutations[CREATE_CLUSTER_ERROR](state, { error });
});
it('sets isCreatingRole to false', () => {
expect(state.isCreatingCluster).toBe(false);
});
it('sets createRoleError to the error object', () => {
expect(state.createClusterError).toBe(error);
});
});
describe(`mutation ${SIGN_OUT}`, () => {
beforeEach(() => {
state.hasCredentials = true;
mutations[SIGN_OUT](state);
});
it('sets hasCredentials to false', () => {
expect(state.hasCredentials).toBe(false);
});
});
}); });
...@@ -12,22 +12,53 @@ describe Gitlab::Ci::Config::Entry::Cache do ...@@ -12,22 +12,53 @@ describe Gitlab::Ci::Config::Entry::Cache do
context 'when entry config value is correct' do context 'when entry config value is correct' do
let(:policy) { nil } let(:policy) { nil }
let(:key) { 'some key' }
let(:config) do let(:config) do
{ key: 'some key', { key: key,
untracked: true, untracked: true,
paths: ['some/path/'], paths: ['some/path/'],
policy: policy } policy: policy }
end end
describe '#value' do describe '#value' do
it 'returns hash value' do shared_examples 'hash key value' do
expect(entry.value).to eq(key: 'some key', untracked: true, paths: ['some/path/'], policy: 'pull-push') it 'returns hash value' do
expect(entry.value).to eq(key: key, untracked: true, paths: ['some/path/'], policy: 'pull-push')
end
end
it_behaves_like 'hash key value'
context 'with files' do
let(:key) { { files: ['a-file', 'other-file'] } }
it_behaves_like 'hash key value'
end
context 'with files and prefix' do
let(:key) { { files: ['a-file', 'other-file'], prefix: 'prefix-value' } }
it_behaves_like 'hash key value'
end
context 'with prefix' do
let(:key) { { prefix: 'prefix-value' } }
it 'key is nil' do
expect(entry.value).to match(a_hash_including(key: nil))
end
end end
end end
describe '#valid?' do describe '#valid?' do
it { is_expected.to be_valid } it { is_expected.to be_valid }
context 'with files' do
let(:key) { { files: ['a-file', 'other-file'] } }
it { is_expected.to be_valid }
end
end end
context 'policy is pull-push' do context 'policy is pull-push' do
...@@ -87,10 +118,44 @@ describe Gitlab::Ci::Config::Entry::Cache do ...@@ -87,10 +118,44 @@ describe Gitlab::Ci::Config::Entry::Cache do
end end
context 'when descendants are invalid' do context 'when descendants are invalid' do
let(:config) { { key: 1 } } context 'with invalid keys' do
let(:config) { { key: 1 } }
it 'reports error with descendants' do it 'reports error with descendants' do
is_expected.to include 'key config should be a string or symbol' is_expected.to include 'key should be a hash, a string or a symbol'
end
end
context 'with empty key' do
let(:config) { { key: {} } }
it 'reports error with descendants' do
is_expected.to include 'key config missing required keys: files'
end
end
context 'with invalid files' do
let(:config) { { key: { files: 'a-file' } } }
it 'reports error with descendants' do
is_expected.to include 'key:files config should be an array of strings'
end
end
context 'with prefix without files' do
let(:config) { { key: { prefix: 'a-prefix' } } }
it 'reports error with descendants' do
is_expected.to include 'key config missing required keys: files'
end
end
context 'when there is an unknown key present' do
let(:config) { { key: { unknown: 'a-file' } } }
it 'reports error with descendants' do
is_expected.to include 'key config contains unknown keys: unknown'
end
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Files do
let(:entry) { described_class.new(config) }
describe 'validations' do
context 'when entry config value is valid' do
let(:config) { ['some/file', 'some/path/'] }
describe '#value' do
it 'returns key value' do
expect(entry.value).to eq config
end
end
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
describe '#errors' do
context 'when entry value is not an array' do
let(:config) { 'string' }
it 'saves errors' do
expect(entry.errors)
.to include 'files config should be an array of strings'
end
end
context 'when entry value is not an array of strings' do
let(:config) { [1] }
it 'saves errors' do
expect(entry.errors)
.to include 'files config should be an array of strings'
end
end
context 'when entry value contains more than two values' do
let(:config) { %w[file1 file2 file3] }
it 'saves errors' do
expect(entry.errors)
.to include 'files config has too many items (maximum is 2)'
end
end
end
end
end
...@@ -6,38 +6,38 @@ describe Gitlab::Ci::Config::Entry::Key do ...@@ -6,38 +6,38 @@ describe Gitlab::Ci::Config::Entry::Key do
let(:entry) { described_class.new(config) } let(:entry) { described_class.new(config) }
describe 'validations' do describe 'validations' do
shared_examples 'key with slash' do it_behaves_like 'key entry validations', 'simple key'
it 'is invalid' do
expect(entry).not_to be_valid
end
it 'reports errors with config value' do context 'when entry config value is correct' do
expect(entry.errors).to include 'key config cannot contain the "/" character' context 'when key is a hash' do
end let(:config) { { files: ['test'], prefix: 'something' } }
end
shared_examples 'key with only dots' do describe '#value' do
it 'is invalid' do it 'returns key value' do
expect(entry).not_to be_valid expect(entry.value).to match(config)
end end
end
it 'reports errors with config value' do describe '#valid?' do
expect(entry.errors).to include 'key config cannot be "." or ".."' it 'is valid' do
expect(entry).to be_valid
end
end
end end
end
context 'when entry config value is correct' do context 'when key is a symbol' do
let(:config) { 'test' } let(:config) { :key }
describe '#value' do describe '#value' do
it 'returns key value' do it 'returns key value' do
expect(entry.value).to eq 'test' expect(entry.value).to eq(config.to_s)
end
end end
end
describe '#valid?' do describe '#valid?' do
it 'is valid' do it 'is valid' do
expect(entry).to be_valid expect(entry).to be_valid
end
end end
end end
end end
...@@ -47,53 +47,11 @@ describe Gitlab::Ci::Config::Entry::Key do ...@@ -47,53 +47,11 @@ describe Gitlab::Ci::Config::Entry::Key do
describe '#errors' do describe '#errors' do
it 'saves errors' do it 'saves errors' do
expect(entry.errors) expect(entry.errors.first)
.to include 'key config should be a string or symbol' .to match /should be a hash, a string or a symbol/
end end
end end
end end
context 'when entry value contains slash' do
let(:config) { 'key/with/some/slashes' }
it_behaves_like 'key with slash'
end
context 'when entry value contains URI encoded slash (%2F)' do
let(:config) { 'key%2Fwith%2Fsome%2Fslashes' }
it_behaves_like 'key with slash'
end
context 'when entry value is a dot' do
let(:config) { '.' }
it_behaves_like 'key with only dots'
end
context 'when entry value is two dots' do
let(:config) { '..' }
it_behaves_like 'key with only dots'
end
context 'when entry value is a URI encoded dot (%2E)' do
let(:config) { '%2e' }
it_behaves_like 'key with only dots'
end
context 'when entry value is two URI encoded dots (%2E)' do
let(:config) { '%2E%2e' }
it_behaves_like 'key with only dots'
end
context 'when entry value is one dot and one URI encoded dot' do
let(:config) { '.%2e' }
it_behaves_like 'key with only dots'
end
end end
describe '.default' do describe '.default' do
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Prefix do
let(:entry) { described_class.new(config) }
describe 'validations' do
it_behaves_like 'key entry validations', :prefix
context 'when entry value is not correct' do
let(:config) { ['incorrect'] }
describe '#errors' do
it 'saves errors' do
expect(entry.errors)
.to include 'prefix config should be a string or symbol'
end
end
end
end
describe '.default' do
it 'returns default key' do
expect(described_class.default).to be_nil
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Pipeline::Seed::Build::Cache do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:head_sha) { project.repository.head_commit.id }
let_it_be(:pipeline) { create(:ci_pipeline, project: project, sha: head_sha) }
let(:processor) { described_class.new(pipeline, config) }
describe '#build_attributes' do
subject { processor.build_attributes }
context 'with cache:key' do
let(:config) do
{
key: 'a-key',
paths: ['vendor/ruby']
}
end
it { is_expected.to include(options: { cache: config }) }
end
context 'with cache:key as a symbol' do
let(:config) do
{
key: :a_key,
paths: ['vendor/ruby']
}
end
it { is_expected.to include(options: { cache: config.merge(key: "a_key") }) }
end
context 'with cache:key:files' do
shared_examples 'default key' do
let(:config) do
{ key: { files: files } }
end
it 'uses default key' do
expected = { options: { cache: { key: 'default' } } }
is_expected.to include(expected)
end
end
shared_examples 'version and gemfile files' do
let(:config) do
{
key: {
files: files
},
paths: ['vendor/ruby']
}
end
it 'builds a string key' do
expected = {
options: {
cache: {
key: '703ecc8fef1635427a1f86a8a1a308831c122392',
paths: ['vendor/ruby']
}
}
}
is_expected.to include(expected)
end
end
context 'with existing files' do
let(:files) { ['VERSION', 'Gemfile.zip'] }
it_behaves_like 'version and gemfile files'
end
context 'with files starting with ./' do
let(:files) { ['Gemfile.zip', './VERSION'] }
it_behaves_like 'version and gemfile files'
end
context 'with feature flag disabled' do
let(:files) { ['VERSION', 'Gemfile.zip'] }
before do
stub_feature_flags(ci_file_based_cache: false)
end
it_behaves_like 'default key'
end
context 'with files ending with /' do
let(:files) { ['Gemfile.zip/'] }
it_behaves_like 'default key'
end
context 'with new line in filenames' do
let(:files) { ["Gemfile.zip\nVERSION"] }
it_behaves_like 'default key'
end
context 'with missing files' do
let(:files) { ['project-gemfile.lock', ''] }
it_behaves_like 'default key'
end
context 'with directories' do
shared_examples 'foo/bar directory key' do
let(:config) do
{
key: {
files: files
}
}
end
it 'builds a string key' do
expected = {
options: {
cache: { key: '74bf43fb1090f161bdd4e265802775dbda2f03d1' }
}
}
is_expected.to include(expected)
end
end
context 'with directory' do
let(:files) { ['foo/bar'] }
it_behaves_like 'foo/bar directory key'
end
context 'with directory ending in slash' do
let(:files) { ['foo/bar/'] }
it_behaves_like 'foo/bar directory key'
end
context 'with directories ending in slash star' do
let(:files) { ['foo/bar/*'] }
it_behaves_like 'foo/bar directory key'
end
end
end
context 'with cache:key:prefix' do
context 'without files' do
let(:config) do
{
key: {
prefix: 'a-prefix'
},
paths: ['vendor/ruby']
}
end
it 'adds prefix to default key' do
expected = {
options: {
cache: {
key: 'a-prefix-default',
paths: ['vendor/ruby']
}
}
}
is_expected.to include(expected)
end
end
context 'with existing files' do
let(:config) do
{
key: {
files: ['VERSION', 'Gemfile.zip'],
prefix: 'a-prefix'
},
paths: ['vendor/ruby']
}
end
it 'adds prefix key' do
expected = {
options: {
cache: {
key: 'a-prefix-703ecc8fef1635427a1f86a8a1a308831c122392',
paths: ['vendor/ruby']
}
}
}
is_expected.to include(expected)
end
end
context 'with missing files' do
let(:config) do
{
key: {
files: ['project-gemfile.lock', ''],
prefix: 'a-prefix'
},
paths: ['vendor/ruby']
}
end
it 'adds prefix to default key' do
expected = {
options: {
cache: {
key: 'a-prefix-default',
paths: ['vendor/ruby']
}
}
}
is_expected.to include(expected)
end
end
end
context 'with all cache option keys' do
let(:config) do
{
key: 'a-key',
paths: ['vendor/ruby'],
untracked: true,
policy: 'push'
}
end
it { is_expected.to include(options: { cache: config }) }
end
context 'with unknown cache option keys' do
let(:config) do
{
key: 'a-key',
unknown_key: true
}
end
it { expect { subject }.to raise_error(ArgumentError, /unknown_key/) }
end
context 'with empty config' do
let(:config) { {} }
it { is_expected.to include(options: {}) }
end
end
end
...@@ -4,7 +4,8 @@ require 'spec_helper' ...@@ -4,7 +4,8 @@ require 'spec_helper'
describe Gitlab::Ci::Pipeline::Seed::Build do describe Gitlab::Ci::Pipeline::Seed::Build do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:pipeline) { create(:ci_empty_pipeline, project: project) } let(:head_sha) { project.repository.head_commit.id }
let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: head_sha) }
let(:attributes) { { name: 'rspec', ref: 'master' } } let(:attributes) { { name: 'rspec', ref: 'master' } }
let(:previous_stages) { [] } let(:previous_stages) { [] }
...@@ -69,6 +70,101 @@ describe Gitlab::Ci::Pipeline::Seed::Build do ...@@ -69,6 +70,101 @@ describe Gitlab::Ci::Pipeline::Seed::Build do
it { is_expected.to include(when: 'never') } it { is_expected.to include(when: 'never') }
end end
end end
context 'with cache:key' do
let(:attributes) do
{
name: 'rspec',
ref: 'master',
cache: {
key: 'a-value'
}
}
end
it { is_expected.to include(options: { cache: { key: 'a-value' } }) }
end
context 'with cache:key:files' do
let(:attributes) do
{
name: 'rspec',
ref: 'master',
cache: {
key: {
files: ['VERSION']
}
}
}
end
it 'includes cache options' do
cache_options = {
options: {
cache: {
key: 'f155568ad0933d8358f66b846133614f76dd0ca4'
}
}
}
is_expected.to include(cache_options)
end
end
context 'with cache:key:prefix' do
let(:attributes) do
{
name: 'rspec',
ref: 'master',
cache: {
key: {
prefix: 'something'
}
}
}
end
it { is_expected.to include(options: { cache: { key: 'something-default' } }) }
end
context 'with cache:key:files and prefix' do
let(:attributes) do
{
name: 'rspec',
ref: 'master',
cache: {
key: {
files: ['VERSION'],
prefix: 'something'
}
}
}
end
it 'includes cache options' do
cache_options = {
options: {
cache: {
key: 'something-f155568ad0933d8358f66b846133614f76dd0ca4'
}
}
}
is_expected.to include(cache_options)
end
end
context 'with empty cache' do
let(:attributes) do
{
name: 'rspec',
ref: 'master',
cache: {}
}
end
it { is_expected.to include(options: {}) }
end
end end
describe '#bridge?' do describe '#bridge?' do
......
...@@ -950,7 +950,7 @@ module Gitlab ...@@ -950,7 +950,7 @@ module Gitlab
config_processor = Gitlab::Ci::YamlProcessor.new(config) config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.stage_builds_attributes("test").size).to eq(1) expect(config_processor.stage_builds_attributes("test").size).to eq(1)
expect(config_processor.stage_builds_attributes("test").first[:options][:cache]).to eq( expect(config_processor.stage_builds_attributes("test").first[:cache]).to eq(
paths: ["logs/", "binaries/"], paths: ["logs/", "binaries/"],
untracked: true, untracked: true,
key: 'key', key: 'key',
...@@ -962,7 +962,7 @@ module Gitlab ...@@ -962,7 +962,7 @@ module Gitlab
config = YAML.dump( config = YAML.dump(
{ {
default: { default: {
cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' } cache: { paths: ["logs/", "binaries/"], untracked: true, key: { files: ['file'] } }
}, },
rspec: { rspec: {
script: "rspec" script: "rspec"
...@@ -972,33 +972,79 @@ module Gitlab ...@@ -972,33 +972,79 @@ module Gitlab
config_processor = Gitlab::Ci::YamlProcessor.new(config) config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.stage_builds_attributes("test").size).to eq(1) expect(config_processor.stage_builds_attributes("test").size).to eq(1)
expect(config_processor.stage_builds_attributes("test").first[:options][:cache]).to eq( expect(config_processor.stage_builds_attributes("test").first[:cache]).to eq(
paths: ["logs/", "binaries/"], paths: ["logs/", "binaries/"],
untracked: true, untracked: true,
key: 'key', key: { files: ['file'] },
policy: 'pull-push' policy: 'pull-push'
) )
end end
it "returns cache when defined in a job" do it 'returns cache key when defined in a job' do
config = YAML.dump({ config = YAML.dump({
rspec: { rspec: {
cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' }, cache: { paths: ['logs/', 'binaries/'], untracked: true, key: 'key' },
script: "rspec" script: 'rspec'
} }
}) })
config_processor = Gitlab::Ci::YamlProcessor.new(config) config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.stage_builds_attributes("test").size).to eq(1) expect(config_processor.stage_builds_attributes('test').size).to eq(1)
expect(config_processor.stage_builds_attributes("test").first[:options][:cache]).to eq( expect(config_processor.stage_builds_attributes('test').first[:cache]).to eq(
paths: ["logs/", "binaries/"], paths: ['logs/', 'binaries/'],
untracked: true, untracked: true,
key: 'key', key: 'key',
policy: 'pull-push' policy: 'pull-push'
) )
end end
it 'returns cache files' do
config = YAML.dump(
rspec: {
cache: {
paths: ['logs/', 'binaries/'],
untracked: true,
key: { files: ['file'] }
},
script: 'rspec'
}
)
config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.stage_builds_attributes('test').size).to eq(1)
expect(config_processor.stage_builds_attributes('test').first[:cache]).to eq(
paths: ['logs/', 'binaries/'],
untracked: true,
key: { files: ['file'] },
policy: 'pull-push'
)
end
it 'returns cache files with prefix' do
config = YAML.dump(
rspec: {
cache: {
paths: ['logs/', 'binaries/'],
untracked: true,
key: { files: ['file'], prefix: 'prefix' }
},
script: 'rspec'
}
)
config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.stage_builds_attributes('test').size).to eq(1)
expect(config_processor.stage_builds_attributes('test').first[:cache]).to eq(
paths: ['logs/', 'binaries/'],
untracked: true,
key: { files: ['file'], prefix: 'prefix' },
policy: 'pull-push'
)
end
it "overwrite cache when defined for a job and globally" do it "overwrite cache when defined for a job and globally" do
config = YAML.dump({ config = YAML.dump({
cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'global' }, cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'global' },
...@@ -1011,7 +1057,7 @@ module Gitlab ...@@ -1011,7 +1057,7 @@ module Gitlab
config_processor = Gitlab::Ci::YamlProcessor.new(config) config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.stage_builds_attributes("test").size).to eq(1) expect(config_processor.stage_builds_attributes("test").size).to eq(1)
expect(config_processor.stage_builds_attributes("test").first[:options][:cache]).to eq( expect(config_processor.stage_builds_attributes("test").first[:cache]).to eq(
paths: ["test/"], paths: ["test/"],
untracked: false, untracked: false,
key: 'local', key: 'local',
...@@ -1862,14 +1908,42 @@ module Gitlab ...@@ -1862,14 +1908,42 @@ module Gitlab
config = YAML.dump({ cache: { key: 1 }, rspec: { script: "test" } }) config = YAML.dump({ cache: { key: 1 }, rspec: { script: "test" } })
expect do expect do
Gitlab::Ci::YamlProcessor.new(config) Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "cache:key config should be a string or symbol") end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "cache:key should be a hash, a string or a symbol")
end end
it "returns errors if job cache:key is not an a string" do it "returns errors if job cache:key is not an a string" do
config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: 1 } } }) config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: 1 } } })
expect do expect do
Gitlab::Ci::YamlProcessor.new(config) Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:cache:key config should be a string or symbol") end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:cache:key should be a hash, a string or a symbol")
end
it 'returns errors if job cache:key:files is not an array of strings' do
config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: { files: [1] } } } })
expect do
Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'jobs:rspec:cache:key:files config should be an array of strings')
end
it 'returns errors if job cache:key:files is an empty array' do
config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: { files: [] } } } })
expect do
Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'jobs:rspec:cache:key:files config requires at least 1 item')
end
it 'returns errors if job defines only cache:key:prefix' do
config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: { prefix: 'prefix-key' } } } })
expect do
Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'jobs:rspec:cache:key config missing required keys: files')
end
it 'returns errors if job cache:key:prefix is not an a string' do
config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: { prefix: 1, files: ['file'] } } } })
expect do
Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'jobs:rspec:cache:key:prefix config should be a string or symbol')
end end
it "returns errors if job cache:untracked is not an array of strings" do it "returns errors if job cache:untracked is not an array of strings" do
......
# frozen_string_literal: true
require 'spec_helper' require 'spec_helper'
describe Gitlab::ExternalAuthorization::Access, :clean_gitlab_redis_cache do describe Gitlab::ExternalAuthorization::Access, :clean_gitlab_redis_cache do
......
# frozen_string_literal: true
require 'spec_helper' require 'spec_helper'
describe Gitlab::ExternalAuthorization::Cache, :clean_gitlab_redis_cache do describe Gitlab::ExternalAuthorization::Cache, :clean_gitlab_redis_cache do
......
# frozen_string_literal: true
require 'spec_helper' require 'spec_helper'
describe Gitlab::ExternalAuthorization::Client do describe Gitlab::ExternalAuthorization::Client do
......
# frozen_string_literal: true
require 'spec_helper' require 'spec_helper'
describe Gitlab::ExternalAuthorization::Logger do describe Gitlab::ExternalAuthorization::Logger do
......
# frozen_string_literal: true
require 'spec_helper' require 'spec_helper'
describe Gitlab::ExternalAuthorization::Response do describe Gitlab::ExternalAuthorization::Response do
......
# frozen_string_literal: true
require 'spec_helper'
describe Ci::CreatePipelineService do
context 'cache' do
let(:user) { create(:admin) }
let(:ref) { 'refs/heads/master' }
let(:source) { :push }
let(:service) { described_class.new(project, user, { ref: ref }) }
let(:pipeline) { service.execute(source) }
let(:job) { pipeline.builds.find_by(name: 'job') }
let(:project) { create(:project, :custom_repo, files: files) }
before do
stub_ci_pipeline_yaml_file(config)
end
context 'with cache:key' do
let(:files) { { 'some-file' => '' } }
let(:config) do
<<~EOY
job:
script:
- ls
cache:
key: 'a-key'
paths: ['logs/', 'binaries/']
untracked: true
EOY
end
it 'uses the provided key' do
expected = {
'key' => 'a-key',
'paths' => ['logs/', 'binaries/'],
'policy' => 'pull-push',
'untracked' => true
}
expect(pipeline).to be_persisted
expect(job.cache).to match(a_collection_including(expected))
end
end
context 'with cache:key:files' do
let(:config) do
<<~EOY
job:
script:
- ls
cache:
paths:
- logs/
key:
files:
- file.lock
- missing-file.lock
EOY
end
context 'when file.lock exists' do
let(:files) { { 'file.lock' => '' } }
it 'builds a cache key' do
expected = {
'key' => /[a-f0-9]{40}/,
'paths' => ['logs/'],
'policy' => 'pull-push'
}
expect(pipeline).to be_persisted
expect(job.cache).to match(a_collection_including(expected))
end
end
context 'when file.lock does not exist' do
let(:files) { { 'some-file' => '' } }
it 'uses default cache key' do
expected = {
'key' => /default/,
'paths' => ['logs/'],
'policy' => 'pull-push'
}
expect(pipeline).to be_persisted
expect(job.cache).to match(a_collection_including(expected))
end
end
end
context 'with cache:key:files and prefix' do
let(:config) do
<<~EOY
job:
script:
- ls
cache:
paths:
- logs/
key:
files:
- file.lock
prefix: '$ENV_VAR'
EOY
end
context 'when file.lock exists' do
let(:files) { { 'file.lock' => '' } }
it 'builds a cache key' do
expected = {
'key' => /\$ENV_VAR-[a-f0-9]{40}/,
'paths' => ['logs/'],
'policy' => 'pull-push'
}
expect(pipeline).to be_persisted
expect(job.cache).to match(a_collection_including(expected))
end
end
context 'when file.lock does not exist' do
let(:files) { { 'some-file' => '' } }
it 'uses default cache key' do
expected = {
'key' => /\$ENV_VAR-default/,
'paths' => ['logs/'],
'policy' => 'pull-push'
}
expect(pipeline).to be_persisted
expect(job.cache).to match(a_collection_including(expected))
end
end
end
context 'with too many files' do
let(:files) { { 'some-file' => '' } }
let(:config) do
<<~EOY
job:
script:
- ls
cache:
paths: ['logs/', 'binaries/']
untracked: true
key:
files:
- file.lock
- other-file.lock
- extra-file.lock
prefix: 'some-prefix'
EOY
end
it 'has errors' do
expect(pipeline).to be_persisted
expect(pipeline.yaml_errors).to eq("jobs:job:cache:key:files config has too many items (maximum is 2)")
expect(job).to be_nil
end
end
end
end
# frozen_string_literal: true
RSpec.shared_examples 'key entry validations' do |config_name|
shared_examples 'key with slash' do
it 'is invalid' do
expect(entry).not_to be_valid
end
it 'reports errors with config value' do
expect(entry.errors).to include "#{config_name} config cannot contain the \"/\" character"
end
end
shared_examples 'key with only dots' do
it 'is invalid' do
expect(entry).not_to be_valid
end
it 'reports errors with config value' do
expect(entry.errors).to include "#{config_name} config cannot be \".\" or \"..\""
end
end
context 'when entry value contains slash' do
let(:config) { 'key/with/some/slashes' }
it_behaves_like 'key with slash'
end
context 'when entry value contains URI encoded slash (%2F)' do
let(:config) { 'key%2Fwith%2Fsome%2Fslashes' }
it_behaves_like 'key with slash'
end
context 'when entry value is a dot' do
let(:config) { '.' }
it_behaves_like 'key with only dots'
end
context 'when entry value is two dots' do
let(:config) { '..' }
it_behaves_like 'key with only dots'
end
context 'when entry value is a URI encoded dot (%2E)' do
let(:config) { '%2e' }
it_behaves_like 'key with only dots'
end
context 'when entry value is two URI encoded dots (%2E)' do
let(:config) { '%2E%2e' }
it_behaves_like 'key with only dots'
end
context 'when entry value is one dot and one URI encoded dot' do
let(:config) { '.%2e' }
it_behaves_like 'key with only dots'
end
context 'when key is a string' do
let(:config) { 'test' }
describe '#value' do
it 'returns key value' do
expect(entry.value).to eq 'test'
end
end
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
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