Commit 76bc1df1 authored by GitLab Bot's avatar GitLab Bot

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2018-04-10

parents 13688f0e e4c8a84d
...@@ -55,22 +55,20 @@ ...@@ -55,22 +55,20 @@
}, },
methods: { methods: {
successCallback(resp) { successCallback(resp) {
return resp.json().then((response) => { // depending of the endpoint the response can either bring a `pipelines` key or not.
// depending of the endpoint the response can either bring a `pipelines` key or not. const pipelines = resp.data.pipelines || resp.data;
const pipelines = response.pipelines || response; this.setCommonData(pipelines);
this.setCommonData(pipelines);
const updatePipelinesEvent = new CustomEvent('update-pipelines-count', { const updatePipelinesEvent = new CustomEvent('update-pipelines-count', {
detail: { detail: {
pipelines: response, pipelines: resp.data,
}, },
});
// notifiy to update the count in tabs
if (this.$el.parentElement) {
this.$el.parentElement.dispatchEvent(updatePipelinesEvent);
}
}); });
// notifiy to update the count in tabs
if (this.$el.parentElement) {
this.$el.parentElement.dispatchEvent(updatePipelinesEvent);
}
}, },
}, },
}; };
......
...@@ -7,10 +7,7 @@ ...@@ -7,10 +7,7 @@
import TablePagination from '../../vue_shared/components/table_pagination.vue'; import TablePagination from '../../vue_shared/components/table_pagination.vue';
import NavigationTabs from '../../vue_shared/components/navigation_tabs.vue'; import NavigationTabs from '../../vue_shared/components/navigation_tabs.vue';
import NavigationControls from './nav_controls.vue'; import NavigationControls from './nav_controls.vue';
import { import { getParameterByName } from '../../lib/utils/common_utils';
getParameterByName,
parseQueryStringIntoObject,
} from '../../lib/utils/common_utils';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
export default { export default {
...@@ -19,10 +16,7 @@ ...@@ -19,10 +16,7 @@
NavigationTabs, NavigationTabs,
NavigationControls, NavigationControls,
}, },
mixins: [ mixins: [pipelinesMixin, CIPaginationMixin],
pipelinesMixin,
CIPaginationMixin,
],
props: { props: {
store: { store: {
type: Object, type: Object,
...@@ -147,25 +141,26 @@ ...@@ -147,25 +141,26 @@
*/ */
shouldRenderTabs() { shouldRenderTabs() {
const { stateMap } = this.$options; const { stateMap } = this.$options;
return this.hasMadeRequest && return (
[ this.hasMadeRequest &&
stateMap.loading, [stateMap.loading, stateMap.tableList, stateMap.error, stateMap.emptyTab].includes(
stateMap.tableList, this.stateToRender,
stateMap.error, )
stateMap.emptyTab, );
].includes(this.stateToRender);
}, },
shouldRenderButtons() { shouldRenderButtons() {
return (this.newPipelinePath || return (
this.resetCachePath || (this.newPipelinePath || this.resetCachePath || this.ciLintPath) && this.shouldRenderTabs
this.ciLintPath) && this.shouldRenderTabs; );
}, },
shouldRenderPagination() { shouldRenderPagination() {
return !this.isLoading && return (
!this.isLoading &&
this.state.pipelines.length && this.state.pipelines.length &&
this.state.pageInfo.total > this.state.pageInfo.perPage; this.state.pageInfo.total > this.state.pageInfo.perPage
);
}, },
emptyTabMessage() { emptyTabMessage() {
...@@ -229,15 +224,13 @@ ...@@ -229,15 +224,13 @@
}, },
methods: { methods: {
successCallback(resp) { successCallback(resp) {
return resp.json().then((response) => { // Because we are polling & the user is interacting verify if the response received
// Because we are polling & the user is interacting verify if the response received // matches the last request made
// matches the last request made if (_.isEqual(resp.config.params, this.requestData)) {
if (_.isEqual(parseQueryStringIntoObject(resp.url.split('?')[1]), this.requestData)) { this.store.storeCount(resp.data.count);
this.store.storeCount(response.count); this.store.storePagination(resp.headers);
this.store.storePagination(resp.headers); this.setCommonData(resp.data.pipelines);
this.setCommonData(response.pipelines); }
}
});
}, },
/** /**
* Handles URL and query parameter changes. * Handles URL and query parameter changes.
...@@ -251,8 +244,9 @@ ...@@ -251,8 +244,9 @@
this.updateInternalState(parameters); this.updateInternalState(parameters);
// fetch new data // fetch new data
return this.service.getPipelines(this.requestData) return this.service
.then((response) => { .getPipelines(this.requestData)
.then(response => {
this.isLoading = false; this.isLoading = false;
this.successCallback(response); this.successCallback(response);
...@@ -271,13 +265,11 @@ ...@@ -271,13 +265,11 @@
handleResetRunnersCache(endpoint) { handleResetRunnersCache(endpoint) {
this.isResetCacheButtonLoading = true; this.isResetCacheButtonLoading = true;
this.service.postAction(endpoint) this.service
.postAction(endpoint)
.then(() => { .then(() => {
this.isResetCacheButtonLoading = false; this.isResetCacheButtonLoading = false;
createFlash( createFlash(s__('Pipelines|Project cache successfully reset.'), 'notice');
s__('Pipelines|Project cache successfully reset.'),
'notice',
);
}) })
.catch(() => { .catch(() => {
this.isResetCacheButtonLoading = false; this.isResetCacheButtonLoading = false;
......
/* eslint-disable class-methods-use-this */ import axios from '../../lib/utils/axios_utils';
import Vue from 'vue';
import VueResource from 'vue-resource';
import '../../vue_shared/vue_resource_interceptor';
Vue.use(VueResource);
export default class PipelinesService { export default class PipelinesService {
/** /**
* Commits and merge request endpoints need to be requested with `.json`. * Commits and merge request endpoints need to be requested with `.json`.
* *
* The url provided to request the pipelines in the new merge request * The url provided to request the pipelines in the new merge request
* page already has `.json`. * page already has `.json`.
* *
* @param {String} root * @param {String} root
*/ */
constructor(root) { constructor(root) {
let endpoint;
if (root.indexOf('.json') === -1) { if (root.indexOf('.json') === -1) {
endpoint = `${root}.json`; this.endpoint = `${root}.json`;
} else { } else {
endpoint = root; this.endpoint = root;
} }
this.pipelines = Vue.resource(endpoint);
} }
getPipelines(data = {}) { getPipelines(data = {}) {
const { scope, page } = data; const { scope, page } = data;
return this.pipelines.get({ scope, page }); return axios.get(this.endpoint, {
params: { scope, page },
});
} }
/** /**
...@@ -38,7 +30,8 @@ export default class PipelinesService { ...@@ -38,7 +30,8 @@ export default class PipelinesService {
* @param {String} endpoint * @param {String} endpoint
* @return {Promise} * @return {Promise}
*/ */
// eslint-disable-next-line class-methods-use-this
postAction(endpoint) { postAction(endpoint) {
return Vue.http.post(`${endpoint}.json`); return axios.post(`${endpoint}.json`);
} }
} }
...@@ -34,7 +34,7 @@ class DeployToken < ActiveRecord::Base ...@@ -34,7 +34,7 @@ class DeployToken < ActiveRecord::Base
end end
def has_access_to?(requested_project) def has_access_to?(requested_project)
project == requested_project active? && project == requested_project
end end
# This is temporal. Currently we limit DeployToken # This is temporal. Currently we limit DeployToken
......
...@@ -149,7 +149,8 @@ module Auth ...@@ -149,7 +149,8 @@ module Auth
def deploy_token_can_pull?(requested_project) def deploy_token_can_pull?(requested_project)
has_authentication_ability?(:read_container_image) && has_authentication_ability?(:read_container_image) &&
current_user.is_a?(DeployToken) && current_user.is_a?(DeployToken) &&
current_user.has_access_to?(requested_project) current_user.has_access_to?(requested_project) &&
current_user.read_registry?
end end
## ##
......
...@@ -6,9 +6,6 @@ module Ci ...@@ -6,9 +6,6 @@ module Ci
attr_reader :runner attr_reader :runner
JOB_QUEUE_DURATION_SECONDS_BUCKETS = [1, 3, 10, 30].freeze
JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET = 5.freeze
Result = Struct.new(:build, :valid?) Result = Struct.new(:build, :valid?)
def initialize(runner) def initialize(runner)
...@@ -109,22 +106,10 @@ module Ci ...@@ -109,22 +106,10 @@ module Ci
end end
def register_success(job) def register_success(job)
labels = { shared_runner: runner.shared?, job_queue_duration_seconds.observe({ shared_runner: @runner.shared? }, Time.now - job.created_at)
jobs_running_for_project: jobs_running_for_project(job) }
job_queue_duration_seconds.observe(labels, Time.now - job.queued_at)
attempt_counter.increment attempt_counter.increment
end end
def jobs_running_for_project(job)
return '+Inf' unless runner.shared?
# excluding currently started job
running_jobs_count = job.project.builds.running.where(runner: Ci::Runner.shared)
.limit(JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET + 1).count - 1
running_jobs_count < JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET ? running_jobs_count : "#{JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET}+"
end
def failed_attempt_counter def failed_attempt_counter
@failed_attempt_counter ||= Gitlab::Metrics.counter(:job_register_attempts_failed_total, "Counts the times a runner tries to register a job") @failed_attempt_counter ||= Gitlab::Metrics.counter(:job_register_attempts_failed_total, "Counts the times a runner tries to register a job")
end end
...@@ -134,7 +119,7 @@ module Ci ...@@ -134,7 +119,7 @@ module Ci
end end
def job_queue_duration_seconds def job_queue_duration_seconds
@job_queue_duration_seconds ||= Gitlab::Metrics.histogram(:job_queue_duration_seconds, 'Request handling execution time', {}, JOB_QUEUE_DURATION_SECONDS_BUCKETS) @job_queue_duration_seconds ||= Gitlab::Metrics.histogram(:job_queue_duration_seconds, 'Request handling execution time')
end end
end end
end end
title: Replace vue resource with axios in pipelines table
merge_request:
author:
type: other
\ No newline at end of file
---
title: Verify that deploy token has valid access when pulling container registry image
merge_request: 18260
author:
type: fixed
---
title: Partition job_queue_duration_seconds with jobs_running_for_project
merge_request: 17730
author:
type: changed
...@@ -7,6 +7,67 @@ be replaced by Gitaly API calls. ...@@ -7,6 +7,67 @@ be replaced by Gitaly API calls.
Visit the [Gitaly Migration Board](https://gitlab.com/gitlab-org/gitaly/boards/331341) for current Visit the [Gitaly Migration Board](https://gitlab.com/gitlab-org/gitaly/boards/331341) for current
status of the migration. status of the migration.
## Developing new Git features
Starting with Gitlab 10.8, all new Git features should be developed in
Gitaly.
> This is a new process that is not clearly defined yet. If you want
to contribute a Git feature and you're getting stuck, reach out to the
Gitaly team or `@jacobvosmaer-gitlab`.
By 'new feature' we mean any method or class in `lib/gitlab/git` that is
called from outside `lib/gitlab/git`. For new methods that are called
from inside `lib/gitlab/git`, see 'Modifying existing Git features'
below.
There should be no new code that touches Git repositories via
disk access (e.g. Rugged, `git`, `rm -rf`) anywhere outside
`lib/gitlab/git`.
The process for adding new Gitaly features is:
- exploration / prototyping
- design and create a new Gitaly RPC [in gitaly-proto](https://gitlab.com/gitlab-org/gitaly-proto)
- release a new version of gitaly-proto
- write implementation and tests for the RPC [in Gitaly](https://gitlab.com/gitlab-org/gitaly), in Go or Ruby
- release a new version of Gitaly
- write client code in gitlab-ce/ee, gitlab-workhorse or gitlab-shell that calls the new Gitaly RPC
These steps often overlap. It is possible to use an unreleased version
of Gitaly and gitaly-proto during testing and development.
- See the [Gitaly repo](https://gitlab.com/gitlab-org/gitaly/blob/master/CONTRIBUTING.md#development-and-testing-with-a-custom-gitaly-proto) for instructions on writing server side code with an unreleased protocol.
- See [below](#running-tests-with-a-locally-modified-version-of-gitaly) for instructions on running gitlab-ce tests with a modified version of Gitaly.
- In GDK run `gdk install` and restart `gdk run` (or `gdk run app`) to use a locally modified Gitaly version for development
### Gitaly-ruby
It is possible to implement and test RPC's in Gitaly using Ruby code,
in
[gitaly-ruby](https://gitlab.com/gitlab-org/gitaly/tree/master/ruby).
This should make it easier to contribute for developers who are less
comfortable writing Go code.
There is documentation for this approach in [the Gitaly
repo](https://gitlab.com/gitlab-org/gitaly/blob/master/doc/ruby_endpoint.md).
## Modifying existing Git features
If you modify existing Git features in `lib/gitlab/git` you need to make
sure the changes also work in Gitaly. Because we are still in the
migration process there are a number of subtle pitfalls. Features that
have been migrated have dual implementations (Gitaly and local). The
Gitaly implementation may or may not use a vendored (and therefore
possibly outdated) copy of the local implementation in `lib/gitlab/git`.
To avoid unexpected problems and conflicts, all changes to
`lib/gitlab/git` need to be approved by a member of the Gitaly team.
For the time being, while the Gitaly migration is still in progress,
there should be no Enterprise Edition-only Git code in
`lib/gitlab/git`. Also no mixins.
## Feature Flags ## Feature Flags
Gitaly makes heavy use of [feature flags](feature_flags.md). Gitaly makes heavy use of [feature flags](feature_flags.md).
...@@ -99,10 +160,14 @@ end ...@@ -99,10 +160,14 @@ end
## Running tests with a locally modified version of Gitaly ## Running tests with a locally modified version of Gitaly
Normally, gitlab-ce/ee tests use a local clone of Gitaly in `tmp/tests/gitaly` Normally, gitlab-ce/ee tests use a local clone of Gitaly in
pinned at the version specified in GITALY_SERVER_VERSION. If you want `tmp/tests/gitaly` pinned at the version specified in
to run tests locally against a modified version of Gitaly you can `GITALY_SERVER_VERSION`. The `GITALY_SERVER_VERSION` file supports
replace `tmp/tests/gitaly` with a symlink. `=my-branch` syntax to use a custom branch in gitlab-org/gitaly. If
you want to run tests locally against a modified version of Gitaly you
can replace `tmp/tests/gitaly` with a symlink. This is much faster
because the `=my-branch` syntax forces a Gitaly re-install each time
you run `rspec`.
```shell ```shell
rm -rf tmp/tests/gitaly rm -rf tmp/tests/gitaly
......
...@@ -62,7 +62,6 @@ FactoryBot.define do ...@@ -62,7 +62,6 @@ FactoryBot.define do
end end
trait :pending do trait :pending do
queued_at 'Di 29. Okt 09:50:59 CET 2013'
status 'pending' status 'pending'
end end
......
import _ from 'underscore';
import Vue from 'vue'; import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import pipelinesTable from '~/commit/pipelines/pipelines_table.vue'; import pipelinesTable from '~/commit/pipelines/pipelines_table.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Pipelines table in Commits and Merge requests', () => { describe('Pipelines table in Commits and Merge requests', () => {
const jsonFixtureName = 'pipelines/pipelines.json'; const jsonFixtureName = 'pipelines/pipelines.json';
let pipeline; let pipeline;
let PipelinesTable; let PipelinesTable;
let mock;
let vm;
preloadFixtures(jsonFixtureName); preloadFixtures(jsonFixtureName);
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios);
const pipelines = getJSONFixture(jsonFixtureName).pipelines; const pipelines = getJSONFixture(jsonFixtureName).pipelines;
PipelinesTable = Vue.extend(pipelinesTable); PipelinesTable = Vue.extend(pipelinesTable);
pipeline = pipelines.find(p => p.user !== null && p.commit !== null); pipeline = pipelines.find(p => p.user !== null && p.commit !== null);
}); });
afterEach(() => {
vm.$destroy();
mock.restore();
});
describe('successful request', () => { describe('successful request', () => {
describe('without pipelines', () => { describe('without pipelines', () => {
const pipelinesEmptyResponse = (request, next) => {
next(request.respondWith(JSON.stringify([]), {
status: 200,
}));
};
beforeEach(function () { beforeEach(function () {
Vue.http.interceptors.push(pipelinesEmptyResponse); mock.onGet('endpoint.json').reply(200, []);
this.component = new PipelinesTable({
propsData: {
endpoint: 'endpoint',
helpPagePath: 'foo',
emptyStateSvgPath: 'foo',
errorStateSvgPath: 'foo',
autoDevopsHelpPath: 'foo',
},
}).$mount();
});
afterEach(function () { vm = mountComponent(PipelinesTable, {
Vue.http.interceptors = _.without( endpoint: 'endpoint.json',
Vue.http.interceptors, pipelinesEmptyResponse, helpPagePath: 'foo',
); emptyStateSvgPath: 'foo',
this.component.$destroy(); errorStateSvgPath: 'foo',
autoDevopsHelpPath: 'foo',
});
}); });
it('should render the empty state', function (done) { it('should render the empty state', function (done) {
setTimeout(() => { setTimeout(() => {
expect(this.component.$el.querySelector('.empty-state')).toBeDefined(); expect(vm.$el.querySelector('.empty-state')).toBeDefined();
expect(this.component.$el.querySelector('.realtime-loading')).toBe(null); expect(vm.$el.querySelector('.realtime-loading')).toBe(null);
expect(this.component.$el.querySelector('.js-pipelines-error-state')).toBe(null); expect(vm.$el.querySelector('.js-pipelines-error-state')).toBe(null);
done(); done();
}, 1); }, 0);
}); });
}); });
describe('with pipelines', () => { describe('with pipelines', () => {
const pipelinesResponse = (request, next) => {
next(request.respondWith(JSON.stringify([pipeline]), {
status: 200,
}));
};
beforeEach(() => { beforeEach(() => {
Vue.http.interceptors.push(pipelinesResponse); mock.onGet('endpoint.json').reply(200, [pipeline]);
vm = mountComponent(PipelinesTable, {
this.component = new PipelinesTable({ endpoint: 'endpoint.json',
propsData: { helpPagePath: 'foo',
endpoint: 'endpoint', emptyStateSvgPath: 'foo',
helpPagePath: 'foo', errorStateSvgPath: 'foo',
emptyStateSvgPath: 'foo', autoDevopsHelpPath: 'foo',
errorStateSvgPath: 'foo', });
autoDevopsHelpPath: 'foo',
},
}).$mount();
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, pipelinesResponse,
);
this.component.$destroy();
}); });
it('should render a table with the received pipelines', (done) => { it('should render a table with the received pipelines', (done) => {
setTimeout(() => { setTimeout(() => {
expect(this.component.$el.querySelectorAll('.ci-table .commit').length).toEqual(1); expect(vm.$el.querySelectorAll('.ci-table .commit').length).toEqual(1);
expect(this.component.$el.querySelector('.realtime-loading')).toBe(null); expect(vm.$el.querySelector('.realtime-loading')).toBe(null);
expect(this.component.$el.querySelector('.empty-state')).toBe(null); expect(vm.$el.querySelector('.empty-state')).toBe(null);
expect(this.component.$el.querySelector('.js-pipelines-error-state')).toBe(null); expect(vm.$el.querySelector('.js-pipelines-error-state')).toBe(null);
done(); done();
}, 0); }, 0);
}); });
}); });
describe('pipeline badge counts', () => { describe('pipeline badge counts', () => {
const pipelinesResponse = (request, next) => {
next(request.respondWith(JSON.stringify([pipeline]), {
status: 200,
}));
};
beforeEach(() => { beforeEach(() => {
Vue.http.interceptors.push(pipelinesResponse); mock.onGet('endpoint.json').reply(200, [pipeline]);
});
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptors, pipelinesResponse);
this.component.$destroy();
}); });
it('should receive update-pipelines-count event', (done) => { it('should receive update-pipelines-count event', (done) => {
...@@ -119,54 +88,38 @@ describe('Pipelines table in Commits and Merge requests', () => { ...@@ -119,54 +88,38 @@ describe('Pipelines table in Commits and Merge requests', () => {
done(); done();
}); });
this.component = new PipelinesTable({ vm = mountComponent(PipelinesTable, {
propsData: { endpoint: 'endpoint.json',
endpoint: 'endpoint',
helpPagePath: 'foo',
emptyStateSvgPath: 'foo',
errorStateSvgPath: 'foo',
autoDevopsHelpPath: 'foo',
},
}).$mount();
element.appendChild(this.component.$el);
});
});
});
describe('unsuccessfull request', () => {
const pipelinesErrorResponse = (request, next) => {
next(request.respondWith(JSON.stringify([]), {
status: 500,
}));
};
beforeEach(function () {
Vue.http.interceptors.push(pipelinesErrorResponse);
this.component = new PipelinesTable({
propsData: {
endpoint: 'endpoint',
helpPagePath: 'foo', helpPagePath: 'foo',
emptyStateSvgPath: 'foo', emptyStateSvgPath: 'foo',
errorStateSvgPath: 'foo', errorStateSvgPath: 'foo',
autoDevopsHelpPath: 'foo', autoDevopsHelpPath: 'foo',
}, });
}).$mount();
element.appendChild(vm.$el);
});
}); });
});
afterEach(function () { describe('unsuccessfull request', () => {
Vue.http.interceptors = _.without( beforeEach(() => {
Vue.http.interceptors, pipelinesErrorResponse, mock.onGet('endpoint.json').reply(500, []);
);
this.component.$destroy(); vm = mountComponent(PipelinesTable, {
endpoint: 'endpoint.json',
helpPagePath: 'foo',
emptyStateSvgPath: 'foo',
errorStateSvgPath: 'foo',
autoDevopsHelpPath: 'foo',
});
}); });
it('should render error state', function (done) { it('should render error state', function (done) {
setTimeout(() => { setTimeout(() => {
expect(this.component.$el.querySelector('.js-pipelines-error-state')).toBeDefined(); expect(vm.$el.querySelector('.js-pipelines-error-state')).toBeDefined();
expect(this.component.$el.querySelector('.realtime-loading')).toBe(null); expect(vm.$el.querySelector('.realtime-loading')).toBe(null);
expect(this.component.$el.querySelector('.js-empty-state')).toBe(null); expect(vm.$el.querySelector('.js-empty-state')).toBe(null);
expect(this.component.$el.querySelector('.ci-table')).toBe(null); expect(vm.$el.querySelector('.ci-table')).toBe(null);
done(); done();
}, 0); }, 0);
}); });
......
import _ from 'underscore';
import Vue from 'vue'; import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import pipelinesComp from '~/pipelines/components/pipelines.vue'; import pipelinesComp from '~/pipelines/components/pipelines.vue';
import Store from '~/pipelines/stores/pipelines_store'; import Store from '~/pipelines/stores/pipelines_store';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
...@@ -12,6 +13,8 @@ describe('Pipelines', () => { ...@@ -12,6 +13,8 @@ describe('Pipelines', () => {
let PipelinesComponent; let PipelinesComponent;
let pipelines; let pipelines;
let vm; let vm;
let mock;
const paths = { const paths = {
endpoint: 'twitter/flight/pipelines.json', endpoint: 'twitter/flight/pipelines.json',
autoDevopsPath: '/help/topics/autodevops/index.md', autoDevopsPath: '/help/topics/autodevops/index.md',
...@@ -34,6 +37,8 @@ describe('Pipelines', () => { ...@@ -34,6 +37,8 @@ describe('Pipelines', () => {
}; };
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios);
pipelines = getJSONFixture(jsonFixtureName); pipelines = getJSONFixture(jsonFixtureName);
PipelinesComponent = Vue.extend(pipelinesComp); PipelinesComponent = Vue.extend(pipelinesComp);
...@@ -41,38 +46,14 @@ describe('Pipelines', () => { ...@@ -41,38 +46,14 @@ describe('Pipelines', () => {
afterEach(() => { afterEach(() => {
vm.$destroy(); vm.$destroy();
mock.restore();
}); });
const pipelinesInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify(pipelines), {
status: 200,
}));
};
const emptyStateInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify({
pipelines: [],
count: {
all: 0,
pending: 0,
running: 0,
finished: 0,
},
}), {
status: 200,
}));
};
const errorInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify({}), {
status: 500,
}));
};
describe('With permission', () => { describe('With permission', () => {
describe('With pipelines in main tab', () => { describe('With pipelines in main tab', () => {
beforeEach((done) => { beforeEach((done) => {
Vue.http.interceptors.push(pipelinesInterceptor); mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
vm = mountComponent(PipelinesComponent, { vm = mountComponent(PipelinesComponent, {
store: new Store(), store: new Store(),
hasGitlabCi: true, hasGitlabCi: true,
...@@ -85,12 +66,6 @@ describe('Pipelines', () => { ...@@ -85,12 +66,6 @@ describe('Pipelines', () => {
}); });
}); });
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, pipelinesInterceptor,
);
});
it('renders tabs', () => { it('renders tabs', () => {
expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All'); expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
}); });
...@@ -116,7 +91,15 @@ describe('Pipelines', () => { ...@@ -116,7 +91,15 @@ describe('Pipelines', () => {
describe('Without pipelines on main tab with CI', () => { describe('Without pipelines on main tab with CI', () => {
beforeEach((done) => { beforeEach((done) => {
Vue.http.interceptors.push(emptyStateInterceptor); mock.onGet('twitter/flight/pipelines.json').reply(200, {
pipelines: [],
count: {
all: 0,
pending: 0,
running: 0,
finished: 0,
},
});
vm = mountComponent(PipelinesComponent, { vm = mountComponent(PipelinesComponent, {
store: new Store(), store: new Store(),
hasGitlabCi: true, hasGitlabCi: true,
...@@ -129,12 +112,6 @@ describe('Pipelines', () => { ...@@ -129,12 +112,6 @@ describe('Pipelines', () => {
}); });
}); });
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, emptyStateInterceptor,
);
});
it('renders tabs', () => { it('renders tabs', () => {
expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All'); expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
}); });
...@@ -158,7 +135,15 @@ describe('Pipelines', () => { ...@@ -158,7 +135,15 @@ describe('Pipelines', () => {
describe('Without pipelines nor CI', () => { describe('Without pipelines nor CI', () => {
beforeEach((done) => { beforeEach((done) => {
Vue.http.interceptors.push(emptyStateInterceptor); mock.onGet('twitter/flight/pipelines.json').reply(200, {
pipelines: [],
count: {
all: 0,
pending: 0,
running: 0,
finished: 0,
},
});
vm = mountComponent(PipelinesComponent, { vm = mountComponent(PipelinesComponent, {
store: new Store(), store: new Store(),
hasGitlabCi: false, hasGitlabCi: false,
...@@ -171,12 +156,6 @@ describe('Pipelines', () => { ...@@ -171,12 +156,6 @@ describe('Pipelines', () => {
}); });
}); });
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, emptyStateInterceptor,
);
});
it('renders empty state', () => { it('renders empty state', () => {
expect(vm.$el.querySelector('.js-empty-state h4').textContent.trim()).toEqual('Build with confidence'); expect(vm.$el.querySelector('.js-empty-state h4').textContent.trim()).toEqual('Build with confidence');
expect(vm.$el.querySelector('.js-get-started-pipelines').getAttribute('href')).toEqual(paths.helpPagePath); expect(vm.$el.querySelector('.js-get-started-pipelines').getAttribute('href')).toEqual(paths.helpPagePath);
...@@ -192,7 +171,7 @@ describe('Pipelines', () => { ...@@ -192,7 +171,7 @@ describe('Pipelines', () => {
describe('When API returns error', () => { describe('When API returns error', () => {
beforeEach((done) => { beforeEach((done) => {
Vue.http.interceptors.push(errorInterceptor); mock.onGet('twitter/flight/pipelines.json').reply(500, {});
vm = mountComponent(PipelinesComponent, { vm = mountComponent(PipelinesComponent, {
store: new Store(), store: new Store(),
hasGitlabCi: false, hasGitlabCi: false,
...@@ -205,12 +184,6 @@ describe('Pipelines', () => { ...@@ -205,12 +184,6 @@ describe('Pipelines', () => {
}); });
}); });
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, errorInterceptor,
);
});
it('renders tabs', () => { it('renders tabs', () => {
expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All'); expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
}); });
...@@ -230,7 +203,8 @@ describe('Pipelines', () => { ...@@ -230,7 +203,8 @@ describe('Pipelines', () => {
describe('Without permission', () => { describe('Without permission', () => {
describe('With pipelines in main tab', () => { describe('With pipelines in main tab', () => {
beforeEach((done) => { beforeEach((done) => {
Vue.http.interceptors.push(pipelinesInterceptor); mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
vm = mountComponent(PipelinesComponent, { vm = mountComponent(PipelinesComponent, {
store: new Store(), store: new Store(),
hasGitlabCi: false, hasGitlabCi: false,
...@@ -243,12 +217,6 @@ describe('Pipelines', () => { ...@@ -243,12 +217,6 @@ describe('Pipelines', () => {
}); });
}); });
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, pipelinesInterceptor,
);
});
it('renders tabs', () => { it('renders tabs', () => {
expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All'); expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
}); });
...@@ -268,7 +236,16 @@ describe('Pipelines', () => { ...@@ -268,7 +236,16 @@ describe('Pipelines', () => {
describe('Without pipelines on main tab with CI', () => { describe('Without pipelines on main tab with CI', () => {
beforeEach((done) => { beforeEach((done) => {
Vue.http.interceptors.push(emptyStateInterceptor); mock.onGet('twitter/flight/pipelines.json').reply(200, {
pipelines: [],
count: {
all: 0,
pending: 0,
running: 0,
finished: 0,
},
});
vm = mountComponent(PipelinesComponent, { vm = mountComponent(PipelinesComponent, {
store: new Store(), store: new Store(),
hasGitlabCi: true, hasGitlabCi: true,
...@@ -281,11 +258,6 @@ describe('Pipelines', () => { ...@@ -281,11 +258,6 @@ describe('Pipelines', () => {
}); });
}); });
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, emptyStateInterceptor,
);
});
it('renders tabs', () => { it('renders tabs', () => {
expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All'); expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
}); });
...@@ -303,7 +275,16 @@ describe('Pipelines', () => { ...@@ -303,7 +275,16 @@ describe('Pipelines', () => {
describe('Without pipelines nor CI', () => { describe('Without pipelines nor CI', () => {
beforeEach((done) => { beforeEach((done) => {
Vue.http.interceptors.push(emptyStateInterceptor); mock.onGet('twitter/flight/pipelines.json').reply(200, {
pipelines: [],
count: {
all: 0,
pending: 0,
running: 0,
finished: 0,
},
});
vm = mountComponent(PipelinesComponent, { vm = mountComponent(PipelinesComponent, {
store: new Store(), store: new Store(),
hasGitlabCi: false, hasGitlabCi: false,
...@@ -316,12 +297,6 @@ describe('Pipelines', () => { ...@@ -316,12 +297,6 @@ describe('Pipelines', () => {
}); });
}); });
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, emptyStateInterceptor,
);
});
it('renders empty state without button to set CI', () => { it('renders empty state without button to set CI', () => {
expect(vm.$el.querySelector('.js-empty-state').textContent.trim()).toEqual('This project is not currently set up to run pipelines.'); expect(vm.$el.querySelector('.js-empty-state').textContent.trim()).toEqual('This project is not currently set up to run pipelines.');
expect(vm.$el.querySelector('.js-get-started-pipelines')).toBeNull(); expect(vm.$el.querySelector('.js-get-started-pipelines')).toBeNull();
...@@ -337,7 +312,8 @@ describe('Pipelines', () => { ...@@ -337,7 +312,8 @@ describe('Pipelines', () => {
describe('When API returns error', () => { describe('When API returns error', () => {
beforeEach((done) => { beforeEach((done) => {
Vue.http.interceptors.push(errorInterceptor); mock.onGet('twitter/flight/pipelines.json').reply(500, {});
vm = mountComponent(PipelinesComponent, { vm = mountComponent(PipelinesComponent, {
store: new Store(), store: new Store(),
hasGitlabCi: false, hasGitlabCi: false,
...@@ -350,12 +326,6 @@ describe('Pipelines', () => { ...@@ -350,12 +326,6 @@ describe('Pipelines', () => {
}); });
}); });
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, errorInterceptor,
);
});
it('renders tabs', () => { it('renders tabs', () => {
expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All'); expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
}); });
...@@ -375,7 +345,8 @@ describe('Pipelines', () => { ...@@ -375,7 +345,8 @@ describe('Pipelines', () => {
describe('successfull request', () => { describe('successfull request', () => {
describe('with pipelines', () => { describe('with pipelines', () => {
beforeEach(() => { beforeEach(() => {
Vue.http.interceptors.push(pipelinesInterceptor); mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
vm = mountComponent(PipelinesComponent, { vm = mountComponent(PipelinesComponent, {
store: new Store(), store: new Store(),
hasGitlabCi: true, hasGitlabCi: true,
...@@ -384,12 +355,6 @@ describe('Pipelines', () => { ...@@ -384,12 +355,6 @@ describe('Pipelines', () => {
}); });
}); });
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, pipelinesInterceptor,
);
});
it('should render table', (done) => { it('should render table', (done) => {
setTimeout(() => { setTimeout(() => {
expect(vm.$el.querySelector('.table-holder')).toBeDefined(); expect(vm.$el.querySelector('.table-holder')).toBeDefined();
......
...@@ -78,19 +78,30 @@ describe DeployToken do ...@@ -78,19 +78,30 @@ describe DeployToken do
describe '#has_access_to?' do describe '#has_access_to?' do
let(:project) { create(:project) } let(:project) { create(:project) }
subject(:deploy_token) { create(:deploy_token, projects: [project]) } subject { deploy_token.has_access_to?(project) }
context 'when the deploy token has access to the project' do context 'when deploy token is active and related to project' do
it 'should return true' do let(:deploy_token) { create(:deploy_token, projects: [project]) }
expect(deploy_token.has_access_to?(project)).to be_truthy
end it { is_expected.to be_truthy }
end end
context 'when the deploy token does not have access to the project' do context 'when deploy token is active but not related to project' do
it 'should return false' do let(:deploy_token) { create(:deploy_token) }
another_project = create(:project)
expect(deploy_token.has_access_to?(another_project)).to be_falsy it { is_expected.to be_falsy }
end end
context 'when deploy token is revoked and related to project' do
let(:deploy_token) { create(:deploy_token, :revoked, projects: [project]) }
it { is_expected.to be_falsy }
end
context 'when deploy token is revoked and not related to the project' do
let(:deploy_token) { create(:deploy_token, :revoked) }
it { is_expected.to be_falsy }
end end
end end
......
...@@ -598,4 +598,140 @@ describe Auth::ContainerRegistryAuthenticationService do ...@@ -598,4 +598,140 @@ describe Auth::ContainerRegistryAuthenticationService do
it_behaves_like 'not a container repository factory' it_behaves_like 'not a container repository factory'
end end
end end
context 'for deploy tokens' do
let(:current_params) do
{ scope: "repository:#{project.full_path}:pull" }
end
context 'when deploy token has read_registry as a scope' do
let(:current_user) { create(:deploy_token, projects: [project]) }
context 'for public project' do
let(:project) { create(:project, :public) }
context 'when pulling' do
it_behaves_like 'a pullable'
end
context 'when pushing' do
let(:current_params) do
{ scope: "repository:#{project.full_path}:push" }
end
it_behaves_like 'an inaccessible'
end
end
context 'for internal project' do
let(:project) { create(:project, :internal) }
context 'when pulling' do
it_behaves_like 'a pullable'
end
context 'when pushing' do
let(:current_params) do
{ scope: "repository:#{project.full_path}:push" }
end
it_behaves_like 'an inaccessible'
end
end
context 'for private project' do
let(:project) { create(:project, :private) }
context 'when pulling' do
it_behaves_like 'a pullable'
end
context 'when pushing' do
let(:current_params) do
{ scope: "repository:#{project.full_path}:push" }
end
it_behaves_like 'an inaccessible'
end
end
end
context 'when deploy token does not have read_registry scope' do
let(:current_user) { create(:deploy_token, projects: [project], read_registry: false) }
context 'for public project' do
let(:project) { create(:project, :public) }
context 'when pulling' do
it_behaves_like 'a pullable'
end
end
context 'for internal project' do
let(:project) { create(:project, :internal) }
context 'when pulling' do
it_behaves_like 'an inaccessible'
end
end
context 'for private project' do
let(:project) { create(:project, :internal) }
context 'when pulling' do
it_behaves_like 'an inaccessible'
end
end
end
context 'when deploy token is not related to the project' do
let(:current_user) { create(:deploy_token, read_registry: false) }
context 'for public project' do
let(:project) { create(:project, :public) }
context 'when pulling' do
it_behaves_like 'a pullable'
end
end
context 'for internal project' do
let(:project) { create(:project, :internal) }
context 'when pulling' do
it_behaves_like 'an inaccessible'
end
end
context 'for private project' do
let(:project) { create(:project, :internal) }
context 'when pulling' do
it_behaves_like 'an inaccessible'
end
end
end
context 'when deploy token has been revoked' do
let(:current_user) { create(:deploy_token, :revoked, projects: [project]) }
context 'for public project' do
let(:project) { create(:project, :public) }
it_behaves_like 'a pullable'
end
context 'for internal project' do
let(:project) { create(:project, :internal) }
it_behaves_like 'an inaccessible'
end
context 'for private project' do
let(:project) { create(:project, :internal) }
it_behaves_like 'an inaccessible'
end
end
end
end end
...@@ -370,89 +370,10 @@ module Ci ...@@ -370,89 +370,10 @@ module Ci
it_behaves_like 'validation is not active' it_behaves_like 'validation is not active'
end end
end end
end
describe '#register_success' do def execute(runner)
let!(:current_time) { Time.new(2018, 4, 5, 14, 0, 0) } described_class.new(runner).execute.build
let!(:attempt_counter) { double('Gitlab::Metrics::NullMetric') }
let!(:job_queue_duration_seconds) { double('Gitlab::Metrics::NullMetric') }
before do
allow(Time).to receive(:now).and_return(current_time)
# Stub defaults for any metrics other than the ones we're testing
allow(Gitlab::Metrics).to receive(:counter)
.with(any_args)
.and_return(Gitlab::Metrics::NullMetric.instance)
allow(Gitlab::Metrics).to receive(:histogram)
.with(any_args)
.and_return(Gitlab::Metrics::NullMetric.instance)
# Stub tested metrics
allow(Gitlab::Metrics).to receive(:counter)
.with(:job_register_attempts_total, anything)
.and_return(attempt_counter)
allow(Gitlab::Metrics).to receive(:histogram)
.with(:job_queue_duration_seconds, anything, anything, anything)
.and_return(job_queue_duration_seconds)
project.update(shared_runners_enabled: true)
pending_job.update(created_at: current_time - 3600, queued_at: current_time - 1800)
end end
shared_examples 'metrics collector' do
it 'increments attempt counter' do
allow(job_queue_duration_seconds).to receive(:observe)
expect(attempt_counter).to receive(:increment)
execute(runner)
end
it 'counts job queuing time histogram with expected labels' do
allow(attempt_counter).to receive(:increment)
expect(job_queue_duration_seconds).to receive(:observe)
.with({ shared_runner: expected_shared_runner,
jobs_running_for_project: expected_jobs_running_for_project_first_job }, 1800)
execute(runner)
end
context 'when project already has running jobs' do
let!(:build2) { create( :ci_build, :running, pipeline: pipeline, runner: shared_runner) }
let!(:build3) { create( :ci_build, :running, pipeline: pipeline, runner: shared_runner) }
it 'counts job queuing time histogram with expected labels' do
allow(attempt_counter).to receive(:increment)
expect(job_queue_duration_seconds).to receive(:observe)
.with({ shared_runner: expected_shared_runner,
jobs_running_for_project: expected_jobs_running_for_project_third_job }, 1800)
execute(runner)
end
end
end
context 'when shared runner is used' do
let(:runner) { shared_runner }
let(:expected_shared_runner) { true }
let(:expected_jobs_running_for_project_first_job) { 0 }
let(:expected_jobs_running_for_project_third_job) { 2 }
it_behaves_like 'metrics collector'
end
context 'when specific runner is used' do
let(:runner) { specific_runner }
let(:expected_shared_runner) { false }
let(:expected_jobs_running_for_project_first_job) { '+Inf' }
let(:expected_jobs_running_for_project_third_job) { '+Inf' }
it_behaves_like 'metrics collector'
end
end
def execute(runner)
described_class.new(runner).execute.build
end end
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment