Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/components/cluster_form_dropdown.vue99
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/components/create_eks_cluster.vue1
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue182
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/components/region_dropdown.vue63
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue2
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/constants.js7
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/index.js20
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/services/aws_services_facade.js134
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/store/actions.js59
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/store/index.js13
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/store/mutation_types.js6
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/store/mutations.js17
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/store/state.js9
-rw-r--r--app/views/profiles/show.html.haml3
-rw-r--r--changelogs/unreleased/18986-allow-to-use-commit-sha-in-cache-key.yml5
-rw-r--r--changelogs/unreleased/22392-eks-create-cluster-fe.yml5
-rw-r--r--changelogs/unreleased/growth-91-when-users-update-their-profile-do-not-let-them-complete-the-up.yml5
-rw-r--r--doc/api/packages.md2
-rw-r--r--doc/ci/yaml/README.md44
-rw-r--r--doc/development/feature_flags/development.md16
-rw-r--r--lib/gitlab/ci/config/entry/files.rb26
-rw-r--r--lib/gitlab/ci/config/entry/key.rb45
-rw-r--r--lib/gitlab/ci/config/entry/prefix.rb20
-rw-r--r--lib/gitlab/ci/pipeline/seed/build.rb9
-rw-r--r--lib/gitlab/ci/pipeline/seed/build/cache.rb77
-rw-r--r--lib/gitlab/ci/yaml_processor.rb2
-rw-r--r--locale/gitlab.pot43
-rw-r--r--spec/features/profiles/user_edit_profile_spec.rb32
-rw-r--r--spec/frontend/create_cluster/eks_cluster/components/cluster_form_dropdown_spec.js44
-rw-r--r--spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js181
-rw-r--r--spec/frontend/create_cluster/eks_cluster/components/region_dropdown_spec.js55
-rw-r--r--spec/frontend/create_cluster/eks_cluster/services/aws_services_facade_spec.js152
-rw-r--r--spec/frontend/create_cluster/eks_cluster/store/actions_spec.js154
-rw-r--r--spec/frontend/create_cluster/eks_cluster/store/mutations_spec.js54
-rw-r--r--spec/lib/gitlab/ci/config/entry/cache_spec.rb77
-rw-r--r--spec/lib/gitlab/ci/config/entry/files_spec.rb54
-rw-r--r--spec/lib/gitlab/ci/config/entry/key_spec.rb94
-rw-r--r--spec/lib/gitlab/ci/config/entry/prefix_spec.rb28
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb261
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/build_spec.rb98
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb100
-rw-r--r--spec/lib/gitlab/external_authorization/access_spec.rb2
-rw-r--r--spec/lib/gitlab/external_authorization/cache_spec.rb2
-rw-r--r--spec/lib/gitlab/external_authorization/client_spec.rb2
-rw-r--r--spec/lib/gitlab/external_authorization/logger_spec.rb2
-rw-r--r--spec/lib/gitlab/external_authorization/response_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/cache_spec.rb168
-rw-r--r--spec/support/shared_examples/lib/gitlab/ci/config/entry/key_validations_shared_examples.rb81
48 files changed, 2130 insertions, 427 deletions
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/cluster_form_dropdown.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/cluster_form_dropdown.vue
index 3c6da43c4c4..e6893c14cda 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/components/cluster_form_dropdown.vue
+++ b/app/assets/javascripts/create_cluster/eks_cluster/components/cluster_form_dropdown.vue
@@ -2,14 +2,19 @@
import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.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 {
components: {
DropdownButton,
DropdownSearchInput,
DropdownHiddenInput,
+ GlIcon,
},
props: {
fieldName: {
@@ -28,7 +33,7 @@ export default {
default: '',
},
value: {
- type: [Object, String],
+ type: [Object, Array, String],
required: false,
default: () => null,
},
@@ -72,6 +77,11 @@ export default {
required: false,
default: false,
},
+ multiple: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
errorMessage: {
type: String,
required: false,
@@ -90,12 +100,11 @@ export default {
searchFn: {
type: Function,
required: false,
- default: searchQuery => item => item.name.toLowerCase().indexOf(searchQuery) > -1,
+ default: defaultSearchFn,
},
},
data() {
return {
- selectedItem: findItem(this.items, this.value),
searchQuery: '',
};
},
@@ -109,36 +118,52 @@ export default {
return this.disabledText;
}
- if (!this.selectedItem) {
+ if (!this.selectedItems.length) {
return this.placeholder;
}
- return this.selectedItemLabel;
+ return this.selectedItemsLabels;
},
results() {
- if (!this.items) {
- return [];
- }
-
- return this.items.filter(this.searchFn(this.searchQuery));
+ return this.getItemsOrEmptyList().filter(this.searchFn(this.searchQuery, this.labelProperty));
},
- selectedItemLabel() {
- return this.selectedItem && this.selectedItem[this.labelProperty];
+ selectedItems() {
+ const valueProp = this.valueProperty;
+ const valueList = toArray(this.value);
+ const items = this.getItemsOrEmptyList();
+
+ return items.filter(item => valueList.some(value => item[valueProp] === value));
},
- selectedItemValue() {
- return (this.selectedItem && this.selectedItem[this.valueProperty]) || '';
+ selectedItemsLabels() {
+ return itemsProp(this.selectedItems, this.labelProperty).join(', ');
},
- },
- watch: {
- value(value) {
- this.selectedItem = findItem(this.items, this.valueProperty, value);
+ selectedItemsValues() {
+ return itemsProp(this.selectedItems, this.valueProperty).join(', ');
},
},
methods: {
- select(item) {
- this.selectedItem = item;
+ getItemsOrEmptyList() {
+ return this.items || [];
+ },
+ selectSingle(item) {
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>
@@ -146,7 +171,7 @@ export default {
<template>
<div>
<div class="js-gcp-machine-type-dropdown dropdown">
- <dropdown-hidden-input :name="fieldName" :value="selectedItemValue" />
+ <dropdown-hidden-input :name="fieldName" :value="selectedItemsValues" />
<dropdown-button
:class="{ 'border-danger': hasErrors }"
:is-disabled="disabled"
@@ -158,15 +183,28 @@ export default {
<div class="dropdown-content">
<ul>
<li v-if="!results.length">
- <span class="js-empty-text menu-item">
- {{ emptyText }}
- </span>
+ <span class="js-empty-text menu-item">{{ emptyText }}</span>
</li>
<li v-for="item in results" :key="item.id">
- <button class="js-dropdown-item" type="button" @click.prevent="select(item)">
- <slot name="item" :item="item">
- {{ item.name }}
- </slot>
+ <button
+ v-if="multiple"
+ class="js-dropdown-item d-flex align-items-center"
+ 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>
</li>
</ul>
@@ -182,8 +220,7 @@ export default {
'text-muted': !hasErrors,
},
]"
+ >{{ errorMessage }}</span
>
- {{ errorMessage }}
- </span>
</div>
</template>
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/create_eks_cluster.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/create_eks_cluster.vue
index 6bcae6ab536..3f7c2204b9f 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/components/create_eks_cluster.vue
+++ b/app/assets/javascripts/create_cluster/eks_cluster/components/create_eks_cluster.vue
@@ -41,6 +41,7 @@ export default {
v-if="hasCredentials"
:gitlab-managed-cluster-help-path="gitlabManagedClusterHelpPath"
:kubernetes-integration-help-path="kubernetesIntegrationHelpPath"
+ :external-link-icon="externalLinkIcon"
/>
<service-credentials-form
v-else
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue
index 1188cf08850..57d5f4f541b 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue
+++ b/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue
@@ -4,8 +4,8 @@ import { sprintf, s__ } from '~/locale';
import _ from 'underscore';
import { GlFormInput, GlFormCheckbox } from '@gitlab/ui';
import ClusterFormDropdown from './cluster_form_dropdown.vue';
-import RegionDropdown from './region_dropdown.vue';
import { KUBERNETES_VERSIONS } from '../constants';
+import LoadingButton from '~/vue_shared/components/loading_button.vue';
const { mapState: mapRolesState, mapActions: mapRolesActions } = createNamespacedHelpers('roles');
const { mapState: mapRegionsState, mapActions: mapRegionsActions } = createNamespacedHelpers(
@@ -22,13 +22,17 @@ const {
mapState: mapSecurityGroupsState,
mapActions: mapSecurityGroupsActions,
} = createNamespacedHelpers('securityGroups');
+const {
+ mapState: mapInstanceTypesState,
+ mapActions: mapInstanceTypesActions,
+} = createNamespacedHelpers('instanceTypes');
export default {
components: {
ClusterFormDropdown,
- RegionDropdown,
GlFormInput,
GlFormCheckbox,
+ LoadingButton,
},
props: {
gitlabManagedClusterHelpPath: {
@@ -39,6 +43,10 @@ export default {
type: String,
required: true,
},
+ externalLinkIcon: {
+ type: String,
+ required: true,
+ },
},
computed: {
...mapState([
@@ -51,7 +59,10 @@ export default {
'selectedSubnet',
'selectedRole',
'selectedSecurityGroup',
+ 'selectedInstanceType',
+ 'nodeCount',
'gitlabManagedCluster',
+ 'isCreatingCluster',
]),
...mapRolesState({
roles: 'items',
@@ -83,6 +94,11 @@ export default {
isLoadingSecurityGroups: 'isLoadingItems',
loadingSecurityGroupsError: 'loadingItemsError',
}),
+ ...mapInstanceTypesState({
+ instanceTypes: 'items',
+ isLoadingInstanceTypes: 'isLoadingItems',
+ loadingInstanceTypesError: 'loadingItemsError',
+ }),
kubernetesVersions() {
return KUBERNETES_VERSIONS;
},
@@ -98,6 +114,27 @@ export default {
securityGroupDropdownDisabled() {
return !this.selectedVpc;
},
+ createClusterButtonDisabled() {
+ return (
+ !this.clusterName ||
+ !this.environmentScope ||
+ !this.kubernetesVersion ||
+ !this.selectedRegion ||
+ !this.selectedKeyPair ||
+ !this.selectedVpc ||
+ !this.selectedSubnet ||
+ !this.selectedRole ||
+ !this.selectedSecurityGroup ||
+ !this.selectedInstanceType ||
+ !this.nodeCount ||
+ this.isCreatingCluster
+ );
+ },
+ createClusterButtonLabel() {
+ return this.isCreatingCluster
+ ? s__('ClusterIntegration|Creating Kubernetes cluster')
+ : s__('ClusterIntegration|Create Kubernetes cluster');
+ },
kubernetesIntegrationHelpText() {
const escapedUrl = _.escape(this.kubernetesIntegrationHelpPath);
@@ -115,11 +152,26 @@ export default {
roleDropdownHelpText() {
return sprintf(
s__(
- '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}.',
+ '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}.',
+ ),
+ {
+ startLink:
+ '<a href="https://docs.aws.amazon.com/eks/latest/userguide/getting-started-console.html#role-create" target="_blank" rel="noopener noreferrer">',
+ externalLinkIcon: this.externalLinkIcon,
+ endLink: '</a>',
+ },
+ false,
+ );
+ },
+ regionsDropdownHelpText() {
+ return sprintf(
+ s__(
+ 'ClusterIntegration|Learn more about %{startLink}Regions %{externalLinkIcon}%{endLink}.',
),
{
startLink:
- '<a href="https://console.aws.amazon.com/iam/home?#roles" target="_blank" rel="noopener noreferrer">',
+ '<a href="https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services/" target="_blank" rel="noopener noreferrer">',
+ externalLinkIcon: this.externalLinkIcon,
endLink: '</a>',
},
false,
@@ -128,11 +180,12 @@ export default {
keyPairDropdownHelpText() {
return sprintf(
s__(
- '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}.',
+ '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}.',
),
{
startLink:
'<a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html#having-ec2-create-your-key-pair" target="_blank" rel="noopener noreferrer">',
+ externalLinkIcon: this.externalLinkIcon,
endLink: '</a>',
},
false,
@@ -141,11 +194,12 @@ export default {
vpcDropdownHelpText() {
return sprintf(
s__(
- '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}.',
+ '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}.',
),
{
startLink:
- '<a href="https://console.aws.amazon.com/vpc/home?#vpc" target="_blank" rel="noopener noreferrer">',
+ '<a href="https://docs.aws.amazon.com/eks/latest/userguide/getting-started-console.html#vpc-create" target="_blank" rel="noopener noreferrer">',
+ externalLinkIcon: this.externalLinkIcon,
endLink: '</a>',
},
false,
@@ -154,11 +208,12 @@ export default {
subnetDropdownHelpText() {
return sprintf(
s__(
- 'ClusterIntegration|Choose the %{startLink}subnets%{endLink} in your VPC where your worker nodes will run.',
+ 'ClusterIntegration|Choose the %{startLink}subnets %{externalLinkIcon} %{endLink} in your VPC where your worker nodes will run.',
),
{
startLink:
'<a href="https://console.aws.amazon.com/vpc/home?#subnets" target="_blank" rel="noopener noreferrer">',
+ externalLinkIcon: this.externalLinkIcon,
endLink: '</a>',
},
false,
@@ -167,11 +222,26 @@ export default {
securityGroupDropdownHelpText() {
return sprintf(
s__(
- 'ClusterIntegration|Choose the %{startLink}security groups%{endLink} to apply to the EKS-managed Elastic Network Interfaces that are created in your worker node subnets.',
+ '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.',
),
{
startLink:
'<a href="https://console.aws.amazon.com/vpc/home?#securityGroups" target="_blank" rel="noopener noreferrer">',
+ externalLinkIcon: this.externalLinkIcon,
+ endLink: '</a>',
+ },
+ false,
+ );
+ },
+ instanceTypesDropdownHelpText() {
+ return sprintf(
+ s__(
+ 'ClusterIntegration|Choose the worker node %{startLink}instance type %{externalLinkIcon} %{endLink}.',
+ ),
+ {
+ startLink:
+ '<a href="https://aws.amazon.com/ec2/instance-types" target="_blank" rel="noopener noreferrer">',
+ externalLinkIcon: this.externalLinkIcon,
endLink: '</a>',
},
false,
@@ -195,9 +265,12 @@ export default {
mounted() {
this.fetchRegions();
this.fetchRoles();
+ this.fetchInstanceTypes();
},
methods: {
...mapActions([
+ 'createCluster',
+ 'signOut',
'setClusterName',
'setEnvironmentScope',
'setKubernetesVersion',
@@ -207,6 +280,8 @@ export default {
'setRole',
'setKeyPair',
'setSecurityGroup',
+ 'setInstanceType',
+ 'setNodeCount',
'setGitlabManagedCluster',
]),
...mapRegionsActions({ fetchRegions: 'fetchItems' }),
@@ -215,15 +290,22 @@ export default {
...mapRolesActions({ fetchRoles: 'fetchItems' }),
...mapKeyPairsActions({ fetchKeyPairs: 'fetchItems' }),
...mapSecurityGroupsActions({ fetchSecurityGroups: 'fetchItems' }),
+ ...mapInstanceTypesActions({ fetchInstanceTypes: 'fetchItems' }),
setRegionAndFetchVpcsAndKeyPairs(region) {
this.setRegion({ region });
+ this.setVpc({ vpc: null });
+ this.setKeyPair({ keyPair: null });
+ this.setSubnet({ subnet: null });
+ this.setSecurityGroup({ securityGroup: null });
this.fetchVpcs({ region });
this.fetchKeyPairs({ region });
},
setVpcAndFetchSubnets(vpc) {
this.setVpc({ vpc });
- this.fetchSubnets({ vpc });
- this.fetchSecurityGroups({ vpc });
+ this.setSubnet({ subnet: null });
+ this.setSecurityGroup({ securityGroup: null });
+ this.fetchSubnets({ vpc, region: this.selectedRegion });
+ this.fetchSecurityGroups({ vpc, region: this.selectedRegion });
},
},
};
@@ -233,7 +315,12 @@ export default {
<h2>
{{ s__('ClusterIntegration|Enter the details for your Amazon EKS Kubernetes cluster') }}
</h2>
- <p v-html="kubernetesIntegrationHelpText"></p>
+ <div class="mb-3" v-html="kubernetesIntegrationHelpText"></div>
+ <div class="mb-3">
+ <button class="btn btn-link js-sign-out" @click.prevent="signOut()">
+ {{ s__('ClusterIntegration|Select a different AWS role') }}
+ </button>
+ </div>
<div class="form-group">
<label class="label-bold" for="eks-cluster-name">{{
s__('ClusterIntegration|Kubernetes cluster name')
@@ -273,7 +360,7 @@ export default {
<cluster-form-dropdown
field-id="eks-role"
field-name="eks-role"
- :input="selectedRole"
+ :value="selectedRole"
:items="roles"
:loading="isLoadingRoles"
:loading-text="s__('ClusterIntegration|Loading IAM Roles')"
@@ -288,13 +375,21 @@ export default {
</div>
<div class="form-group">
<label class="label-bold" for="eks-role">{{ s__('ClusterIntegration|Region') }}</label>
- <region-dropdown
+ <cluster-form-dropdown
+ field-id="eks-region"
+ field-name="eks-region"
:value="selectedRegion"
- :regions="regions"
- :error="loadingRegionsError"
+ :items="regions"
:loading="isLoadingRegions"
+ :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="Boolean(loadingRegionsError)"
+ :error-message="s__('ClusterIntegration|Could not load regions from your AWS account')"
@input="setRegionAndFetchVpcsAndKeyPairs($event)"
/>
+ <p class="form-text text-muted" v-html="regionsDropdownHelpText"></p>
</div>
<div class="form-group">
<label class="label-bold" for="eks-key-pair">{{
@@ -303,7 +398,7 @@ export default {
<cluster-form-dropdown
field-id="eks-key-pair"
field-name="eks-key-pair"
- :input="selectedKeyPair"
+ :value="selectedKeyPair"
:items="keyPairs"
:disabled="keyPairDropdownDisabled"
:disabled-text="s__('ClusterIntegration|Select a region to choose a Key Pair')"
@@ -323,7 +418,7 @@ export default {
<cluster-form-dropdown
field-id="eks-vpc"
field-name="eks-vpc"
- :input="selectedVpc"
+ :value="selectedVpc"
:items="vpcs"
:loading="isLoadingVpcs"
:disabled="vpcDropdownDisabled"
@@ -339,11 +434,12 @@ export default {
<p class="form-text text-muted" v-html="vpcDropdownHelpText"></p>
</div>
<div class="form-group">
- <label class="label-bold" for="eks-role">{{ s__('ClusterIntegration|Subnet') }}</label>
+ <label class="label-bold" for="eks-role">{{ s__('ClusterIntegration|Subnets') }}</label>
<cluster-form-dropdown
field-id="eks-subnet"
field-name="eks-subnet"
- :input="selectedSubnet"
+ multiple
+ :value="selectedSubnet"
:items="subnets"
:loading="isLoadingSubnets"
:disabled="subnetDropdownDisabled"
@@ -360,12 +456,12 @@ export default {
</div>
<div class="form-group">
<label class="label-bold" for="eks-security-group">{{
- s__('ClusterIntegration|Security groups')
+ s__('ClusterIntegration|Security group')
}}</label>
<cluster-form-dropdown
field-id="eks-security-group"
field-name="eks-security-group"
- :input="selectedSecurityGroup"
+ :value="selectedSecurityGroup"
:items="securityGroups"
:loading="isLoadingSecurityGroups"
:disabled="securityGroupDropdownDisabled"
@@ -383,6 +479,39 @@ export default {
<p class="form-text text-muted" v-html="securityGroupDropdownHelpText"></p>
</div>
<div class="form-group">
+ <label class="label-bold" for="eks-instance-type">{{
+ s__('ClusterIntegration|Instance type')
+ }}</label>
+ <cluster-form-dropdown
+ field-id="eks-instance-type"
+ field-name="eks-instance-type"
+ :value="selectedInstanceType"
+ :items="instanceTypes"
+ :loading="isLoadingInstanceTypes"
+ :loading-text="s__('ClusterIntegration|Loading instance types')"
+ :placeholder="s__('ClusterIntergation|Select an instance type')"
+ :search-field-placeholder="s__('ClusterIntegration|Search instance types')"
+ :empty-text="s__('ClusterIntegration|No instance type found')"
+ :has-errors="Boolean(loadingInstanceTypesError)"
+ :error-message="s__('ClusterIntegration|Could not load instance types')"
+ @input="setInstanceType({ instanceType: $event })"
+ />
+ <p class="form-text text-muted" v-html="instanceTypesDropdownHelpText"></p>
+ </div>
+ <div class="form-group">
+ <label class="label-bold" for="eks-node-count">{{
+ s__('ClusterIntegration|Number of nodes')
+ }}</label>
+ <gl-form-input
+ id="eks-node-count"
+ type="number"
+ min="1"
+ step="1"
+ :value="nodeCount"
+ @input="setNodeCount({ nodeCount: $event })"
+ />
+ </div>
+ <div class="form-group">
<gl-form-checkbox
:checked="gitlabManagedCluster"
@input="setGitlabManagedCluster({ gitlabManagedCluster: $event })"
@@ -390,5 +519,14 @@ export default {
>
<p class="form-text text-muted" v-html="gitlabManagedHelpText"></p>
</div>
+ <div class="form-group">
+ <loading-button
+ class="js-create-cluster btn-success"
+ :disabled="createClusterButtonDisabled"
+ :loading="isCreatingCluster"
+ :label="createClusterButtonLabel"
+ @click="createCluster()"
+ />
+ </div>
</form>
</template>
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/region_dropdown.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/region_dropdown.vue
deleted file mode 100644
index 765955305c8..00000000000
--- a/app/assets/javascripts/create_cluster/eks_cluster/components/region_dropdown.vue
+++ /dev/null
@@ -1,63 +0,0 @@
-<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>
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue
index 185fecba2d8..ab33e9fbc95 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue
+++ b/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue
@@ -131,7 +131,7 @@ export default {
<p class="form-text text-muted" v-html="provisionRoleArnHelpText"></p>
</div>
<loading-button
- class="js-submit-service-credentials"
+ class="js-submit-service-credentials btn-success"
type="submit"
:disabled="submitButtonDisabled"
:loading="isCreatingRole"
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/constants.js b/app/assets/javascripts/create_cluster/eks_cluster/constants.js
index 339642f991e..a850ba89818 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/constants.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/constants.js
@@ -1,7 +1,2 @@
// eslint-disable-next-line import/prefer-default-export
-export const KUBERNETES_VERSIONS = [
- { name: '1.14', value: '1.14' },
- { name: '1.13', value: '1.13' },
- { name: '1.12', value: '1.12' },
- { name: '1.11', value: '1.11' },
-];
+export const KUBERNETES_VERSIONS = [{ name: '1.14', value: '1.14' }];
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/index.js b/app/assets/javascripts/create_cluster/eks_cluster/index.js
index e634a743d1d..27f859d8972 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/index.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/index.js
@@ -12,10 +12,19 @@ export default el => {
kubernetesIntegrationHelpPath,
accountAndExternalIdsHelpPath,
createRoleArnHelpPath,
+ getRolesPath,
+ getRegionsPath,
+ getKeyPairsPath,
+ getVpcsPath,
+ getSubnetsPath,
+ getSecurityGroupsPath,
+ getInstanceTypesPath,
externalId,
accountId,
hasCredentials,
createRolePath,
+ createClusterPath,
+ signOutPath,
externalLinkIcon,
} = el.dataset;
@@ -27,6 +36,17 @@ export default el => {
externalId,
accountId,
createRolePath,
+ createClusterPath,
+ signOutPath,
+ },
+ apiPaths: {
+ getRolesPath,
+ getRegionsPath,
+ getKeyPairsPath,
+ getVpcsPath,
+ getSubnetsPath,
+ getSecurityGroupsPath,
+ getInstanceTypesPath,
},
}),
components: {
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/services/aws_services_facade.js b/app/assets/javascripts/create_cluster/eks_cluster/services/aws_services_facade.js
index d982e4db4c1..21b87d525cf 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/services/aws_services_facade.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/services/aws_services_facade.js
@@ -1,84 +1,58 @@
-import EC2 from 'aws-sdk/clients/ec2';
-import IAM from 'aws-sdk/clients/iam';
-
-export const fetchRoles = () => {
- const iam = new IAM();
-
- return iam
- .listRoles()
- .promise()
- .then(({ Roles: roles }) => roles.map(({ RoleName: name }) => ({ name })));
-};
-
-export const fetchKeyPairs = () => {
- const ec2 = new EC2();
-
- return ec2
- .describeKeyPairs()
- .promise()
- .then(({ KeyPairs: keyPairs }) => keyPairs.map(({ RegionName: name }) => ({ name })));
-};
-
-export const fetchRegions = () => {
- const ec2 = new EC2();
-
- return ec2
- .describeRegions()
- .promise()
- .then(({ Regions: regions }) =>
- regions.map(({ RegionName: name }) => ({
- name,
- value: name,
+import axios from '~/lib/utils/axios_utils';
+
+export default apiPaths => ({
+ fetchRoles() {
+ return axios
+ .get(apiPaths.getRolesPath)
+ .then(({ data: { roles } }) =>
+ roles.map(({ role_name: name, arn: value }) => ({ name, value })),
+ );
+ },
+ fetchKeyPairs({ region }) {
+ return axios
+ .get(apiPaths.getKeyPairsPath, { params: { region } })
+ .then(({ data: { key_pairs: keyPairs } }) =>
+ keyPairs.map(({ key_name }) => ({ name: key_name, value: key_name })),
+ );
+ },
+ fetchRegions() {
+ return axios.get(apiPaths.getRegionsPath).then(({ data: { regions } }) =>
+ regions.map(({ region_name }) => ({
+ name: region_name,
+ value: region_name,
})),
);
-};
-
-export const fetchVpcs = () => {
- const ec2 = new EC2();
-
- return ec2
- .describeVpcs()
- .promise()
- .then(({ Vpcs: vpcs }) =>
- vpcs.map(({ VpcId: id }) => ({
- value: id,
- name: id,
+ },
+ fetchVpcs({ region }) {
+ return axios.get(apiPaths.getVpcsPath, { params: { region } }).then(({ data: { vpcs } }) =>
+ vpcs.map(({ vpc_id }) => ({
+ value: vpc_id,
+ name: vpc_id,
})),
);
-};
-
-export const fetchSubnets = ({ vpc }) => {
- const ec2 = new EC2();
-
- return ec2
- .describeSubnets({
- Filters: [
- {
- Name: 'vpc-id',
- Values: [vpc],
- },
- ],
- })
- .promise()
- .then(({ Subnets: subnets }) => subnets.map(({ SubnetId: id }) => ({ id, name: id })));
-};
-
-export const fetchSecurityGroups = ({ vpc }) => {
- const ec2 = new EC2();
-
- return ec2
- .describeSecurityGroups({
- Filters: [
- {
- Name: 'vpc-id',
- Values: [vpc],
- },
- ],
- })
- .promise()
- .then(({ SecurityGroups: securityGroups }) =>
- securityGroups.map(({ GroupName: name, GroupId: value }) => ({ name, value })),
- );
-};
-
-export default () => {};
+ },
+ fetchSubnets({ vpc, region }) {
+ return axios
+ .get(apiPaths.getSubnetsPath, { params: { vpc_id: vpc, region } })
+ .then(({ data: { subnets } }) =>
+ subnets.map(({ subnet_id }) => ({ name: subnet_id, value: subnet_id })),
+ );
+ },
+ fetchSecurityGroups({ vpc, region }) {
+ return axios
+ .get(apiPaths.getSecurityGroupsPath, { params: { vpc_id: vpc, region } })
+ .then(({ data: { security_groups: securityGroups } }) =>
+ securityGroups.map(({ group_name: name, group_id: value }) => ({ name, value })),
+ );
+ },
+ fetchInstanceTypes() {
+ return axios
+ .get(apiPaths.getInstanceTypesPath)
+ .then(({ data: { instance_types: instanceTypes } }) =>
+ instanceTypes.map(({ instance_type_name }) => ({
+ name: instance_type_name,
+ value: instance_type_name,
+ })),
+ );
+ },
+});
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js b/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js
index 16a7547957e..72f15263a8f 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js
@@ -1,5 +1,12 @@
import * as types from './mutation_types';
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) => {
commit(types.SET_CLUSTER_NAME, payload);
@@ -37,6 +44,44 @@ export const createRoleError = ({ commit }, 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) => {
commit(types.SET_REGION, payload);
};
@@ -64,3 +109,17 @@ export const setSecurityGroup = ({ commit }, payload) => {
export const setGitlabManagedCluster = ({ commit }, 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)));
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/index.js b/app/assets/javascripts/create_cluster/eks_cluster/store/index.js
index 22cca5b816e..5982fc8a2fd 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/store/index.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/store/index.js
@@ -6,10 +6,12 @@ import state from './state';
import clusterDropdownStore from './cluster_dropdown';
-import * as awsServices from '../services/aws_services_facade';
+import awsServicesFactory from '../services/aws_services_facade';
-const createStore = ({ initialState }) =>
- new Vuex.Store({
+const createStore = ({ initialState, apiPaths }) => {
+ const awsServices = awsServicesFactory(apiPaths);
+
+ return new Vuex.Store({
actions,
getters,
mutations,
@@ -39,7 +41,12 @@ const createStore = ({ initialState }) =>
namespaced: true,
...clusterDropdownStore(awsServices.fetchSecurityGroups),
},
+ instanceTypes: {
+ namespaced: true,
+ ...clusterDropdownStore(awsServices.fetchInstanceTypes),
+ },
},
});
+};
export default createStore;
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/mutation_types.js b/app/assets/javascripts/create_cluster/eks_cluster/store/mutation_types.js
index 398b48d725f..f9204cc2207 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/store/mutation_types.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/store/mutation_types.js
@@ -7,7 +7,13 @@ export const SET_KEY_PAIR = 'SET_KEY_PAIR';
export const SET_SUBNET = 'SET_SUBNET';
export const SET_ROLE = 'SET_ROLE';
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 REQUEST_CREATE_ROLE = 'REQUEST_CREATE_ROLE';
export const CREATE_ROLE_SUCCESS = 'CREATE_ROLE_SUCCESS';
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';
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/mutations.js b/app/assets/javascripts/create_cluster/eks_cluster/store/mutations.js
index f7752a23574..aa04c8f7079 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/store/mutations.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/store/mutations.js
@@ -28,6 +28,12 @@ export default {
[types.SET_SECURITY_GROUP](state, { 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 }) {
state.gitlabManagedCluster = gitlabManagedCluster;
},
@@ -46,4 +52,15 @@ export default {
state.createRoleError = error;
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;
+ },
};
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/state.js b/app/assets/javascripts/create_cluster/eks_cluster/store/state.js
index b69ae5b51e5..2e3a05a9187 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/store/state.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/store/state.js
@@ -1,5 +1,7 @@
import { KUBERNETES_VERSIONS } from '../constants';
+const [{ value: kubernetesVersion }] = KUBERNETES_VERSIONS;
+
export default () => ({
createRolePath: null,
@@ -12,13 +14,18 @@ export default () => ({
clusterName: '',
environmentScope: '*',
- kubernetesVersion: [KUBERNETES_VERSIONS].value,
+ kubernetesVersion,
selectedRegion: '',
selectedRole: '',
selectedKeyPair: '',
selectedVpc: '',
selectedSubnet: '',
selectedSecurityGroup: '',
+ selectedInstanceType: 'm5.large',
+ nodeCount: '3',
+
+ isCreatingCluster: false,
+ createClusterError: false,
gitlabManagedCluster: true,
});
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 6b27db23315..cfad274f91d 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -94,8 +94,7 @@
- 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 :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') }, class: 'input-md'
+ = f.select :role, ::User.roles.keys.map { |role| [role.titleize, role] }, { prompt: _('Select your role') }, required: true, class: 'input-md'
= render_if_exists 'profiles/email_settings', form: f
= f.text_field :skype, class: 'input-md', placeholder: s_("Profiles|username")
diff --git a/changelogs/unreleased/18986-allow-to-use-commit-sha-in-cache-key.yml b/changelogs/unreleased/18986-allow-to-use-commit-sha-in-cache-key.yml
new file mode 100644
index 00000000000..3e08b80282a
--- /dev/null
+++ b/changelogs/unreleased/18986-allow-to-use-commit-sha-in-cache-key.yml
@@ -0,0 +1,5 @@
+---
+title: Build CI cache key from commit SHAs that changed given files
+merge_request: 19392
+author:
+type: added
diff --git a/changelogs/unreleased/22392-eks-create-cluster-fe.yml b/changelogs/unreleased/22392-eks-create-cluster-fe.yml
new file mode 100644
index 00000000000..133154de03f
--- /dev/null
+++ b/changelogs/unreleased/22392-eks-create-cluster-fe.yml
@@ -0,0 +1,5 @@
+---
+title: Create AWS EKS cluster
+merge_request: 19578
+author:
+type: added
diff --git a/changelogs/unreleased/growth-91-when-users-update-their-profile-do-not-let-them-complete-the-up.yml b/changelogs/unreleased/growth-91-when-users-update-their-profile-do-not-let-them-complete-the-up.yml
new file mode 100644
index 00000000000..739d865b516
--- /dev/null
+++ b/changelogs/unreleased/growth-91-when-users-update-their-profile-do-not-let-them-complete-the-up.yml
@@ -0,0 +1,5 @@
+---
+title: Make role required when editing profile
+merge_request: 19636
+author:
+type: changed
diff --git a/doc/api/packages.md b/doc/api/packages.md
index 52cc1d5c97e..bab3f91bc40 100644
--- a/doc/api/packages.md
+++ b/doc/api/packages.md
@@ -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`. |
```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:
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index ea67617ff5c..27ced0eecf5 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -1535,6 +1535,50 @@ cache:
- 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`
Set `untracked: true` to cache all files that are untracked in your Git
diff --git a/doc/development/feature_flags/development.md b/doc/development/feature_flags/development.md
index de8098434c2..1ce9525d74e 100644
--- a/doc/development/feature_flags/development.md
+++ b/doc/development/feature_flags/development.md
@@ -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
[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,
-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,
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):
-
-```
-/chatops run feature set some_feature 0
-```
-
-Note that you can do this at any time, even before the merge request using the
-flag has been merged!
+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
+to `true`.
## Feature groups
diff --git a/lib/gitlab/ci/config/entry/files.rb b/lib/gitlab/ci/config/entry/files.rb
new file mode 100644
index 00000000000..d0d6a36d754
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/files.rb
@@ -0,0 +1,26 @@
+# 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
diff --git a/lib/gitlab/ci/config/entry/key.rb b/lib/gitlab/ci/config/entry/key.rb
index 0c10967e629..f12f0919348 100644
--- a/lib/gitlab/ci/config/entry/key.rb
+++ b/lib/gitlab/ci/config/entry/key.rb
@@ -7,11 +7,48 @@ module Gitlab
##
# Entry that represents a key.
#
- class Key < ::Gitlab::Config::Entry::Node
- include ::Gitlab::Config::Entry::Validatable
+ class Key < ::Gitlab::Config::Entry::Simplifiable
+ strategy :SimpleKey, if: -> (config) { config.is_a?(String) || config.is_a?(Symbol) }
+ strategy :ComplexKey, if: -> (config) { config.is_a?(Hash) }
- validations do
- validates :config, key: true
+ class SimpleKey < ::Gitlab::Config::Entry::Node
+ 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
def self.default
diff --git a/lib/gitlab/ci/config/entry/prefix.rb b/lib/gitlab/ci/config/entry/prefix.rb
new file mode 100644
index 00000000000..3244ad6d611
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/prefix.rb
@@ -0,0 +1,20 @@
+# 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
diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb
index fc9c540088b..1d698a32ba8 100644
--- a/lib/gitlab/ci/pipeline/seed/build.rb
+++ b/lib/gitlab/ci/pipeline/seed/build.rb
@@ -29,6 +29,8 @@ module Gitlab
.fabricate(attributes.delete(:except))
@rules = Gitlab::Ci::Build::Rules
.new(attributes.delete(:rules))
+ @cache = Seed::Build::Cache
+ .new(pipeline, attributes.delete(:cache))
end
def name
@@ -59,6 +61,7 @@ module Gitlab
@seed_attributes
.deep_merge(pipeline_attributes)
.deep_merge(rules_attributes)
+ .deep_merge(cache_attributes)
end
def bridge?
@@ -150,6 +153,12 @@ module Gitlab
@using_rules ? @rules.evaluate(@pipeline, self).build_attributes : {}
end
end
+
+ def cache_attributes
+ strong_memoize(:cache_attributes) do
+ @cache.build_attributes
+ end
+ end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/seed/build/cache.rb b/lib/gitlab/ci/pipeline/seed/build/cache.rb
new file mode 100644
index 00000000000..7671035b896
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/seed/build/cache.rb
@@ -0,0 +1,77 @@
+# 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
diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb
index c2a55fa8b1b..f48ffa9c1f7 100644
--- a/lib/gitlab/ci/yaml_processor.rb
+++ b/lib/gitlab/ci/yaml_processor.rb
@@ -43,11 +43,11 @@ module Gitlab
needs_attributes: job.dig(:needs, :job),
interruptible: job[:interruptible],
rules: job[:rules],
+ cache: job[:cache],
options: {
image: job[:image],
services: job[:services],
artifacts: job[:artifacts],
- cache: job[:cache],
dependencies: job[:dependencies],
job_timeout: job[:timeout],
before_script: job[:before_script],
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 1e5a9ecf360..93bde33b8f9 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -3547,10 +3547,13 @@ msgstr ""
msgid "ClusterIntegration|Choose a prefix to be used for your namespaces. Defaults to your project path."
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 ""
-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 ""
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 ""
msgid "ClusterIntegration|Could not load VPCs for the selected region"
msgstr ""
+msgid "ClusterIntegration|Could not load instance types"
+msgstr ""
+
msgid "ClusterIntegration|Could not load regions from your AWS account"
msgstr ""
@@ -3634,6 +3640,9 @@ msgstr ""
msgid "ClusterIntegration|Create new Cluster on GKE"
msgstr ""
+msgid "ClusterIntegration|Creating Kubernetes cluster"
+msgstr ""
+
msgid "ClusterIntegration|Did you know?"
msgstr ""
@@ -3742,6 +3751,9 @@ msgstr ""
msgid "ClusterIntegration|Instance cluster"
msgstr ""
+msgid "ClusterIntegration|Instance type"
+msgstr ""
+
msgid "ClusterIntegration|Integrate Kubernetes cluster automation"
msgstr ""
@@ -3817,7 +3829,7 @@ msgstr ""
msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}."
msgstr ""
-msgid "ClusterIntegration|Learn more about %{startLink}Regions%{endLink}."
+msgid "ClusterIntegration|Learn more about %{startLink}Regions %{externalLinkIcon}%{endLink}."
msgstr ""
msgid "ClusterIntegration|Learn more about Kubernetes"
@@ -3844,6 +3856,9 @@ msgstr ""
msgid "ClusterIntegration|Loading VPCs"
msgstr ""
+msgid "ClusterIntegration|Loading instance types"
+msgstr ""
+
msgid "ClusterIntegration|Loading security groups"
msgstr ""
@@ -3868,6 +3883,9 @@ msgstr ""
msgid "ClusterIntegration|No VPCs found"
msgstr ""
+msgid "ClusterIntegration|No instance type found"
+msgstr ""
+
msgid "ClusterIntegration|No machine types matched your search"
msgstr ""
@@ -3964,6 +3982,9 @@ msgstr ""
msgid "ClusterIntegration|Search VPCs"
msgstr ""
+msgid "ClusterIntegration|Search instance types"
+msgstr ""
+
msgid "ClusterIntegration|Search machine types"
msgstr ""
@@ -3982,7 +4003,7 @@ msgstr ""
msgid "ClusterIntegration|Search zones"
msgstr ""
-msgid "ClusterIntegration|Security groups"
+msgid "ClusterIntegration|Security group"
msgstr ""
msgid "ClusterIntegration|See and edit the details for your Kubernetes cluster"
@@ -3994,7 +4015,10 @@ msgstr ""
msgid "ClusterIntegration|Select a VPC to choose a subnet"
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 ""
msgid "ClusterIntegration|Select a region to choose a Key Pair"
@@ -4015,10 +4039,10 @@ msgstr ""
msgid "ClusterIntegration|Select project to choose zone"
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 ""
-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 ""
msgid "ClusterIntegration|Select zone"
@@ -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."
msgstr ""
-msgid "ClusterIntegration|Subnet"
+msgid "ClusterIntegration|Subnets"
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}"
@@ -4174,6 +4198,9 @@ msgstr ""
msgid "ClusterIntergation|Select a subnet"
msgstr ""
+msgid "ClusterIntergation|Select an instance type"
+msgstr ""
+
msgid "ClusterIntergation|Select key pair"
msgstr ""
diff --git a/spec/features/profiles/user_edit_profile_spec.rb b/spec/features/profiles/user_edit_profile_spec.rb
index 23f660d111a..9839b3d6c80 100644
--- a/spec/features/profiles/user_edit_profile_spec.rb
+++ b/spec/features/profiles/user_edit_profile_spec.rb
@@ -23,6 +23,7 @@ describe 'User edit profile' do
fill_in 'user_location', with: 'Ukraine'
fill_in 'user_bio', with: 'I <3 GitLab'
fill_in 'user_organization', with: 'GitLab'
+ select 'Data Analyst', from: 'user_role'
submit_settings
expect(user.reload).to have_attributes(
@@ -31,7 +32,8 @@ describe 'User edit profile' do
twitter: 'testtwitter',
website_url: 'testurl',
bio: 'I <3 GitLab',
- organization: 'GitLab'
+ organization: 'GitLab',
+ role: 'data_analyst'
)
expect(find('#user_location').value).to eq 'Ukraine'
@@ -66,34 +68,6 @@ describe 'User edit profile' do
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
before do
attach_file(:user_avatar, Rails.root.join('spec', 'fixtures', 'banana_sample.gif'))
diff --git a/spec/frontend/create_cluster/eks_cluster/components/cluster_form_dropdown_spec.js b/spec/frontend/create_cluster/eks_cluster/components/cluster_form_dropdown_spec.js
index 366c2fc7b26..efbe2635fcc 100644
--- a/spec/frontend/create_cluster/eks_cluster/components/cluster_form_dropdown_spec.js
+++ b/spec/frontend/create_cluster/eks_cluster/components/cluster_form_dropdown_spec.js
@@ -3,7 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import ClusterFormDropdown from '~/create_cluster/eks_cluster/components/cluster_form_dropdown.vue';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.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', () => {
let vm;
@@ -41,24 +41,50 @@ describe('ClusterFormDropdown', () => {
.trigger('click');
});
- it('displays selected item label', () => {
- expect(vm.find(DropdownButton).props('toggleText')).toEqual(secondItem.name);
+ it('emits input event with selected item', () => {
+ 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', () => {
- expect(vm.find(DropdownHiddenInput).props('value')).toEqual(secondItem.value);
+ it('displays a checked GlIcon next to the item', () => {
+ 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', () => {
it('displays selected item custom label', () => {
const labelProperty = 'customLabel';
- const selectedItem = { [labelProperty]: 'Name' };
+ const label = 'Name';
+ const currentValue = 1;
+ const customLabelItems = [{ [labelProperty]: label, value: currentValue }];
- vm.setProps({ labelProperty });
- vm.setData({ selectedItem });
+ vm.setProps({ labelProperty, items: customLabelItems, value: currentValue });
- expect(vm.find(DropdownButton).props('toggleText')).toEqual(selectedItem[labelProperty]);
+ expect(vm.find(DropdownButton).props('toggleText')).toEqual(label);
});
});
diff --git a/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js b/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js
index 69290f6dfa9..25d613d64ed 100644
--- a/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js
+++ b/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js
@@ -4,7 +4,6 @@ import Vue from 'vue';
import { GlFormCheckbox } from '@gitlab/ui';
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 clusterDropdownStoreState from '~/create_cluster/eks_cluster/store/cluster_dropdown/state';
@@ -21,17 +20,21 @@ describe('EksClusterConfigurationForm', () => {
let subnetsState;
let keyPairsState;
let securityGroupsState;
+ let instanceTypesState;
let vpcsActions;
let rolesActions;
let regionsActions;
let subnetsActions;
let keyPairsActions;
let securityGroupsActions;
+ let instanceTypesActions;
let vm;
beforeEach(() => {
state = eksClusterFormState();
actions = {
+ signOut: jest.fn(),
+ createCluster: jest.fn(),
setClusterName: jest.fn(),
setEnvironmentScope: jest.fn(),
setKubernetesVersion: jest.fn(),
@@ -41,6 +44,8 @@ describe('EksClusterConfigurationForm', () => {
setRole: jest.fn(),
setKeyPair: jest.fn(),
setSecurityGroup: jest.fn(),
+ setInstanceType: jest.fn(),
+ setNodeCount: jest.fn(),
setGitlabManagedCluster: jest.fn(),
};
regionsActions = {
@@ -61,6 +66,9 @@ describe('EksClusterConfigurationForm', () => {
securityGroupsActions = {
fetchItems: jest.fn(),
};
+ instanceTypesActions = {
+ fetchItems: jest.fn(),
+ };
rolesState = {
...clusterDropdownStoreState(),
};
@@ -79,6 +87,9 @@ describe('EksClusterConfigurationForm', () => {
securityGroupsState = {
...clusterDropdownStoreState(),
};
+ instanceTypesState = {
+ ...clusterDropdownStoreState(),
+ };
store = new Vuex.Store({
state,
actions,
@@ -113,6 +124,11 @@ describe('EksClusterConfigurationForm', () => {
state: securityGroupsState,
actions: securityGroupsActions,
},
+ instanceTypes: {
+ namespaced: true,
+ state: instanceTypesState,
+ actions: instanceTypesActions,
+ },
},
});
});
@@ -124,6 +140,7 @@ describe('EksClusterConfigurationForm', () => {
propsData: {
gitlabManagedClusterHelpPath: '',
kubernetesIntegrationHelpPath: '',
+ externalLinkIcon: '',
},
});
});
@@ -132,15 +149,34 @@ describe('EksClusterConfigurationForm', () => {
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 findEnvironmentScopeInput = () => vm.find('[id=eks-environment-scope]');
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 findVpcDropdown = () => vm.find('[field-id="eks-vpc"]');
const findSubnetDropdown = () => vm.find('[field-id="eks-subnet"]');
const findRoleDropdown = () => vm.find('[field-id="eks-role"]');
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);
describe('when mounted', () => {
@@ -151,6 +187,15 @@ describe('EksClusterConfigurationForm', () => {
it('fetches available roles', () => {
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', () => {
@@ -180,11 +225,13 @@ describe('EksClusterConfigurationForm', () => {
});
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', () => {
- 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', () => {
@@ -329,6 +376,34 @@ describe('EksClusterConfigurationForm', () => {
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', () => {
@@ -381,8 +456,10 @@ describe('EksClusterConfigurationForm', () => {
describe('when vpc is selected', () => {
const vpc = { name: 'vpc-1' };
+ const region = 'east-1';
beforeEach(() => {
+ state.selectedRegion = region;
findVpcDropdown().vm.$emit('input', vpc);
});
@@ -390,14 +467,34 @@ describe('EksClusterConfigurationForm', () => {
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', () => {
- expect(subnetsActions.fetchItems).toHaveBeenCalledWith(expect.anything(), { vpc }, undefined);
+ expect(subnetsActions.fetchItems).toHaveBeenCalledWith(
+ expect.anything(),
+ { vpc, region },
+ undefined,
+ );
});
it('dispatches fetchSecurityGroups action', () => {
expect(securityGroupsActions.fetchItems).toHaveBeenCalledWith(
expect.anything(),
- { vpc },
+ { vpc, region },
undefined,
);
});
@@ -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();
+ });
+ });
});
diff --git a/spec/frontend/create_cluster/eks_cluster/components/region_dropdown_spec.js b/spec/frontend/create_cluster/eks_cluster/components/region_dropdown_spec.js
deleted file mode 100644
index 0ebb5026a4b..00000000000
--- a/spec/frontend/create_cluster/eks_cluster/components/region_dropdown_spec.js
+++ /dev/null
@@ -1,55 +0,0 @@
-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',
- );
- });
-});
diff --git a/spec/frontend/create_cluster/eks_cluster/services/aws_services_facade_spec.js b/spec/frontend/create_cluster/eks_cluster/services/aws_services_facade_spec.js
new file mode 100644
index 00000000000..25be858dcb3
--- /dev/null
+++ b/spec/frontend/create_cluster/eks_cluster/services/aws_services_facade_spec.js
@@ -0,0 +1,152 @@
+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);
+ });
+ });
+});
diff --git a/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js b/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js
index 99c8cdba296..cf6c317a2df 100644
--- a/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js
+++ b/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js
@@ -13,12 +13,20 @@ import {
SET_ROLE,
SET_SECURITY_GROUP,
SET_GITLAB_MANAGED_CLUSTER,
+ SET_INSTANCE_TYPE,
+ SET_NODE_COUNT,
REQUEST_CREATE_ROLE,
CREATE_ROLE_SUCCESS,
CREATE_ROLE_ERROR,
+ REQUEST_CREATE_CLUSTER,
+ CREATE_CLUSTER_ERROR,
+ SIGN_OUT,
} from '~/create_cluster/eks_cluster/store/mutation_types';
import axios from '~/lib/utils/axios_utils';
import MockAdapter from 'axios-mock-adapter';
+import createFlash from '~/flash';
+
+jest.mock('~/flash');
describe('EKS Cluster Store Actions', () => {
let clusterName;
@@ -30,25 +38,34 @@ describe('EKS Cluster Store Actions', () => {
let role;
let keyPair;
let securityGroup;
+ let instanceType;
+ let nodeCount;
let gitlabManagedCluster;
let mock;
let state;
+ let newClusterUrl;
beforeEach(() => {
clusterName = 'my cluster';
environmentScope = 'production';
kubernetesVersion = '11.1';
- region = { name: 'regions-1' };
- vpc = { name: 'vpc-1' };
- subnet = { name: 'subnet-1' };
- role = { name: 'role-1' };
- keyPair = { name: 'key-pair-1' };
- securityGroup = { name: 'default group' };
+ region = 'regions-1';
+ vpc = 'vpc-1';
+ subnet = 'subnet-1';
+ role = 'role-1';
+ keyPair = 'key-pair-1';
+ securityGroup = 'default group';
+ instanceType = 'small-1';
+ nodeCount = '5';
gitlabManagedCluster = true;
+ newClusterUrl = '/clusters/1';
+
state = {
...createState(),
createRolePath: '/clusters/roles/',
+ signOutPath: '/aws/signout',
+ createClusterPath: '/clusters/',
};
});
@@ -71,6 +88,8 @@ describe('EKS Cluster Store Actions', () => {
${'setVpc'} | ${SET_VPC} | ${{ vpc }} | ${'vpc'}
${'setSubnet'} | ${SET_SUBNET} | ${{ subnet }} | ${'subnet'}
${'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'}
`(`$action commits $mutation with $payloadDescription payload`, data => {
const { action, mutation, payload } = data;
@@ -149,4 +168,127 @@ describe('EKS Cluster Store Actions', () => {
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 }]);
+ });
+ });
});
diff --git a/spec/frontend/create_cluster/eks_cluster/store/mutations_spec.js b/spec/frontend/create_cluster/eks_cluster/store/mutations_spec.js
index 2637b4822a5..0fb392f5eea 100644
--- a/spec/frontend/create_cluster/eks_cluster/store/mutations_spec.js
+++ b/spec/frontend/create_cluster/eks_cluster/store/mutations_spec.js
@@ -8,10 +8,15 @@ import {
SET_SUBNET,
SET_ROLE,
SET_SECURITY_GROUP,
+ SET_INSTANCE_TYPE,
+ SET_NODE_COUNT,
SET_GITLAB_MANAGED_CLUSTER,
REQUEST_CREATE_ROLE,
CREATE_ROLE_SUCCESS,
CREATE_ROLE_ERROR,
+ REQUEST_CREATE_CLUSTER,
+ CREATE_CLUSTER_ERROR,
+ SIGN_OUT,
} from '~/create_cluster/eks_cluster/store/mutation_types';
import createState from '~/create_cluster/eks_cluster/store/state';
import mutations from '~/create_cluster/eks_cluster/store/mutations';
@@ -27,6 +32,8 @@ describe('Create EKS cluster store mutations', () => {
let role;
let keyPair;
let securityGroup;
+ let instanceType;
+ let nodeCount;
let gitlabManagedCluster;
beforeEach(() => {
@@ -39,6 +46,8 @@ describe('Create EKS cluster store mutations', () => {
role = { name: 'role-1' };
keyPair = { name: 'key pair' };
securityGroup = { name: 'default group' };
+ instanceType = 'small-1';
+ nodeCount = '5';
gitlabManagedCluster = false;
state = createState();
@@ -53,8 +62,10 @@ describe('Create EKS cluster store mutations', () => {
${SET_REGION} | ${'selectedRegion'} | ${{ region }} | ${region} | ${'selected region payload'}
${SET_KEY_PAIR} | ${'selectedKeyPair'} | ${{ keyPair }} | ${keyPair} | ${'selected key pair 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_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'}
`(`$mutation sets $mutatedProperty to $expectedValueDescription`, data => {
const { mutation, mutatedProperty, payload, expectedValue } = data;
@@ -118,4 +129,45 @@ describe('Create EKS cluster store mutations', () => {
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);
+ });
+ });
});
diff --git a/spec/lib/gitlab/ci/config/entry/cache_spec.rb b/spec/lib/gitlab/ci/config/entry/cache_spec.rb
index 9aab3664e1c..4fa0a57dc82 100644
--- a/spec/lib/gitlab/ci/config/entry/cache_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/cache_spec.rb
@@ -12,22 +12,53 @@ describe Gitlab::Ci::Config::Entry::Cache do
context 'when entry config value is correct' do
let(:policy) { nil }
+ let(:key) { 'some key' }
let(:config) do
- { key: 'some key',
+ { key: key,
untracked: true,
paths: ['some/path/'],
policy: policy }
end
describe '#value' do
- it 'returns hash value' do
- expect(entry.value).to eq(key: 'some key', untracked: true, paths: ['some/path/'], policy: 'pull-push')
+ shared_examples 'hash key value' do
+ 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
describe '#valid?' do
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
context 'policy is pull-push' do
@@ -87,10 +118,44 @@ describe Gitlab::Ci::Config::Entry::Cache do
end
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
- is_expected.to include 'key config should be a string or symbol'
+ it 'reports error with descendants' do
+ 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
diff --git a/spec/lib/gitlab/ci/config/entry/files_spec.rb b/spec/lib/gitlab/ci/config/entry/files_spec.rb
new file mode 100644
index 00000000000..2bebbd7b198
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/files_spec.rb
@@ -0,0 +1,54 @@
+# 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
diff --git a/spec/lib/gitlab/ci/config/entry/key_spec.rb b/spec/lib/gitlab/ci/config/entry/key_spec.rb
index a7874447725..327607e2266 100644
--- a/spec/lib/gitlab/ci/config/entry/key_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/key_spec.rb
@@ -6,38 +6,38 @@ describe Gitlab::Ci::Config::Entry::Key do
let(:entry) { described_class.new(config) }
describe 'validations' do
- shared_examples 'key with slash' do
- it 'is invalid' do
- expect(entry).not_to be_valid
- end
+ it_behaves_like 'key entry validations', 'simple key'
- it 'reports errors with config value' do
- expect(entry.errors).to include 'key config cannot contain the "/" character'
- end
- end
+ context 'when entry config value is correct' do
+ context 'when key is a hash' do
+ let(:config) { { files: ['test'], prefix: 'something' } }
- shared_examples 'key with only dots' do
- it 'is invalid' do
- expect(entry).not_to be_valid
- end
+ describe '#value' do
+ it 'returns key value' do
+ expect(entry.value).to match(config)
+ end
+ end
- it 'reports errors with config value' do
- expect(entry.errors).to include 'key config cannot be "." or ".."'
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
end
- end
- context 'when entry config value is correct' do
- let(:config) { 'test' }
+ context 'when key is a symbol' do
+ let(:config) { :key }
- describe '#value' do
- it 'returns key value' do
- expect(entry.value).to eq 'test'
+ describe '#value' do
+ it 'returns key value' do
+ expect(entry.value).to eq(config.to_s)
+ end
end
- end
- describe '#valid?' do
- it 'is valid' do
- expect(entry).to be_valid
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
end
end
end
@@ -47,53 +47,11 @@ describe Gitlab::Ci::Config::Entry::Key do
describe '#errors' do
it 'saves errors' do
- expect(entry.errors)
- .to include 'key config should be a string or symbol'
+ expect(entry.errors.first)
+ .to match /should be a hash, a string or a symbol/
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
describe '.default' do
diff --git a/spec/lib/gitlab/ci/config/entry/prefix_spec.rb b/spec/lib/gitlab/ci/config/entry/prefix_spec.rb
new file mode 100644
index 00000000000..8132a674488
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/prefix_spec.rb
@@ -0,0 +1,28 @@
+# 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
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb
new file mode 100644
index 00000000000..6a8b804597c
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb
@@ -0,0 +1,261 @@
+# 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
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
index 945baf47b7b..62e5fd566f7 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
@@ -4,7 +4,8 @@ require 'spec_helper'
describe Gitlab::Ci::Pipeline::Seed::Build do
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(:previous_stages) { [] }
@@ -69,6 +70,101 @@ describe Gitlab::Ci::Pipeline::Seed::Build do
it { is_expected.to include(when: 'never') }
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
describe '#bridge?' do
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index 5a173470dfe..35a4749922e 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -950,7 +950,7 @@ module Gitlab
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[:options][:cache]).to eq(
+ expect(config_processor.stage_builds_attributes("test").first[:cache]).to eq(
paths: ["logs/", "binaries/"],
untracked: true,
key: 'key',
@@ -962,7 +962,7 @@ module Gitlab
config = YAML.dump(
{
default: {
- cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' }
+ cache: { paths: ["logs/", "binaries/"], untracked: true, key: { files: ['file'] } }
},
rspec: {
script: "rspec"
@@ -972,33 +972,79 @@ module Gitlab
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[:options][:cache]).to eq(
+ expect(config_processor.stage_builds_attributes("test").first[:cache]).to eq(
paths: ["logs/", "binaries/"],
untracked: true,
- key: 'key',
+ key: { files: ['file'] },
policy: 'pull-push'
)
end
- it "returns cache when defined in a job" do
+ it 'returns cache key when defined in a job' do
config = YAML.dump({
rspec: {
- cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' },
- script: "rspec"
+ cache: { paths: ['logs/', 'binaries/'], untracked: true, key: 'key' },
+ 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[:options][:cache]).to eq(
- paths: ["logs/", "binaries/"],
+ 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: 'key',
policy: 'pull-push'
)
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
config = YAML.dump({
cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'global' },
@@ -1011,7 +1057,7 @@ module Gitlab
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[:options][:cache]).to eq(
+ expect(config_processor.stage_builds_attributes("test").first[:cache]).to eq(
paths: ["test/"],
untracked: false,
key: 'local',
@@ -1862,14 +1908,42 @@ module Gitlab
config = YAML.dump({ cache: { key: 1 }, rspec: { script: "test" } })
expect do
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
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 } } })
expect do
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
it "returns errors if job cache:untracked is not an array of strings" do
diff --git a/spec/lib/gitlab/external_authorization/access_spec.rb b/spec/lib/gitlab/external_authorization/access_spec.rb
index 5dc2521b310..8a08b2a6275 100644
--- a/spec/lib/gitlab/external_authorization/access_spec.rb
+++ b/spec/lib/gitlab/external_authorization/access_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::ExternalAuthorization::Access, :clean_gitlab_redis_cache do
diff --git a/spec/lib/gitlab/external_authorization/cache_spec.rb b/spec/lib/gitlab/external_authorization/cache_spec.rb
index 58e7d626707..1f217249f97 100644
--- a/spec/lib/gitlab/external_authorization/cache_spec.rb
+++ b/spec/lib/gitlab/external_authorization/cache_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::ExternalAuthorization::Cache, :clean_gitlab_redis_cache do
diff --git a/spec/lib/gitlab/external_authorization/client_spec.rb b/spec/lib/gitlab/external_authorization/client_spec.rb
index a87f50b4586..a17d933e3bb 100644
--- a/spec/lib/gitlab/external_authorization/client_spec.rb
+++ b/spec/lib/gitlab/external_authorization/client_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::ExternalAuthorization::Client do
diff --git a/spec/lib/gitlab/external_authorization/logger_spec.rb b/spec/lib/gitlab/external_authorization/logger_spec.rb
index 81f1b2390e6..380e765309c 100644
--- a/spec/lib/gitlab/external_authorization/logger_spec.rb
+++ b/spec/lib/gitlab/external_authorization/logger_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::ExternalAuthorization::Logger do
diff --git a/spec/lib/gitlab/external_authorization/response_spec.rb b/spec/lib/gitlab/external_authorization/response_spec.rb
index 43211043eca..e1f6e9ac1fa 100644
--- a/spec/lib/gitlab/external_authorization/response_spec.rb
+++ b/spec/lib/gitlab/external_authorization/response_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::ExternalAuthorization::Response do
diff --git a/spec/services/ci/create_pipeline_service/cache_spec.rb b/spec/services/ci/create_pipeline_service/cache_spec.rb
new file mode 100644
index 00000000000..4e0567132ff
--- /dev/null
+++ b/spec/services/ci/create_pipeline_service/cache_spec.rb
@@ -0,0 +1,168 @@
+# 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
diff --git a/spec/support/shared_examples/lib/gitlab/ci/config/entry/key_validations_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/ci/config/entry/key_validations_shared_examples.rb
new file mode 100644
index 00000000000..b0b3e46332d
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/ci/config/entry/key_validations_shared_examples.rb
@@ -0,0 +1,81 @@
+# 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