Commit eed996ac authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent b42f312d
......@@ -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));
},
selectedItemLabel() {
return this.selectedItem && this.selectedItem[this.labelProperty];
return this.getItemsOrEmptyList().filter(this.searchFn(this.searchQuery, this.labelProperty));
},
selectedItemValue() {
return (this.selectedItem && this.selectedItem[this.valueProperty]) || '';
selectedItems() {
const valueProp = this.valueProperty;
const valueList = toArray(this.value);
const items = this.getItemsOrEmptyList();
return items.filter(item => valueList.some(value => item[valueProp] === value));
},
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>
......@@ -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
......
<script>
import { sprintf, s__ } from '~/locale';
import ClusterFormDropdown from './cluster_form_dropdown.vue';
export default {
components: {
ClusterFormDropdown,
},
props: {
regions: {
type: Array,
required: false,
default: () => [],
},
loading: {
type: Boolean,
required: false,
default: false,
},
error: {
type: Object,
required: false,
default: null,
},
},
computed: {
hasErrors() {
return Boolean(this.error);
},
helpText() {
return sprintf(
s__('ClusterIntegration|Learn more about %{startLink}Regions%{endLink}.'),
{
startLink:
'<a href="https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services/" target="_blank" rel="noopener noreferrer">',
endLink: '</a>',
},
false,
);
},
},
};
</script>
<template>
<div>
<cluster-form-dropdown
field-id="eks-region"
field-name="eks-region"
:items="regions"
:loading="loading"
:loading-text="s__('ClusterIntegration|Loading Regions')"
:placeholder="s__('ClusterIntergation|Select a region')"
:search-field-placeholder="s__('ClusterIntegration|Search regions')"
:empty-text="s__('ClusterIntegration|No region found')"
:has-errors="hasErrors"
:error-message="s__('ClusterIntegration|Could not load regions from your AWS account')"
v-bind="$attrs"
v-on="$listeners"
/>
<p class="form-text text-muted" v-html="helpText"></p>
</div>
</template>
......@@ -131,7 +131,7 @@ export default {
<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"
......
// 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' }];
......@@ -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: {
......
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],
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 })),
);
},
],
})
.promise()
.then(({ SecurityGroups: securityGroups }) =>
securityGroups.map(({ GroupName: name, GroupId: value }) => ({ name, value })),
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 })),
);
};
export default () => {};
},
fetchInstanceTypes() {
return axios
.get(apiPaths.getInstanceTypesPath)
.then(({ data: { instance_types: instanceTypes } }) =>
instanceTypes.map(({ instance_type_name }) => ({
name: instance_type_name,
value: instance_type_name,
})),
);
},
});
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)));
......@@ -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;
......@@ -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';
......@@ -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;
},
};
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,
});
......@@ -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")
......
---
title: Build CI cache key from commit SHAs that changed given files
merge_request: 19392
author:
type: added
---
title: Create AWS EKS cluster
merge_request: 19578
author:
type: added
---
title: Make role required when editing profile
merge_request: 19636
author:
type: changed
......@@ -61,7 +61,7 @@ GET /groups/:id/packages
| `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:
......
......@@ -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
......
......@@ -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
......
# frozen_string_literal: true
module Gitlab
module Ci
class Config
module Entry
##
# Entry that represents an array of file paths.
#
class Files < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, array_of_strings: true
validates :config, length: {
minimum: 1,
maximum: 2,
too_short: 'requires at least %{count} item',
too_long: 'has too many items (maximum is %{count})'
}
end
end
end
end
end
end
......@@ -7,7 +7,11 @@ module Gitlab
##
# Entry that represents a key.
#
class Key < ::Gitlab::Config::Entry::Node
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) }
class SimpleKey < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable
validations do
......@@ -17,6 +21,39 @@ module Gitlab
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
'default'
end
end
end
end
......
# frozen_string_literal: true
module Gitlab
module Ci
class Config
module Entry
##
# Entry that represents a key prefix.
#
class Prefix < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, key: true
end
end
end
end
end
end
......@@ -29,6 +29,8 @@ module Gitlab
.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
......
# frozen_string_literal: true
module Gitlab
module Ci
module Pipeline
module Seed
class Build
class Cache
def initialize(pipeline, cache)
@pipeline = pipeline
local_cache = cache.to_h.deep_dup
@key = local_cache.delete(:key)
@paths = local_cache.delete(:paths)
@policy = local_cache.delete(:policy)
@untracked = local_cache.delete(:untracked)
raise ArgumentError, "unknown cache keys: #{local_cache.keys}" if local_cache.any?
end
def build_attributes
{
options: {
cache: {
key: key_string,
paths: @paths,
policy: @policy,
untracked: @untracked
}.compact.presence
}.compact
}
end
private
def key_string
key_from_string || key_from_files
end
def key_from_string
@key.to_s if @key.is_a?(String) || @key.is_a?(Symbol)
end
def key_from_files
return unless @key.is_a?(Hash)
[@key[:prefix], files_digest].select(&:present?).join('-')
end
def files_digest
hash_of_the_latest_changes || 'default'
end
def hash_of_the_latest_changes
return unless Feature.enabled?(:ci_file_based_cache, @pipeline.project, default_enabled: true)
ids = files.map { |path| last_commit_id_for_path(path) }
ids = ids.compact.sort.uniq
Digest::SHA1.hexdigest(ids.join('-')) if ids.any?
end
def files
@key[:files]
.to_a
.select(&:present?)
.uniq
end
def last_commit_id_for_path(path)
@pipeline.project.repository.last_commit_id_for_path(@pipeline.sha, path)
end
end
end
end
end
end
end
......@@ -43,11 +43,11 @@ module Gitlab
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],
......
......@@ -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 ""
......
......@@ -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'))
......
......@@ -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);
});
});
......
......@@ -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();
});
});
});
import { shallowMount } from '@vue/test-utils';
import ClusterFormDropdown from '~/create_cluster/eks_cluster/components/cluster_form_dropdown.vue';
import RegionDropdown from '~/create_cluster/eks_cluster/components/region_dropdown.vue';
describe('RegionDropdown', () => {
let vm;
const getClusterFormDropdown = () => vm.find(ClusterFormDropdown);
beforeEach(() => {
vm = shallowMount(RegionDropdown);
});
afterEach(() => vm.destroy());
it('renders a cluster-form-dropdown', () => {
expect(getClusterFormDropdown().exists()).toBe(true);
});
it('sets regions to cluster-form-dropdown items property', () => {
const regions = [{ name: 'basic' }];
vm.setProps({ regions });
expect(getClusterFormDropdown().props('items')).toEqual(regions);
});
it('sets a loading text', () => {
expect(getClusterFormDropdown().props('loadingText')).toEqual('Loading Regions');
});
it('sets a placeholder', () => {
expect(getClusterFormDropdown().props('placeholder')).toEqual('Select a region');
});
it('sets an empty results text', () => {
expect(getClusterFormDropdown().props('emptyText')).toEqual('No region found');
});
it('sets a search field placeholder', () => {
expect(getClusterFormDropdown().props('searchFieldPlaceholder')).toEqual('Search regions');
});
it('sets hasErrors property', () => {
vm.setProps({ error: {} });
expect(getClusterFormDropdown().props('hasErrors')).toEqual(true);
});
it('sets an error message', () => {
expect(getClusterFormDropdown().props('errorMessage')).toEqual(
'Could not load regions from your AWS account',
);
});
});
import awsServicesFacadeFactory from '~/create_cluster/eks_cluster/services/aws_services_facade';
import axios from '~/lib/utils/axios_utils';
import AxiosMockAdapter from 'axios-mock-adapter';
describe('awsServicesFacade', () => {
let apiPaths;
let axiosMock;
let awsServices;
let region;
let vpc;
beforeEach(() => {
apiPaths = {
getKeyPairsPath: '/clusters/aws/api/key_pairs',
getRegionsPath: '/clusters/aws/api/regions',
getRolesPath: '/clusters/aws/api/roles',
getSecurityGroupsPath: '/clusters/aws/api/security_groups',
getSubnetsPath: '/clusters/aws/api/subnets',
getVpcsPath: '/clusters/aws/api/vpcs',
getInstanceTypesPath: '/clusters/aws/api/instance_types',
};
region = 'west-1';
vpc = 'vpc-2';
awsServices = awsServicesFacadeFactory(apiPaths);
axiosMock = new AxiosMockAdapter(axios);
});
describe('when fetchRegions succeeds', () => {
let regions;
let regionsOutput;
beforeEach(() => {
regions = [{ region_name: 'east-1' }, { region_name: 'west-2' }];
regionsOutput = regions.map(({ region_name: name }) => ({ name, value: name }));
axiosMock.onGet(apiPaths.getRegionsPath).reply(200, { regions });
});
it('return list of roles where each item has a name and value', () => {
expect(awsServices.fetchRegions()).resolves.toEqual(regionsOutput);
});
});
describe('when fetchRoles succeeds', () => {
let roles;
let rolesOutput;
beforeEach(() => {
roles = [
{ role_name: 'admin', arn: 'aws::admin' },
{ role_name: 'read-only', arn: 'aws::read-only' },
];
rolesOutput = roles.map(({ role_name: name, arn: value }) => ({ name, value }));
axiosMock.onGet(apiPaths.getRolesPath).reply(200, { roles });
});
it('return list of regions where each item has a name and value', () => {
expect(awsServices.fetchRoles()).resolves.toEqual(rolesOutput);
});
});
describe('when fetchKeyPairs succeeds', () => {
let keyPairs;
let keyPairsOutput;
beforeEach(() => {
keyPairs = [{ key_pair: 'key-pair' }, { key_pair: 'key-pair-2' }];
keyPairsOutput = keyPairs.map(({ key_name: name }) => ({ name, value: name }));
axiosMock
.onGet(apiPaths.getKeyPairsPath, { params: { region } })
.reply(200, { key_pairs: keyPairs });
});
it('return list of key pairs where each item has a name and value', () => {
expect(awsServices.fetchKeyPairs({ region })).resolves.toEqual(keyPairsOutput);
});
});
describe('when fetchVpcs succeeds', () => {
let vpcs;
let vpcsOutput;
beforeEach(() => {
vpcs = [{ vpc_id: 'vpc-1' }, { vpc_id: 'vpc-2' }];
vpcsOutput = vpcs.map(({ vpc_id: name }) => ({ name, value: name }));
axiosMock.onGet(apiPaths.getVpcsPath, { params: { region } }).reply(200, { vpcs });
});
it('return list of vpcs where each item has a name and value', () => {
expect(awsServices.fetchVpcs({ region })).resolves.toEqual(vpcsOutput);
});
});
describe('when fetchSubnets succeeds', () => {
let subnets;
let subnetsOutput;
beforeEach(() => {
subnets = [{ subnet_id: 'vpc-1' }, { subnet_id: 'vpc-2' }];
subnetsOutput = subnets.map(({ subnet_id }) => ({ name: subnet_id, value: subnet_id }));
axiosMock
.onGet(apiPaths.getSubnetsPath, { params: { region, vpc_id: vpc } })
.reply(200, { subnets });
});
it('return list of subnets where each item has a name and value', () => {
expect(awsServices.fetchSubnets({ region, vpc })).resolves.toEqual(subnetsOutput);
});
});
describe('when fetchSecurityGroups succeeds', () => {
let securityGroups;
let securityGroupsOutput;
beforeEach(() => {
securityGroups = [
{ group_name: 'admin group', group_id: 'group-1' },
{ group_name: 'basic group', group_id: 'group-2' },
];
securityGroupsOutput = securityGroups.map(({ group_id: value, group_name: name }) => ({
name,
value,
}));
axiosMock
.onGet(apiPaths.getSecurityGroupsPath, { params: { region, vpc_id: vpc } })
.reply(200, { security_groups: securityGroups });
});
it('return list of security groups where each item has a name and value', () => {
expect(awsServices.fetchSecurityGroups({ region, vpc })).resolves.toEqual(
securityGroupsOutput,
);
});
});
describe('when fetchInstanceTypes succeeds', () => {
let instanceTypes;
let instanceTypesOutput;
beforeEach(() => {
instanceTypes = [{ instance_type_name: 't2.small' }, { instance_type_name: 't2.medium' }];
instanceTypesOutput = instanceTypes.map(({ instance_type_name }) => ({
name: instance_type_name,
value: instance_type_name,
}));
axiosMock.onGet(apiPaths.getInstanceTypesPath).reply(200, { instance_types: instanceTypes });
});
it('return list of instance types where each item has a name and value', () => {
expect(awsServices.fetchInstanceTypes()).resolves.toEqual(instanceTypesOutput);
});
});
});
......@@ -13,12 +13,20 @@ import {
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 }]);
});
});
});
......@@ -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);
});
});
});
......@@ -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
shared_examples 'hash key value' do
it 'returns hash value' do
expect(entry.value).to eq(key: 'some key', untracked: true, paths: ['some/path/'], policy: 'pull-push')
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
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'
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
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Files do
let(:entry) { described_class.new(config) }
describe 'validations' do
context 'when entry config value is valid' do
let(:config) { ['some/file', 'some/path/'] }
describe '#value' do
it 'returns key value' do
expect(entry.value).to eq config
end
end
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
describe '#errors' do
context 'when entry value is not an array' do
let(:config) { 'string' }
it 'saves errors' do
expect(entry.errors)
.to include 'files config should be an array of strings'
end
end
context 'when entry value is not an array of strings' do
let(:config) { [1] }
it 'saves errors' do
expect(entry.errors)
.to include 'files config should be an array of strings'
end
end
context 'when entry value contains more than two values' do
let(:config) { %w[file1 file2 file3] }
it 'saves errors' do
expect(entry.errors)
.to include 'files config has too many items (maximum is 2)'
end
end
end
end
end
......@@ -6,32 +6,31 @@ 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'
context 'when entry config value is correct' do
context 'when key is a hash' do
let(:config) { { files: ['test'], prefix: 'something' } }
it 'reports errors with config value' do
expect(entry.errors).to include 'key config cannot contain the "/" character'
describe '#value' do
it 'returns key value' do
expect(entry.value).to match(config)
end
end
shared_examples 'key with only dots' do
it 'is invalid' do
expect(entry).not_to be_valid
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
it 'reports errors with config value' do
expect(entry.errors).to include 'key config cannot be "." or ".."'
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'
expect(entry.value).to eq(config.to_s)
end
end
......@@ -41,58 +40,17 @@ describe Gitlab::Ci::Config::Entry::Key do
end
end
end
end
context 'when entry value is not correct' do
let(:config) { ['incorrect'] }
describe '#errors' do
it 'saves errors' do
expect(entry.errors)
.to include 'key config should be a string or symbol'
end
end
end
context 'when entry value contains slash' do
let(:config) { 'key/with/some/slashes' }
it_behaves_like 'key with slash'
expect(entry.errors.first)
.to match /should be a hash, a string or a symbol/
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
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Prefix do
let(:entry) { described_class.new(config) }
describe 'validations' do
it_behaves_like 'key entry validations', :prefix
context 'when entry value is not correct' do
let(:config) { ['incorrect'] }
describe '#errors' do
it 'saves errors' do
expect(entry.errors)
.to include 'prefix config should be a string or symbol'
end
end
end
end
describe '.default' do
it 'returns default key' do
expect(described_class.default).to be_nil
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Pipeline::Seed::Build::Cache do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:head_sha) { project.repository.head_commit.id }
let_it_be(:pipeline) { create(:ci_pipeline, project: project, sha: head_sha) }
let(:processor) { described_class.new(pipeline, config) }
describe '#build_attributes' do
subject { processor.build_attributes }
context 'with cache:key' do
let(:config) do
{
key: 'a-key',
paths: ['vendor/ruby']
}
end
it { is_expected.to include(options: { cache: config }) }
end
context 'with cache:key as a symbol' do
let(:config) do
{
key: :a_key,
paths: ['vendor/ruby']
}
end
it { is_expected.to include(options: { cache: config.merge(key: "a_key") }) }
end
context 'with cache:key:files' do
shared_examples 'default key' do
let(:config) do
{ key: { files: files } }
end
it 'uses default key' do
expected = { options: { cache: { key: 'default' } } }
is_expected.to include(expected)
end
end
shared_examples 'version and gemfile files' do
let(:config) do
{
key: {
files: files
},
paths: ['vendor/ruby']
}
end
it 'builds a string key' do
expected = {
options: {
cache: {
key: '703ecc8fef1635427a1f86a8a1a308831c122392',
paths: ['vendor/ruby']
}
}
}
is_expected.to include(expected)
end
end
context 'with existing files' do
let(:files) { ['VERSION', 'Gemfile.zip'] }
it_behaves_like 'version and gemfile files'
end
context 'with files starting with ./' do
let(:files) { ['Gemfile.zip', './VERSION'] }
it_behaves_like 'version and gemfile files'
end
context 'with feature flag disabled' do
let(:files) { ['VERSION', 'Gemfile.zip'] }
before do
stub_feature_flags(ci_file_based_cache: false)
end
it_behaves_like 'default key'
end
context 'with files ending with /' do
let(:files) { ['Gemfile.zip/'] }
it_behaves_like 'default key'
end
context 'with new line in filenames' do
let(:files) { ["Gemfile.zip\nVERSION"] }
it_behaves_like 'default key'
end
context 'with missing files' do
let(:files) { ['project-gemfile.lock', ''] }
it_behaves_like 'default key'
end
context 'with directories' do
shared_examples 'foo/bar directory key' do
let(:config) do
{
key: {
files: files
}
}
end
it 'builds a string key' do
expected = {
options: {
cache: { key: '74bf43fb1090f161bdd4e265802775dbda2f03d1' }
}
}
is_expected.to include(expected)
end
end
context 'with directory' do
let(:files) { ['foo/bar'] }
it_behaves_like 'foo/bar directory key'
end
context 'with directory ending in slash' do
let(:files) { ['foo/bar/'] }
it_behaves_like 'foo/bar directory key'
end
context 'with directories ending in slash star' do
let(:files) { ['foo/bar/*'] }
it_behaves_like 'foo/bar directory key'
end
end
end
context 'with cache:key:prefix' do
context 'without files' do
let(:config) do
{
key: {
prefix: 'a-prefix'
},
paths: ['vendor/ruby']
}
end
it 'adds prefix to default key' do
expected = {
options: {
cache: {
key: 'a-prefix-default',
paths: ['vendor/ruby']
}
}
}
is_expected.to include(expected)
end
end
context 'with existing files' do
let(:config) do
{
key: {
files: ['VERSION', 'Gemfile.zip'],
prefix: 'a-prefix'
},
paths: ['vendor/ruby']
}
end
it 'adds prefix key' do
expected = {
options: {
cache: {
key: 'a-prefix-703ecc8fef1635427a1f86a8a1a308831c122392',
paths: ['vendor/ruby']
}
}
}
is_expected.to include(expected)
end
end
context 'with missing files' do
let(:config) do
{
key: {
files: ['project-gemfile.lock', ''],
prefix: 'a-prefix'
},
paths: ['vendor/ruby']
}
end
it 'adds prefix to default key' do
expected = {
options: {
cache: {
key: 'a-prefix-default',
paths: ['vendor/ruby']
}
}
}
is_expected.to include(expected)
end
end
end
context 'with all cache option keys' do
let(:config) do
{
key: 'a-key',
paths: ['vendor/ruby'],
untracked: true,
policy: 'push'
}
end
it { is_expected.to include(options: { cache: config }) }
end
context 'with unknown cache option keys' do
let(:config) do
{
key: 'a-key',
unknown_key: true
}
end
it { expect { subject }.to raise_error(ArgumentError, /unknown_key/) }
end
context 'with empty config' do
let(:config) { {} }
it { is_expected.to include(options: {}) }
end
end
end
......@@ -4,7 +4,8 @@ require 'spec_helper'
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
......
......@@ -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
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::ExternalAuthorization::Access, :clean_gitlab_redis_cache do
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::ExternalAuthorization::Cache, :clean_gitlab_redis_cache do
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::ExternalAuthorization::Client do
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::ExternalAuthorization::Logger do
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::ExternalAuthorization::Response do
......
# frozen_string_literal: true
require 'spec_helper'
describe Ci::CreatePipelineService do
context 'cache' do
let(:user) { create(:admin) }
let(:ref) { 'refs/heads/master' }
let(:source) { :push }
let(:service) { described_class.new(project, user, { ref: ref }) }
let(:pipeline) { service.execute(source) }
let(:job) { pipeline.builds.find_by(name: 'job') }
let(:project) { create(:project, :custom_repo, files: files) }
before do
stub_ci_pipeline_yaml_file(config)
end
context 'with cache:key' do
let(:files) { { 'some-file' => '' } }
let(:config) do
<<~EOY
job:
script:
- ls
cache:
key: 'a-key'
paths: ['logs/', 'binaries/']
untracked: true
EOY
end
it 'uses the provided key' do
expected = {
'key' => 'a-key',
'paths' => ['logs/', 'binaries/'],
'policy' => 'pull-push',
'untracked' => true
}
expect(pipeline).to be_persisted
expect(job.cache).to match(a_collection_including(expected))
end
end
context 'with cache:key:files' do
let(:config) do
<<~EOY
job:
script:
- ls
cache:
paths:
- logs/
key:
files:
- file.lock
- missing-file.lock
EOY
end
context 'when file.lock exists' do
let(:files) { { 'file.lock' => '' } }
it 'builds a cache key' do
expected = {
'key' => /[a-f0-9]{40}/,
'paths' => ['logs/'],
'policy' => 'pull-push'
}
expect(pipeline).to be_persisted
expect(job.cache).to match(a_collection_including(expected))
end
end
context 'when file.lock does not exist' do
let(:files) { { 'some-file' => '' } }
it 'uses default cache key' do
expected = {
'key' => /default/,
'paths' => ['logs/'],
'policy' => 'pull-push'
}
expect(pipeline).to be_persisted
expect(job.cache).to match(a_collection_including(expected))
end
end
end
context 'with cache:key:files and prefix' do
let(:config) do
<<~EOY
job:
script:
- ls
cache:
paths:
- logs/
key:
files:
- file.lock
prefix: '$ENV_VAR'
EOY
end
context 'when file.lock exists' do
let(:files) { { 'file.lock' => '' } }
it 'builds a cache key' do
expected = {
'key' => /\$ENV_VAR-[a-f0-9]{40}/,
'paths' => ['logs/'],
'policy' => 'pull-push'
}
expect(pipeline).to be_persisted
expect(job.cache).to match(a_collection_including(expected))
end
end
context 'when file.lock does not exist' do
let(:files) { { 'some-file' => '' } }
it 'uses default cache key' do
expected = {
'key' => /\$ENV_VAR-default/,
'paths' => ['logs/'],
'policy' => 'pull-push'
}
expect(pipeline).to be_persisted
expect(job.cache).to match(a_collection_including(expected))
end
end
end
context 'with too many files' do
let(:files) { { 'some-file' => '' } }
let(:config) do
<<~EOY
job:
script:
- ls
cache:
paths: ['logs/', 'binaries/']
untracked: true
key:
files:
- file.lock
- other-file.lock
- extra-file.lock
prefix: 'some-prefix'
EOY
end
it 'has errors' do
expect(pipeline).to be_persisted
expect(pipeline.yaml_errors).to eq("jobs:job:cache:key:files config has too many items (maximum is 2)")
expect(job).to be_nil
end
end
end
end
# frozen_string_literal: true
RSpec.shared_examples 'key entry validations' do |config_name|
shared_examples 'key with slash' do
it 'is invalid' do
expect(entry).not_to be_valid
end
it 'reports errors with config value' do
expect(entry.errors).to include "#{config_name} config cannot contain the \"/\" character"
end
end
shared_examples 'key with only dots' do
it 'is invalid' do
expect(entry).not_to be_valid
end
it 'reports errors with config value' do
expect(entry.errors).to include "#{config_name} config cannot be \".\" or \"..\""
end
end
context 'when entry value contains slash' do
let(:config) { 'key/with/some/slashes' }
it_behaves_like 'key with slash'
end
context 'when entry value contains URI encoded slash (%2F)' do
let(:config) { 'key%2Fwith%2Fsome%2Fslashes' }
it_behaves_like 'key with slash'
end
context 'when entry value is a dot' do
let(:config) { '.' }
it_behaves_like 'key with only dots'
end
context 'when entry value is two dots' do
let(:config) { '..' }
it_behaves_like 'key with only dots'
end
context 'when entry value is a URI encoded dot (%2E)' do
let(:config) { '%2e' }
it_behaves_like 'key with only dots'
end
context 'when entry value is two URI encoded dots (%2E)' do
let(:config) { '%2E%2e' }
it_behaves_like 'key with only dots'
end
context 'when entry value is one dot and one URI encoded dot' do
let(:config) { '.%2e' }
it_behaves_like 'key with only dots'
end
context 'when key is a string' do
let(:config) { 'test' }
describe '#value' do
it 'returns key value' do
expect(entry.value).to eq 'test'
end
end
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment