diff options
40 files changed, 755 insertions, 779 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bcb0c8fbca8..c3163b687b4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1011,6 +1011,5 @@ schedule:review_apps_cleanup: - schedules@gitlab-org/gitlab-ee kubernetes: active except: - - master - tags - /(^docs[\/-].*|.*-docs$)/ diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index edca45f22f9..a8d615dd8f0 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -41,6 +41,11 @@ export default { required: true, }, }, + data() { + return { + assignedDiscussions: false, + }; + }, computed: { ...mapState({ isLoading: state => state.diffs.isLoading, @@ -58,9 +63,9 @@ export default { plainDiffPath: state => state.diffs.plainDiffPath, emailPatchPath: state => state.diffs.emailPatchPath, }), - ...mapState('diffs', ['showTreeList']), + ...mapState('diffs', ['showTreeList', 'isLoading']), ...mapGetters('diffs', ['isParallelView']), - ...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']), + ...mapGetters(['isNotesFetched', 'getNoteableData']), targetBranch() { return { branchName: this.targetBranchName, @@ -147,11 +152,12 @@ export default { } }, setDiscussions() { - if (this.isNotesFetched) { + if (this.isNotesFetched && !this.assignedDiscussions && !this.isLoading) { requestIdleCallback( - () => { - this.assignDiscussionsToDiff(this.discussionsStructuredByLineCode); - }, + () => + this.assignDiscussionsToDiff().then(() => { + this.assignedDiscussions = true; + }), { timeout: 1000 }, ); } diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue index f72c7a84e5c..958e57c5652 100644 --- a/app/assets/javascripts/diffs/components/diff_file.vue +++ b/app/assets/javascripts/diffs/components/diff_file.vue @@ -29,7 +29,7 @@ export default { }, computed: { ...mapState('diffs', ['currentDiffFileId']), - ...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']), + ...mapGetters(['isNotesFetched']), isCollapsed() { return this.file.collapsed || false; }, @@ -79,7 +79,7 @@ export default { .then(() => { requestIdleCallback( () => { - this.assignDiscussionsToDiff(this.discussionsStructuredByLineCode); + this.assignDiscussionsToDiff(); }, { timeout: 1000 }, ); diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 1e0b27b538d..ca8ae605cb4 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -5,7 +5,6 @@ import createFlash from '~/flash'; import { s__ } from '~/locale'; import { handleLocationHash, historyPushState } from '~/lib/utils/common_utils'; import { mergeUrlParams, getLocationHash } from '~/lib/utils/url_utility'; -import { reduceDiscussionsToLineCodes } from '../../notes/stores/utils'; import { getDiffPositionByLineCode, getNoteFormData } from './utils'; import * as types from './mutation_types'; import { @@ -36,18 +35,17 @@ export const fetchDiffFiles = ({ state, commit }) => { // This is adding line discussions to the actual lines in the diff tree // once for parallel and once for inline mode -export const assignDiscussionsToDiff = ({ state, commit }, allLineDiscussions) => { +export const assignDiscussionsToDiff = ( + { commit, state, rootState }, + discussions = rootState.notes.discussions, +) => { const diffPositionByLineCode = getDiffPositionByLineCode(state.diffFiles); - Object.values(allLineDiscussions).forEach(discussions => { - if (discussions.length > 0) { - const { fileHash } = discussions[0]; - commit(types.SET_LINE_DISCUSSIONS_FOR_FILE, { - fileHash, - discussions, - diffPositionByLineCode, - }); - } + discussions.filter(discussion => discussion.diff_discussion).forEach(discussion => { + commit(types.SET_LINE_DISCUSSIONS_FOR_FILE, { + discussion, + diffPositionByLineCode, + }); }); }; @@ -190,9 +188,7 @@ export const saveDiffDiscussion = ({ dispatch }, { note, formData }) => { return dispatch('saveNote', postData, { root: true }) .then(result => dispatch('updateDiscussion', result.discussion, { root: true })) - .then(discussion => - dispatch('assignDiscussionsToDiff', reduceDiscussionsToLineCodes([discussion])), - ) + .then(discussion => dispatch('assignDiscussionsToDiff', [discussion])) .catch(() => createFlash(s__('MergeRequests|Saving the comment failed'))); }; diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js index 0b4485ecdb5..5a8aebd2086 100644 --- a/app/assets/javascripts/diffs/store/mutations.js +++ b/app/assets/javascripts/diffs/store/mutations.js @@ -90,53 +90,67 @@ export default { })); }, - [types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, discussions, diffPositionByLineCode }) { - const selectedFile = state.diffFiles.find(f => f.fileHash === fileHash); - const firstDiscussion = discussions[0]; - const isDiffDiscussion = firstDiscussion.diff_discussion; - const hasLineCode = firstDiscussion.line_code; - const diffPosition = diffPositionByLineCode[firstDiscussion.line_code]; - - if ( - selectedFile && - isDiffDiscussion && - hasLineCode && - diffPosition && + [types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { discussion, diffPositionByLineCode }) { + const { latestDiff } = state; + + const discussionLineCode = discussion.line_code; + const fileHash = discussion.diff_file.file_hash; + const lineCheck = ({ lineCode }) => + lineCode === discussionLineCode && isDiscussionApplicableToLine({ - discussion: firstDiscussion, - diffPosition, - latestDiff: state.latestDiff, - }) - ) { - const targetLine = selectedFile.parallelDiffLines.find( - line => - (line.left && line.left.lineCode === firstDiscussion.line_code) || - (line.right && line.right.lineCode === firstDiscussion.line_code), - ); - if (targetLine) { - if (targetLine.left && targetLine.left.lineCode === firstDiscussion.line_code) { - Object.assign(targetLine.left, { - discussions, - }); - } else { - Object.assign(targetLine.right, { - discussions, + discussion, + diffPosition: diffPositionByLineCode[lineCode], + latestDiff, + }); + + state.diffFiles = state.diffFiles.map(diffFile => { + if (diffFile.fileHash === fileHash) { + const file = { ...diffFile }; + + if (file.highlightedDiffLines) { + file.highlightedDiffLines = file.highlightedDiffLines.map(line => { + if (lineCheck(line)) { + return { + ...line, + discussions: line.discussions.concat(discussion), + }; + } + + return line; }); } - } - - if (selectedFile.highlightedDiffLines) { - const targetInlineLine = selectedFile.highlightedDiffLines.find( - line => line.lineCode === firstDiscussion.line_code, - ); - if (targetInlineLine) { - Object.assign(targetInlineLine, { - discussions, + if (file.parallelDiffLines) { + file.parallelDiffLines = file.parallelDiffLines.map(line => { + const left = line.left && lineCheck(line.left); + const right = line.right && lineCheck(line.right); + + if (left || right) { + return { + left: { + ...line.left, + discussions: left ? line.left.discussions.concat(discussion) : [], + }, + right: { + ...line.right, + discussions: right ? line.right.discussions.concat(discussion) : [], + }, + }; + } + + return line; }); } + + if (!file.parallelDiffLines || !file.highlightedDiffLines) { + file.discussions = file.discussions.concat(discussion); + } + + return file; } - } + + return diffFile; + }); }, [types.REMOVE_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, lineCode }) { diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js index c7b5a35cc14..dbfcf8cc921 100644 --- a/app/assets/javascripts/due_date_select.js +++ b/app/assets/javascripts/due_date_select.js @@ -3,8 +3,7 @@ import Pikaday from 'pikaday'; import dateFormat from 'dateformat'; import { __ } from '~/locale'; import axios from './lib/utils/axios_utils'; -import { timeFor } from './lib/utils/datetime_utility'; -import { parsePikadayDate, pikadayToString } from './lib/utils/datefix'; +import { timeFor, parsePikadayDate, pikadayToString } from './lib/utils/datetime_utility'; import boardsStore from './boards/stores/boards_store'; class DueDateSelect { diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index 0140960b367..c81a2230310 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -1,6 +1,3 @@ -/* eslint-disable no-new, no-unused-vars, consistent-return, no-else-return */ -/* global GitLab */ - import $ from 'jquery'; import Pikaday from 'pikaday'; import Autosave from './autosave'; @@ -8,7 +5,7 @@ import UsersSelect from './users_select'; import GfmAutoComplete from './gfm_auto_complete'; import ZenMode from './zen_mode'; import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select'; -import { parsePikadayDate, pikadayToString } from './lib/utils/datefix'; +import { parsePikadayDate, pikadayToString } from './lib/utils/datetime_utility'; export default class IssuableForm { constructor(form) { @@ -19,9 +16,11 @@ export default class IssuableForm { this.handleSubmit = this.handleSubmit.bind(this); this.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i; - new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup(); - new UsersSelect(); - new ZenMode(); + this.gfmAutoComplete = new GfmAutoComplete( + gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources, + ).setup(); + this.usersSelect = new UsersSelect(); + this.zenMode = new ZenMode(); this.titleField = this.form.find('input[name*="[title]"]'); this.descriptionField = this.form.find('textarea[name*="[description]"]'); @@ -57,8 +56,16 @@ export default class IssuableForm { } initAutosave() { - new Autosave(this.titleField, [document.location.pathname, document.location.search, 'title']); - return new Autosave(this.descriptionField, [document.location.pathname, document.location.search, 'description']); + this.autosave = new Autosave(this.titleField, [ + document.location.pathname, + document.location.search, + 'title', + ]); + return new Autosave(this.descriptionField, [ + document.location.pathname, + document.location.search, + 'description', + ]); } handleSubmit() { @@ -74,7 +81,7 @@ export default class IssuableForm { this.$wipExplanation = this.form.find('.js-wip-explanation'); this.$noWipExplanation = this.form.find('.js-no-wip-explanation'); if (!(this.$wipExplanation.length && this.$noWipExplanation.length)) { - return; + return undefined; } this.form.on('click', '.js-toggle-wip', this.toggleWip); this.titleField.on('keyup blur', this.renderWipExplanation); @@ -89,10 +96,9 @@ export default class IssuableForm { if (this.workInProgress()) { this.$wipExplanation.show(); return this.$noWipExplanation.hide(); - } else { - this.$wipExplanation.hide(); - return this.$noWipExplanation.show(); } + this.$wipExplanation.hide(); + return this.$noWipExplanation.show(); } toggleWip(event) { @@ -110,7 +116,7 @@ export default class IssuableForm { } addWip() { - this.titleField.val(`WIP: ${(this.titleField.val())}`); + this.titleField.val(`WIP: ${this.titleField.val()}`); } initTargetBranchDropdown() { diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue index ba14aaeed2c..ac19034f69d 100644 --- a/app/assets/javascripts/jobs/components/job_app.vue +++ b/app/assets/javascripts/jobs/components/job_app.vue @@ -77,11 +77,11 @@ 'shouldRenderCalloutMessage', 'shouldRenderTriggeredLabel', 'hasEnvironment', - 'isJobStuck', 'hasTrace', 'emptyStateIllustration', 'isScrollingDown', 'emptyStateAction', + 'hasRunnersForProject', ]), shouldRenderContent() { @@ -195,9 +195,9 @@ <!-- Body Section --> <stuck-block - v-if="isJobStuck" + v-if="job.stuck" class="js-job-stuck" - :has-no-runners-for-project="job.runners.available" + :has-no-runners-for-project="hasRunnersForProject" :tags="job.tags" :runners-path="runnerSettingsUrl" /> diff --git a/app/assets/javascripts/jobs/components/stuck_block.vue b/app/assets/javascripts/jobs/components/stuck_block.vue index a60643b2c65..1d5789b175a 100644 --- a/app/assets/javascripts/jobs/components/stuck_block.vue +++ b/app/assets/javascripts/jobs/components/stuck_block.vue @@ -23,14 +23,7 @@ export default { <template> <div class="bs-callout bs-callout-warning"> <p - v-if="hasNoRunnersForProject" - class="js-stuck-no-runners append-bottom-0" - > - {{ s__(`Job|This job is stuck, because the project - doesn't have any runners online assigned to it.`) }} - </p> - <p - v-else-if="tags.length" + v-if="tags.length" class="js-stuck-with-tags append-bottom-0" > {{ s__(`This job is stuck, because you don't have @@ -44,6 +37,13 @@ export default { </span> </p> <p + v-else-if="hasNoRunnersForProject" + class="js-stuck-no-runners append-bottom-0" + > + {{ s__(`Job|This job is stuck, because the project + doesn't have any runners online assigned to it.`) }} + </p> + <p v-else class="js-stuck-no-active-runner append-bottom-0" > diff --git a/app/assets/javascripts/jobs/store/getters.js b/app/assets/javascripts/jobs/store/getters.js index 4ce395a9106..4de01f8e532 100644 --- a/app/assets/javascripts/jobs/store/getters.js +++ b/app/assets/javascripts/jobs/store/getters.js @@ -41,17 +41,10 @@ export const emptyStateIllustration = state => (state.job && state.job.status && state.job.status.illustration) || {}; export const emptyStateAction = state => (state.job && state.job.status && state.job.status.action) || {}; -/** - * When the job is pending and there are no available runners - * we need to render the stuck block; - * - * @returns {Boolean} - */ -export const isJobStuck = state => - (!_.isEmpty(state.job.status) && state.job.status.group === 'pending') && - (!_.isEmpty(state.job.runners) && state.job.runners.available === false); export const isScrollingDown = state => isScrolledToBottom() && !state.isTraceComplete; +export const hasRunnersForProject = state => state.job.runners.available && !state.job.runners.online; + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/lib/utils/datefix.js b/app/assets/javascripts/lib/utils/datefix.js deleted file mode 100644 index 19e4085dbbb..00000000000 --- a/app/assets/javascripts/lib/utils/datefix.js +++ /dev/null @@ -1,28 +0,0 @@ -export const pad = (val, len = 2) => `0${val}`.slice(-len); - -/** - * Formats dates in Pickaday - * @param {String} dateString Date in yyyy-mm-dd format - * @return {Date} UTC format - */ -export const parsePikadayDate = dateString => { - const parts = dateString.split('-'); - const year = parseInt(parts[0], 10); - const month = parseInt(parts[1] - 1, 10); - const day = parseInt(parts[2], 10); - - return new Date(year, month, day); -}; - -/** - * Used `onSelect` method in pickaday - * @param {Date} date UTC format - * @return {String} Date formated in yyyy-mm-dd - */ -export const pikadayToString = date => { - const day = pad(date.getDate()); - const month = pad(date.getMonth() + 1); - const year = date.getFullYear(); - - return `${year}-${month}-${day}`; -}; diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 833dbefd3dc..1bdf98d0c97 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -1,4 +1,5 @@ import $ from 'jquery'; +import _ from 'underscore'; import timeago from 'timeago.js'; import dateFormat from 'dateformat'; import { pluralize } from './text_utility'; @@ -46,6 +47,8 @@ const getMonthNames = abbreviated => { ]; }; +export const pad = (val, len = 2) => `0${val}`.slice(-len); + /** * Given a date object returns the day of the week in English * @param {date} date @@ -74,10 +77,10 @@ let timeagoInstance; /** * Sets a timeago Instance */ -export function getTimeago() { +export const getTimeago = () => { if (!timeagoInstance) { - const localeRemaining = function getLocaleRemaining(number, index) { - return [ + const localeRemaining = (number, index) => + [ [s__('Timeago|just now'), s__('Timeago|right now')], [s__('Timeago|%s seconds ago'), s__('Timeago|%s seconds remaining')], [s__('Timeago|1 minute ago'), s__('Timeago|1 minute remaining')], @@ -93,9 +96,9 @@ export function getTimeago() { [s__('Timeago|1 year ago'), s__('Timeago|1 year remaining')], [s__('Timeago|%s years ago'), s__('Timeago|%s years remaining')], ][index]; - }; - const locale = function getLocale(number, index) { - return [ + + const locale = (number, index) => + [ [s__('Timeago|just now'), s__('Timeago|right now')], [s__('Timeago|%s seconds ago'), s__('Timeago|in %s seconds')], [s__('Timeago|1 minute ago'), s__('Timeago|in 1 minute')], @@ -111,7 +114,6 @@ export function getTimeago() { [s__('Timeago|1 year ago'), s__('Timeago|in 1 year')], [s__('Timeago|%s years ago'), s__('Timeago|in %s years')], ][index]; - }; timeago.register(timeagoLanguageCode, locale); timeago.register(`${timeagoLanguageCode}-remaining`, localeRemaining); @@ -119,7 +121,7 @@ export function getTimeago() { } return timeagoInstance; -} +}; /** * For the given element, renders a timeago instance. @@ -184,7 +186,7 @@ export const getDayDifference = (a, b) => { * @param {Number} seconds * @return {String} */ -export function timeIntervalInWords(intervalInSeconds) { +export const timeIntervalInWords = intervalInSeconds => { const secondsInteger = parseInt(intervalInSeconds, 10); const minutes = Math.floor(secondsInteger / 60); const seconds = secondsInteger - minutes * 60; @@ -196,9 +198,9 @@ export function timeIntervalInWords(intervalInSeconds) { text = `${seconds} ${pluralize('second', seconds)}`; } return text; -} +}; -export function dateInWords(date, abbreviated = false, hideYear = false) { +export const dateInWords = (date, abbreviated = false, hideYear = false) => { if (!date) return date; const month = date.getMonth(); @@ -240,7 +242,7 @@ export function dateInWords(date, abbreviated = false, hideYear = false) { } return `${monthName} ${date.getDate()}, ${year}`; -} +}; /** * Returns month name based on provided date. @@ -391,3 +393,83 @@ export const formatTime = milliseconds => { formattedTime += remainingSeconds; return formattedTime; }; + +/** + * Formats dates in Pickaday + * @param {String} dateString Date in yyyy-mm-dd format + * @return {Date} UTC format + */ +export const parsePikadayDate = dateString => { + const parts = dateString.split('-'); + const year = parseInt(parts[0], 10); + const month = parseInt(parts[1] - 1, 10); + const day = parseInt(parts[2], 10); + + return new Date(year, month, day); +}; + +/** + * Used `onSelect` method in pickaday + * @param {Date} date UTC format + * @return {String} Date formated in yyyy-mm-dd + */ +export const pikadayToString = date => { + const day = pad(date.getDate()); + const month = pad(date.getMonth() + 1); + const year = date.getFullYear(); + + return `${year}-${month}-${day}`; +}; + +/** + * Accepts seconds and returns a timeObject { weeks: #, days: #, hours: #, minutes: # } + * Seconds can be negative or positive, zero or non-zero. Can be configured for any day + * or week length. + */ +export const parseSeconds = (seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {}) => { + const DAYS_PER_WEEK = daysPerWeek; + const HOURS_PER_DAY = hoursPerDay; + const MINUTES_PER_HOUR = 60; + const MINUTES_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR; + const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR; + + const timePeriodConstraints = { + weeks: MINUTES_PER_WEEK, + days: MINUTES_PER_DAY, + hours: MINUTES_PER_HOUR, + minutes: 1, + }; + + let unorderedMinutes = Math.abs(seconds / MINUTES_PER_HOUR); + + return _.mapObject(timePeriodConstraints, minutesPerPeriod => { + const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod); + + unorderedMinutes -= periodCount * minutesPerPeriod; + + return periodCount; + }); +}; + +/** + * Accepts a timeObject (see parseSeconds) and returns a condensed string representation of it + * (e.g. '1w 2d 3h 1m' or '1h 30m'). Zero value units are not included. + */ +export const stringifyTime = timeObject => { + const reducedTime = _.reduce( + timeObject, + (memo, unitValue, unitName) => { + const isNonZero = !!unitValue; + return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo; + }, + '', + ).trim(); + return reducedTime.length ? reducedTime : '0m'; +}; + +/** + * Accepts a time string of any size (e.g. '1w 2d 3h 5m' or '1w 2d') and returns + * the first non-zero unit/value pair. + */ +export const abbreviateTime = timeStr => + timeStr.split(' ').filter(unitStr => unitStr.charAt(0) !== '0')[0]; diff --git a/app/assets/javascripts/lib/utils/pretty_time.js b/app/assets/javascripts/lib/utils/pretty_time.js deleted file mode 100644 index d92b8a7179f..00000000000 --- a/app/assets/javascripts/lib/utils/pretty_time.js +++ /dev/null @@ -1,63 +0,0 @@ -import _ from 'underscore'; - -/* - * TODO: Make these methods more configurable (e.g. stringifyTime condensed or - * non-condensed, abbreviateTimelengths) - * */ - -/* - * Accepts seconds and returns a timeObject { weeks: #, days: #, hours: #, minutes: # } - * Seconds can be negative or positive, zero or non-zero. Can be configured for any day - * or week length. -*/ - -export function parseSeconds(seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {}) { - const DAYS_PER_WEEK = daysPerWeek; - const HOURS_PER_DAY = hoursPerDay; - const MINUTES_PER_HOUR = 60; - const MINUTES_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR; - const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR; - - const timePeriodConstraints = { - weeks: MINUTES_PER_WEEK, - days: MINUTES_PER_DAY, - hours: MINUTES_PER_HOUR, - minutes: 1, - }; - - let unorderedMinutes = Math.abs(seconds / MINUTES_PER_HOUR); - - return _.mapObject(timePeriodConstraints, minutesPerPeriod => { - const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod); - - unorderedMinutes -= periodCount * minutesPerPeriod; - - return periodCount; - }); -} - -/* -* Accepts a timeObject (see parseSeconds) and returns a condensed string representation of it -* (e.g. '1w 2d 3h 1m' or '1h 30m'). Zero value units are not included. -*/ - -export function stringifyTime(timeObject) { - const reducedTime = _.reduce( - timeObject, - (memo, unitValue, unitName) => { - const isNonZero = !!unitValue; - return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo; - }, - '', - ).trim(); - return reducedTime.length ? reducedTime : '0m'; -} - -/* -* Accepts a time string of any size (e.g. '1w 2d 3h 5m' or '1w 2d') and returns -* the first non-zero unit/value pair. -*/ - -export function abbreviateTime(timeStr) { - return timeStr.split(' ').filter(unitStr => unitStr.charAt(0) !== '0')[0]; -} diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js index df5cd1b8c51..0beedcacf33 100644 --- a/app/assets/javascripts/member_expiration_date.js +++ b/app/assets/javascripts/member_expiration_date.js @@ -1,6 +1,6 @@ import $ from 'jquery'; import Pikaday from 'pikaday'; -import { parsePikadayDate, pikadayToString } from './lib/utils/datefix'; +import { parsePikadayDate, pikadayToString } from './lib/utils/datetime_utility'; // Add datepickers to all `js-access-expiration-date` elements. If those elements are // children of an element with the `clearable-input` class, and have a sibling diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index 21c334a9d33..e4f36154fcd 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -1,6 +1,5 @@ import _ from 'underscore'; import * as constants from '../constants'; -import { reduceDiscussionsToLineCodes } from './utils'; import { collapseSystemNotes } from './collapse_utils'; export const discussions = state => collapseSystemNotes(state.discussions); @@ -31,9 +30,6 @@ export const notesById = state => return acc; }, {}); -export const discussionsStructuredByLineCode = state => - reduceDiscussionsToLineCodes(state.discussions); - export const noteableType = state => { const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE, EPIC_NOTEABLE_TYPE } = constants; diff --git a/app/assets/javascripts/notes/stores/utils.js b/app/assets/javascripts/notes/stores/utils.js index 0e41ff03d67..dd57539e4d8 100644 --- a/app/assets/javascripts/notes/stores/utils.js +++ b/app/assets/javascripts/notes/stores/utils.js @@ -25,18 +25,6 @@ export const getQuickActionText = note => { return text; }; -export const reduceDiscussionsToLineCodes = selectedDiscussions => - selectedDiscussions.reduce((acc, note) => { - if (note.diff_discussion && note.line_code) { - // For context about line notes: there might be multiple notes with the same line code - const items = acc[note.line_code] || []; - items.push(note); - - Object.assign(acc, { [note.line_code]: items }); - } - return acc; - }, {}); - export const hasQuickActions = note => REGEX_QUICK_ACTIONS.test(note); export const stripQuickActions = note => note.replace(REGEX_QUICK_ACTIONS, '').trim(); diff --git a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue index 1d030c4f67f..259858e4b46 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue @@ -1,111 +1,111 @@ <script> - import { __, sprintf } from '~/locale'; - import { abbreviateTime } from '~/lib/utils/pretty_time'; - import icon from '~/vue_shared/components/icon.vue'; - import tooltip from '~/vue_shared/directives/tooltip'; +import { __, sprintf } from '~/locale'; +import { abbreviateTime } from '~/lib/utils/datetime_utility'; +import icon from '~/vue_shared/components/icon.vue'; +import tooltip from '~/vue_shared/directives/tooltip'; - export default { - name: 'TimeTrackingCollapsedState', - components: { - icon, +export default { + name: 'TimeTrackingCollapsedState', + components: { + icon, + }, + directives: { + tooltip, + }, + props: { + showComparisonState: { + type: Boolean, + required: true, }, - directives: { - tooltip, + showSpentOnlyState: { + type: Boolean, + required: true, }, - props: { - showComparisonState: { - type: Boolean, - required: true, - }, - showSpentOnlyState: { - type: Boolean, - required: true, - }, - showEstimateOnlyState: { - type: Boolean, - required: true, - }, - showNoTimeTrackingState: { - type: Boolean, - required: true, - }, - timeSpentHumanReadable: { - type: String, - required: false, - default: '', - }, - timeEstimateHumanReadable: { - type: String, - required: false, - default: '', - }, + showEstimateOnlyState: { + type: Boolean, + required: true, }, - computed: { - timeSpent() { - return this.abbreviateTime(this.timeSpentHumanReadable); - }, - timeEstimate() { - return this.abbreviateTime(this.timeEstimateHumanReadable); - }, - divClass() { - if (this.showComparisonState) { - return 'compare'; - } else if (this.showEstimateOnlyState) { - return 'estimate-only'; - } else if (this.showSpentOnlyState) { - return 'spend-only'; - } else if (this.showNoTimeTrackingState) { - return 'no-tracking'; - } + showNoTimeTrackingState: { + type: Boolean, + required: true, + }, + timeSpentHumanReadable: { + type: String, + required: false, + default: '', + }, + timeEstimateHumanReadable: { + type: String, + required: false, + default: '', + }, + }, + computed: { + timeSpent() { + return this.abbreviateTime(this.timeSpentHumanReadable); + }, + timeEstimate() { + return this.abbreviateTime(this.timeEstimateHumanReadable); + }, + divClass() { + if (this.showComparisonState) { + return 'compare'; + } else if (this.showEstimateOnlyState) { + return 'estimate-only'; + } else if (this.showSpentOnlyState) { + return 'spend-only'; + } else if (this.showNoTimeTrackingState) { + return 'no-tracking'; + } + return ''; + }, + spanClass() { + if (this.showComparisonState) { return ''; - }, - spanClass() { - if (this.showComparisonState) { - return ''; - } else if (this.showEstimateOnlyState || this.showSpentOnlyState) { - return 'bold'; - } else if (this.showNoTimeTrackingState) { - return 'no-value'; - } + } else if (this.showEstimateOnlyState || this.showSpentOnlyState) { + return 'bold'; + } else if (this.showNoTimeTrackingState) { + return 'no-value'; + } - return ''; - }, - text() { - if (this.showComparisonState) { - return `${this.timeSpent} / ${this.timeEstimate}`; - } else if (this.showEstimateOnlyState) { - return `-- / ${this.timeEstimate}`; - } else if (this.showSpentOnlyState) { - return `${this.timeSpent} / --`; - } else if (this.showNoTimeTrackingState) { - return 'None'; - } + return ''; + }, + text() { + if (this.showComparisonState) { + return `${this.timeSpent} / ${this.timeEstimate}`; + } else if (this.showEstimateOnlyState) { + return `-- / ${this.timeEstimate}`; + } else if (this.showSpentOnlyState) { + return `${this.timeSpent} / --`; + } else if (this.showNoTimeTrackingState) { + return 'None'; + } - return ''; - }, - timeTrackedTooltipText() { - let title; - if (this.showComparisonState) { - title = __('Time remaining'); - } else if (this.showEstimateOnlyState) { - title = __('Estimated'); - } else if (this.showSpentOnlyState) { - title = __('Time spent'); - } + return ''; + }, + timeTrackedTooltipText() { + let title; + if (this.showComparisonState) { + title = __('Time remaining'); + } else if (this.showEstimateOnlyState) { + title = __('Estimated'); + } else if (this.showSpentOnlyState) { + title = __('Time spent'); + } - return sprintf('%{title}: %{text}', ({ title, text: this.text })); - }, - tooltipText() { - return this.showNoTimeTrackingState ? __('Time tracking') : this.timeTrackedTooltipText; - }, + return sprintf('%{title}: %{text}', { title, text: this.text }); + }, + tooltipText() { + return this.showNoTimeTrackingState ? __('Time tracking') : this.timeTrackedTooltipText; }, - methods: { - abbreviateTime(timeStr) { - return abbreviateTime(timeStr); - }, + }, + methods: { + abbreviateTime(timeStr) { + return abbreviateTime(timeStr); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue index dc599e1b9fc..e74912d628f 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue @@ -1,5 +1,5 @@ <script> -import { parseSeconds, stringifyTime } from '../../../lib/utils/pretty_time'; +import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility'; import tooltip from '../../../vue_shared/directives/tooltip'; export default { diff --git a/app/assets/javascripts/vue_shared/components/pikaday.vue b/app/assets/javascripts/vue_shared/components/pikaday.vue index 782d8e3abf6..26c99aecae4 100644 --- a/app/assets/javascripts/vue_shared/components/pikaday.vue +++ b/app/assets/javascripts/vue_shared/components/pikaday.vue @@ -1,6 +1,6 @@ <script> import Pikaday from 'pikaday'; -import { parsePikadayDate, pikadayToString } from '../../lib/utils/datefix'; +import { parsePikadayDate, pikadayToString } from '~/lib/utils/datetime_utility'; export default { name: 'DatePicker', diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index e8e943872de..f0f791742f4 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -107,7 +107,7 @@ module Clusters end def kubeclient - @kubeclient ||= build_kube_client!(api_groups: ['api', 'apis/rbac.authorization.k8s.io']) + @kubeclient ||= build_kube_client! end private @@ -136,7 +136,7 @@ module Clusters Gitlab::NamespaceSanitizer.sanitize(slug) end - def build_kube_client!(api_groups: ['api'], api_version: 'v1') + def build_kube_client! raise "Incomplete settings" unless api_url && actual_namespace unless (username && password) || token @@ -145,8 +145,6 @@ module Clusters Gitlab::Kubernetes::KubeClient.new( api_url, - api_groups, - api_version, auth_options: kubeclient_auth_options, ssl_options: kubeclient_ssl_options, http_proxy_uri: ENV['http_proxy'] diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index f119555f16b..798944d0c06 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -144,7 +144,7 @@ class KubernetesService < DeploymentService end def kubeclient - @kubeclient ||= build_kube_client!(api_groups: ['api', 'apis/rbac.authorization.k8s.io']) + @kubeclient ||= build_kube_client! end def deprecated? @@ -182,13 +182,11 @@ class KubernetesService < DeploymentService slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '') end - def build_kube_client!(api_groups: ['api'], api_version: 'v1') + def build_kube_client! raise "Incomplete settings" unless api_url && actual_namespace && token Gitlab::Kubernetes::KubeClient.new( api_url, - api_groups, - api_version, auth_options: kubeclient_auth_options, ssl_options: kubeclient_ssl_options, http_proxy_uri: ENV['http_proxy'] diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb index 066a5b1885c..9ddce0d2c80 100644 --- a/app/serializers/build_details_entity.rb +++ b/app/serializers/build_details_entity.rb @@ -5,6 +5,7 @@ class BuildDetailsEntity < JobEntity expose :tag_list, as: :tags expose :has_trace?, as: :has_trace expose :stage + expose :stuck?, as: :stuck expose :user, using: UserEntity expose :runner, using: RunnerEntity expose :pipeline, using: PipelineEntity diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb index 3ae0a4a19d0..6ee63db8eb9 100644 --- a/app/services/clusters/gcp/finalize_creation_service.rb +++ b/app/services/clusters/gcp/finalize_creation_service.rb @@ -60,18 +60,15 @@ module Clusters 'https://' + gke_cluster.endpoint, Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate), gke_cluster.master_auth.username, - gke_cluster.master_auth.password, - api_groups: ['api', 'apis/rbac.authorization.k8s.io'] + gke_cluster.master_auth.password ) end - def build_kube_client!(api_url, ca_pem, username, password, api_groups: ['api'], api_version: 'v1') + def build_kube_client!(api_url, ca_pem, username, password) raise "Incomplete settings" unless api_url && username && password Gitlab::Kubernetes::KubeClient.new( api_url, - api_groups, - api_version, auth_options: { username: username, password: password }, ssl_options: kubeclient_ssl_options(ca_pem), http_proxy_uri: ENV['http_proxy'] diff --git a/changelogs/unreleased/52202-consider-moving-isjobstuck-verification-to-backend.yml b/changelogs/unreleased/52202-consider-moving-isjobstuck-verification-to-backend.yml new file mode 100644 index 00000000000..0efd97d91b8 --- /dev/null +++ b/changelogs/unreleased/52202-consider-moving-isjobstuck-verification-to-backend.yml @@ -0,0 +1,5 @@ +--- +title: Renders stuck block when runners are stuck +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/53055-combine-date-util-functions.yml b/changelogs/unreleased/53055-combine-date-util-functions.yml new file mode 100644 index 00000000000..56d4406f1bf --- /dev/null +++ b/changelogs/unreleased/53055-combine-date-util-functions.yml @@ -0,0 +1,5 @@ +--- +title: Combine all datetime library functions into 'datetime_utility.js' +merge_request: 22570 +author: +type: other diff --git a/lib/gitlab/ci/templates/Android.gitlab-ci.yml b/lib/gitlab/ci/templates/Android.gitlab-ci.yml index bf7831b937c..6e138639b71 100644 --- a/lib/gitlab/ci/templates/Android.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Android.gitlab-ci.yml @@ -1,51 +1,45 @@ -# Read more about this script on this blog post https://about.gitlab.com/2016/11/30/setting-up-gitlab-ci-for-android-projects/, by Greyson Parrelli +# Read more about this script on this blog post https://about.gitlab.com/2018/10/24/setting-up-gitlab-ci-for-android-projects/, by Jason Lenny image: openjdk:8-jdk variables: ANDROID_COMPILE_SDK: "28" - ANDROID_BUILD_TOOLS: "28.0.3" - ANDROID_SDK_TOOLS: "26.1.1" + ANDROID_BUILD_TOOLS: "28.0.2" + ANDROID_SDK_TOOLS: "4333796" before_script: -- apt-get --quiet update --yes -- apt-get --quiet install --yes wget tar unzip lib32stdc++6 lib32z1 -- wget --quiet --output-document=android-sdk.zip https://dl.google.com/android/repository/sdk-tools-linux-4333796.zip -- unzip android-sdk.zip -d android-sdk-linux -- echo y | android-sdk-linux/tools/bin/sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}" > /dev/null -- echo y | android-sdk-linux/tools/bin/sdkmanager platform-tools > /dev/null -- echo y | android-sdk-linux/tools/bin/sdkmanager "build-tools;${ANDROID_BUILD_TOOLS}" > /dev/null -- echo y | android-sdk-linux/tools/bin/sdkmanager "extras;google;google_play_services" > /dev/null -- echo y | android-sdk-linux/tools/bin/sdkmanager "extras;google;m2repository" > /dev/null -- export ANDROID_HOME=$PWD/android-sdk-linux -- export PATH=$PATH:$PWD/android-sdk-linux/platform-tools/ -- yes | android-sdk-linux/tools/bin/sdkmanager --licenses & -- chmod +x ./gradlew + - apt-get --quiet update --yes + - apt-get --quiet install --yes wget tar unzip lib32stdc++6 lib32z1 + - wget --quiet --output-document=android-sdk.zip https://dl.google.com/android/repository/sdk-tools-linux-${ANDROID_SDK_TOOLS}.zip + - unzip -d android-sdk-linux android-sdk.zip + - echo y | android-sdk-linux/tools/bin/sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}" >/dev/null + - echo y | android-sdk-linux/tools/bin/sdkmanager "platform-tools" >/dev/null + - echo y | android-sdk-linux/tools/bin/sdkmanager "build-tools;${ANDROID_BUILD_TOOLS}" >/dev/null + - export ANDROID_HOME=$PWD/android-sdk-linux + - export PATH=$PATH:$PWD/android-sdk-linux/platform-tools/ + - chmod +x ./gradlew + # temporarily disable checking for EPIPE error and use yes to accept all licenses + - set +o pipefail + - yes | android-sdk-linux/tools/bin/sdkmanager --licenses + - set -o pipefail stages: -- build -- test + - build + - test -build: +lintDebug: stage: build script: - - ./gradlew assembleDebug + - ./gradlew -Pci --console=plain :app:lintDebug -PbuildDir=lint + +assembleDebug: + stage: build + script: + - ./gradlew assembleDebug artifacts: paths: - app/build/outputs/ -unitTests: - stage: test - script: - - ./gradlew test - -functionalTests: +debugTests: stage: test script: - - wget --quiet --output-document=android-wait-for-emulator https://raw.githubusercontent.com/travis-ci/travis-cookbooks/0f497eb71291b52a703143c5cd63a217c8766dc9/community-cookbooks/android-sdk/files/default/android-wait-for-emulator - - chmod +x android-wait-for-emulator - - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter sys-img-x86-google_apis-${ANDROID_COMPILE_SDK} - - echo no | android-sdk-linux/tools/android create avd -n test -t android-${ANDROID_COMPILE_SDK} --abi google_apis/x86 - - android-sdk-linux/tools/emulator64-x86 -avd test -no-window -no-audio & - - ./android-wait-for-emulator - - adb shell input keyevent 82 - - ./gradlew cAT + - ./gradlew -Pci --console=plain :app:testDebug diff --git a/lib/gitlab/kubernetes/kube_client.rb b/lib/gitlab/kubernetes/kube_client.rb index e88a15b8acd..f266177bec1 100644 --- a/lib/gitlab/kubernetes/kube_client.rb +++ b/lib/gitlab/kubernetes/kube_client.rb @@ -13,11 +13,21 @@ module Gitlab class KubeClient include Gitlab::Utils::StrongMemoize - SUPPORTED_API_GROUPS = [ - 'api', - 'apis/rbac.authorization.k8s.io', - 'apis/extensions' - ].freeze + SUPPORTED_API_GROUPS = { + core: { group: 'api', version: 'v1' }, + rbac: { group: 'apis/rbac.authorization.k8s.io', version: 'v1' }, + extensions: { group: 'apis/extensions', version: 'v1beta1' } + }.freeze + + SUPPORTED_API_GROUPS.each do |name, params| + client_method_name = "#{name}_client".to_sym + + define_method(client_method_name) do + strong_memoize(client_method_name) do + build_kubeclient(params[:group], params[:version]) + end + end + end # Core API methods delegates to the core api group client delegate :get_pods, @@ -62,48 +72,21 @@ module Gitlab :watch_pod_log, to: :core_client - def initialize(api_prefix, api_groups = ['api'], api_version = 'v1', **kubeclient_options) - raise ArgumentError unless check_api_groups_supported?(api_groups) + attr_reader :api_prefix, :kubeclient_options + def initialize(api_prefix, **kubeclient_options) @api_prefix = api_prefix - @api_groups = api_groups - @api_version = api_version @kubeclient_options = kubeclient_options end - def discover! - clients.each(&:discover) - end - - def clients - hashed_clients.values - end - - def core_client - hashed_clients['api'] - end - - def rbac_client - hashed_clients['apis/rbac.authorization.k8s.io'] - end - - def extensions_client - hashed_clients['apis/extensions'] - end - - def hashed_clients - strong_memoize(:hashed_clients) do - @api_groups.map do |api_group| - api_url = join_api_url(@api_prefix, api_group) - [api_group, ::Kubeclient::Client.new(api_url, @api_version, **@kubeclient_options)] - end.to_h - end - end - private - def check_api_groups_supported?(api_groups) - api_groups.all? {|api_group| SUPPORTED_API_GROUPS.include?(api_group) } + def build_kubeclient(api_group, api_version) + ::Kubeclient::Client.new( + join_api_url(api_prefix, api_group), + api_version, + **kubeclient_options + ) end def join_api_url(api_prefix, api_path) diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index 1484676eea3..2023d4b0bd0 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -297,6 +297,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do expect(response).to match_response_schema('job/job_details') expect(json_response['runners']['online']).to be false expect(json_response['runners']['available']).to be false + expect(json_response['stuck']).to be true end end @@ -309,6 +310,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do expect(response).to match_response_schema('job/job_details') expect(json_response['runners']['online']).to be false expect(json_response['runners']['available']).to be true + expect(json_response['stuck']).to be true end end diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index c3902ecdd17..b3bea92e635 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -721,6 +721,62 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do expect(page).not_to have_css('.js-job-sidebar.right-sidebar-collpased') end end + + context 'stuck', :js do + before do + visit project_job_path(project, job) + wait_for_requests + end + + context 'without active runners available' do + let(:runner) { create(:ci_runner, :instance, active: false) } + let(:job) { create(:ci_build, :pending, pipeline: pipeline, runner: runner) } + + it 'renders message about job being stuck because no runners are active' do + expect(page).to have_css('.js-stuck-no-active-runner') + expect(page).to have_content("This job is stuck, because you don't have any active runners that can run this job.") + end + end + + context 'when available runners can not run specified tag' do + let(:runner) { create(:ci_runner, :instance, active: false) } + let(:job) { create(:ci_build, :pending, pipeline: pipeline, runner: runner, tag_list: %w(docker linux)) } + + it 'renders message about job being stuck because of no runners with the specified tags' do + expect(page).to have_css('.js-stuck-with-tags') + expect(page).to have_content("This job is stuck, because you don't have any active runners online with any of these tags assigned to them:") + end + end + + context 'when runners are offline and build has tags' do + let(:runner) { create(:ci_runner, :instance, active: true) } + let(:job) { create(:ci_build, :pending, pipeline: pipeline, runner: runner, tag_list: %w(docker linux)) } + + it 'renders message about job being stuck because of no runners with the specified tags' do + expect(page).to have_css('.js-stuck-with-tags') + expect(page).to have_content("This job is stuck, because you don't have any active runners online with any of these tags assigned to them:") + end + end + + context 'without any runners available' do + let(:job) { create(:ci_build, :pending, pipeline: pipeline) } + + it 'renders message about job being stuck because not runners are available' do + expect(page).to have_css('.js-stuck-no-active-runner') + expect(page).to have_content("This job is stuck, because you don't have any active runners that can run this job.") + end + end + + context 'without available runners online' do + let(:runner) { create(:ci_runner, :instance, active: true) } + let(:job) { create(:ci_build, :pending, pipeline: pipeline, runner: runner) } + + it 'renders message about job being stuck because runners are offline' do + expect(page).to have_css('.js-stuck-no-runners') + expect(page).to have_content("This job is stuck, because the project doesn't have any runners online assigned to it.") + end + end + end end describe "POST /:project/jobs/:id/cancel", :js do diff --git a/spec/fixtures/api/schemas/job/job_details.json b/spec/fixtures/api/schemas/job/job_details.json index 8218474705c..cdf7b049ab6 100644 --- a/spec/fixtures/api/schemas/job/job_details.json +++ b/spec/fixtures/api/schemas/job/job_details.json @@ -18,6 +18,7 @@ "runner": { "$ref": "runner.json" }, "runners": { "$ref": "runners.json" }, "has_trace": { "type": "boolean" }, - "stage": { "type": "string" } + "stage": { "type": "string" }, + "stuck": { "type": "boolean" } } } diff --git a/spec/javascripts/datetime_utility_spec.js b/spec/javascripts/datetime_utility_spec.js index 9fedbcc4c25..2821f4d6793 100644 --- a/spec/javascripts/datetime_utility_spec.js +++ b/spec/javascripts/datetime_utility_spec.js @@ -192,3 +192,163 @@ describe('formatTime', () => { }); }); }); + +describe('datefix', () => { + describe('pad', () => { + it('should add a 0 when length is smaller than 2', () => { + expect(datetimeUtility.pad(2)).toEqual('02'); + }); + + it('should not add a zero when lenght matches the default', () => { + expect(datetimeUtility.pad(12)).toEqual('12'); + }); + + it('should add a 0 when lenght is smaller than the provided', () => { + expect(datetimeUtility.pad(12, 3)).toEqual('012'); + }); + }); + + describe('parsePikadayDate', () => { + // removed because of https://gitlab.com/gitlab-org/gitlab-ce/issues/39834 + }); + + describe('pikadayToString', () => { + it('should format a UTC date into yyyy-mm-dd format', () => { + expect(datetimeUtility.pikadayToString(new Date('2020-01-29:00:00'))).toEqual('2020-01-29'); + }); + }); +}); + +describe('prettyTime methods', () => { + const assertTimeUnits = (obj, minutes, hours, days, weeks) => { + expect(obj.minutes).toBe(minutes); + expect(obj.hours).toBe(hours); + expect(obj.days).toBe(days); + expect(obj.weeks).toBe(weeks); + }; + + describe('parseSeconds', () => { + it('should correctly parse a negative value', () => { + const zeroSeconds = datetimeUtility.parseSeconds(-1000); + + assertTimeUnits(zeroSeconds, 16, 0, 0, 0); + }); + + it('should correctly parse a zero value', () => { + const zeroSeconds = datetimeUtility.parseSeconds(0); + + assertTimeUnits(zeroSeconds, 0, 0, 0, 0); + }); + + it('should correctly parse a small non-zero second values', () => { + const subOneMinute = datetimeUtility.parseSeconds(10); + const aboveOneMinute = datetimeUtility.parseSeconds(100); + const manyMinutes = datetimeUtility.parseSeconds(1000); + + assertTimeUnits(subOneMinute, 0, 0, 0, 0); + assertTimeUnits(aboveOneMinute, 1, 0, 0, 0); + assertTimeUnits(manyMinutes, 16, 0, 0, 0); + }); + + it('should correctly parse large second values', () => { + const aboveOneHour = datetimeUtility.parseSeconds(4800); + const aboveOneDay = datetimeUtility.parseSeconds(110000); + const aboveOneWeek = datetimeUtility.parseSeconds(25000000); + + assertTimeUnits(aboveOneHour, 20, 1, 0, 0); + assertTimeUnits(aboveOneDay, 33, 6, 3, 0); + assertTimeUnits(aboveOneWeek, 26, 0, 3, 173); + }); + + it('should correctly accept a custom param for hoursPerDay', () => { + const config = { hoursPerDay: 24 }; + + const aboveOneHour = datetimeUtility.parseSeconds(4800, config); + const aboveOneDay = datetimeUtility.parseSeconds(110000, config); + const aboveOneWeek = datetimeUtility.parseSeconds(25000000, config); + + assertTimeUnits(aboveOneHour, 20, 1, 0, 0); + assertTimeUnits(aboveOneDay, 33, 6, 1, 0); + assertTimeUnits(aboveOneWeek, 26, 8, 4, 57); + }); + + it('should correctly accept a custom param for daysPerWeek', () => { + const config = { daysPerWeek: 7 }; + + const aboveOneHour = datetimeUtility.parseSeconds(4800, config); + const aboveOneDay = datetimeUtility.parseSeconds(110000, config); + const aboveOneWeek = datetimeUtility.parseSeconds(25000000, config); + + assertTimeUnits(aboveOneHour, 20, 1, 0, 0); + assertTimeUnits(aboveOneDay, 33, 6, 3, 0); + assertTimeUnits(aboveOneWeek, 26, 0, 0, 124); + }); + + it('should correctly accept custom params for daysPerWeek and hoursPerDay', () => { + const config = { daysPerWeek: 55, hoursPerDay: 14 }; + + const aboveOneHour = datetimeUtility.parseSeconds(4800, config); + const aboveOneDay = datetimeUtility.parseSeconds(110000, config); + const aboveOneWeek = datetimeUtility.parseSeconds(25000000, config); + + assertTimeUnits(aboveOneHour, 20, 1, 0, 0); + assertTimeUnits(aboveOneDay, 33, 2, 2, 0); + assertTimeUnits(aboveOneWeek, 26, 0, 1, 9); + }); + }); + + describe('stringifyTime', () => { + it('should stringify values with all non-zero units', () => { + const timeObject = { + weeks: 1, + days: 4, + hours: 7, + minutes: 20, + }; + + const timeString = datetimeUtility.stringifyTime(timeObject); + + expect(timeString).toBe('1w 4d 7h 20m'); + }); + + it('should stringify values with some non-zero units', () => { + const timeObject = { + weeks: 0, + days: 4, + hours: 0, + minutes: 20, + }; + + const timeString = datetimeUtility.stringifyTime(timeObject); + + expect(timeString).toBe('4d 20m'); + }); + + it('should stringify values with no non-zero units', () => { + const timeObject = { + weeks: 0, + days: 0, + hours: 0, + minutes: 0, + }; + + const timeString = datetimeUtility.stringifyTime(timeObject); + + expect(timeString).toBe('0m'); + }); + }); + + describe('abbreviateTime', () => { + it('should abbreviate stringified times for weeks', () => { + const fullTimeString = '1w 3d 4h 5m'; + + expect(datetimeUtility.abbreviateTime(fullTimeString)).toBe('1w'); + }); + + it('should abbreviate stringified times for non-weeks', () => { + const fullTimeString = '0w 3d 4h 5m'; + + expect(datetimeUtility.abbreviateTime(fullTimeString)).toBe('3d'); + }); + }); +}); diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js index 85c1926fcb1..bb623953710 100644 --- a/spec/javascripts/diffs/store/actions_spec.js +++ b/spec/javascripts/diffs/store/actions_spec.js @@ -27,7 +27,6 @@ import actions, { toggleShowTreeList, } from '~/diffs/store/actions'; import * as types from '~/diffs/store/mutation_types'; -import { reduceDiscussionsToLineCodes } from '~/notes/stores/utils'; import axios from '~/lib/utils/axios_utils'; import testAction from '../../helpers/vuex_action_helper'; @@ -152,7 +151,7 @@ describe('DiffsStoreActions', () => { original_position: diffPosition, }; - const discussions = reduceDiscussionsToLineCodes([singleDiscussion]); + const discussions = [singleDiscussion]; testAction( assignDiscussionsToDiff, @@ -162,8 +161,7 @@ describe('DiffsStoreActions', () => { { type: types.SET_LINE_DISCUSSIONS_FOR_FILE, payload: { - fileHash: 'ABC', - discussions: [singleDiscussion], + discussion: singleDiscussion, diffPositionByLineCode: { ABC_1_1: { baseSha: 'abc', @@ -581,7 +579,6 @@ describe('DiffsStoreActions', () => { describe('saveDiffDiscussion', () => { beforeEach(() => { spyOnDependency(actions, 'getNoteFormData').and.returnValue('testData'); - spyOnDependency(actions, 'reduceDiscussionsToLineCodes').and.returnValue('discussions'); }); it('dispatches actions', done => { @@ -602,7 +599,7 @@ describe('DiffsStoreActions', () => { .then(() => { expect(dispatch.calls.argsFor(0)).toEqual(['saveNote', 'testData', { root: true }]); expect(dispatch.calls.argsFor(1)).toEqual(['updateDiscussion', 'test', { root: true }]); - expect(dispatch.calls.argsFor(2)).toEqual(['assignDiscussionsToDiff', 'discussions']); + expect(dispatch.calls.argsFor(2)).toEqual(['assignDiscussionsToDiff', ['discussion']]); }) .then(done) .catch(done.fail); diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js index b7e28391419..4b6d3d5bcba 100644 --- a/spec/javascripts/diffs/store/mutations_spec.js +++ b/spec/javascripts/diffs/store/mutations_spec.js @@ -198,40 +198,32 @@ describe('DiffsStoreMutations', () => { }, ], }; - const discussions = [ - { - id: 1, - line_code: 'ABC_1', - diff_discussion: true, - resolvable: true, - original_position: diffPosition, - position: diffPosition, + const discussion = { + id: 1, + line_code: 'ABC_1', + diff_discussion: true, + resolvable: true, + original_position: diffPosition, + position: diffPosition, + diff_file: { + file_hash: state.diffFiles[0].fileHash, }, - { - id: 2, - line_code: 'ABC_1', - diff_discussion: true, - resolvable: true, - original_position: diffPosition, - position: diffPosition, - }, - ]; + }; const diffPositionByLineCode = { ABC_1: diffPosition, }; mutations[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { - fileHash: 'ABC', - discussions, + discussion, diffPositionByLineCode, }); - expect(state.diffFiles[0].parallelDiffLines[0].left.discussions.length).toEqual(2); - expect(state.diffFiles[0].parallelDiffLines[0].left.discussions[1].id).toEqual(2); + expect(state.diffFiles[0].parallelDiffLines[0].left.discussions.length).toEqual(1); + expect(state.diffFiles[0].parallelDiffLines[0].left.discussions[0].id).toEqual(1); - expect(state.diffFiles[0].highlightedDiffLines[0].discussions.length).toEqual(2); - expect(state.diffFiles[0].highlightedDiffLines[0].discussions[1].id).toEqual(2); + expect(state.diffFiles[0].highlightedDiffLines[0].discussions.length).toEqual(1); + expect(state.diffFiles[0].highlightedDiffLines[0].discussions[0].id).toEqual(1); }); it('should add legacy discussions to the given line', () => { @@ -272,36 +264,30 @@ describe('DiffsStoreMutations', () => { }, ], }; - const discussions = [ - { - id: 1, - line_code: 'ABC_1', - diff_discussion: true, - active: true, + const discussion = { + id: 1, + line_code: 'ABC_1', + diff_discussion: true, + active: true, + diff_file: { + file_hash: state.diffFiles[0].fileHash, }, - { - id: 2, - line_code: 'ABC_1', - diff_discussion: true, - active: true, - }, - ]; + }; const diffPositionByLineCode = { ABC_1: diffPosition, }; mutations[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { - fileHash: 'ABC', - discussions, + discussion, diffPositionByLineCode, }); - expect(state.diffFiles[0].parallelDiffLines[0].left.discussions.length).toEqual(2); - expect(state.diffFiles[0].parallelDiffLines[0].left.discussions[1].id).toEqual(2); + expect(state.diffFiles[0].parallelDiffLines[0].left.discussions.length).toEqual(1); + expect(state.diffFiles[0].parallelDiffLines[0].left.discussions[0].id).toEqual(1); - expect(state.diffFiles[0].highlightedDiffLines[0].discussions.length).toEqual(2); - expect(state.diffFiles[0].highlightedDiffLines[0].discussions[1].id).toEqual(2); + expect(state.diffFiles[0].highlightedDiffLines[0].discussions.length).toEqual(1); + expect(state.diffFiles[0].highlightedDiffLines[0].discussions[0].id).toEqual(1); }); }); diff --git a/spec/javascripts/jobs/components/job_app_spec.js b/spec/javascripts/jobs/components/job_app_spec.js index e6d403dc826..288c06d6615 100644 --- a/spec/javascripts/jobs/components/job_app_spec.js +++ b/spec/javascripts/jobs/components/job_app_spec.js @@ -88,7 +88,9 @@ describe('Job App ', () => { describe('triggered job', () => { beforeEach(() => { - mock.onGet(props.endpoint).replyOnce(200, Object.assign({}, job, { started: '2017-05-24T10:59:52.000+01:00' })); + mock + .onGet(props.endpoint) + .replyOnce(200, Object.assign({}, job, { started: '2017-05-24T10:59:52.000+01:00' })); vm = mountComponentWithStore(Component, { props, store }); }); @@ -133,57 +135,106 @@ describe('Job App ', () => { }); describe('stuck block', () => { - it('renders stuck block when there are no runners', done => { - mock.onGet(props.endpoint).replyOnce( - 200, - Object.assign({}, job, { - status: { - group: 'pending', - icon: 'status_pending', - label: 'pending', - text: 'pending', - details_path: 'path', - }, - runners: { - available: false, - }, - }), - ); - vm = mountComponentWithStore(Component, { props, store }); - - setTimeout(() => { - expect(vm.$el.querySelector('.js-job-stuck')).not.toBeNull(); + describe('without active runners availabl', () => { + it('renders stuck block when there are no runners', done => { + mock.onGet(props.endpoint).replyOnce( + 200, + Object.assign({}, job, { + status: { + group: 'pending', + icon: 'status_pending', + label: 'pending', + text: 'pending', + details_path: 'path', + }, + stuck: true, + runners: { + available: false, + online: false, + }, + tags: [], + }), + ); + vm = mountComponentWithStore(Component, { props, store }); - done(); - }, 0); + setTimeout(() => { + expect(vm.$el.querySelector('.js-job-stuck')).not.toBeNull(); + expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain( + "This job is stuck, because you don't have any active runners that can run this job.", + ); + done(); + }, 0); + }); }); - it('renders tags in stuck block when there are no runners', done => { - mock.onGet(props.endpoint).replyOnce( - 200, - Object.assign({}, job, { - status: { - group: 'pending', - icon: 'status_pending', - label: 'pending', - text: 'pending', - details_path: 'path', - }, - runners: { - available: false, - }, - }), - ); + describe('when available runners can not run specified tag', () => { + it('renders tags in stuck block when there are no runners', done => { + mock.onGet(props.endpoint).replyOnce( + 200, + Object.assign({}, job, { + status: { + group: 'pending', + icon: 'status_pending', + label: 'pending', + text: 'pending', + details_path: 'path', + }, + stuck: true, + runners: { + available: false, + online: false, + }, + }), + ); - vm = mountComponentWithStore(Component, { - props, - store, + vm = mountComponentWithStore(Component, { + props, + store, + }); + + setTimeout(() => { + expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(job.tags[0]); + expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain( + "This job is stuck, because you don't have any active runners online with any of these tags assigned to them:", + ); + done(); + }, 0); }); + }); - setTimeout(() => { - expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(job.tags[0]); - done(); - }, 0); + describe('when runners are offline and build has tags', () => { + it('renders message about job being stuck because of no runners with the specified tags', done => { + mock.onGet(props.endpoint).replyOnce( + 200, + Object.assign({}, job, { + status: { + group: 'pending', + icon: 'status_pending', + label: 'pending', + text: 'pending', + details_path: 'path', + }, + stuck: true, + runners: { + available: true, + online: true, + }, + }), + ); + + vm = mountComponentWithStore(Component, { + props, + store, + }); + + setTimeout(() => { + expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(job.tags[0]); + expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain( + "This job is stuck, because you don't have any active runners online with any of these tags assigned to them:", + ); + done(); + }, 0); + }) }); it('does not renders stuck block when there are no runners', done => { @@ -418,10 +469,11 @@ describe('Job App ', () => { vm.$store.state.trace = 'Update'; setTimeout(() => { - expect(vm.$el.querySelector('.js-build-trace').textContent.trim()).not.toContain('Update'); - expect(vm.$el.querySelector('.js-build-trace').textContent.trim()).toContain( - 'Different', + expect(vm.$el.querySelector('.js-build-trace').textContent.trim()).not.toContain( + 'Update', ); + + expect(vm.$el.querySelector('.js-build-trace').textContent.trim()).toContain('Different'); done(); }, 0); }); diff --git a/spec/javascripts/jobs/store/getters_spec.js b/spec/javascripts/jobs/store/getters_spec.js index 46a20122ec8..34e9707eadd 100644 --- a/spec/javascripts/jobs/store/getters_spec.js +++ b/spec/javascripts/jobs/store/getters_spec.js @@ -175,43 +175,37 @@ describe('Job Store Getters', () => { }); }); - describe('isJobStuck', () => { - describe('when job is pending and runners are not available', () => { + describe('hasRunnersForProject', () => { + describe('with available and offline runners', () => { it('returns true', () => { - localState.job.status = { - group: 'pending', - }; localState.job.runners = { - available: false, + available: true, + online: false }; - expect(getters.isJobStuck(localState)).toEqual(true); + expect(getters.hasRunnersForProject(localState)).toEqual(true); }); }); - describe('when job is not pending', () => { + describe('with non available runners', () => { it('returns false', () => { - localState.job.status = { - group: 'running', - }; localState.job.runners = { available: false, + online: false }; - expect(getters.isJobStuck(localState)).toEqual(false); + expect(getters.hasRunnersForProject(localState)).toEqual(false); }); }); - describe('when runners are available', () => { + describe('with online runners', () => { it('returns false', () => { - localState.job.status = { - group: 'pending', - }; localState.job.runners = { - available: true, + available: false, + online: true }; - expect(getters.isJobStuck(localState)).toEqual(false); + expect(getters.hasRunnersForProject(localState)).toEqual(false); }); }); }); diff --git a/spec/javascripts/lib/utils/datefix_spec.js b/spec/javascripts/lib/utils/datefix_spec.js deleted file mode 100644 index a9f3abcf2a4..00000000000 --- a/spec/javascripts/lib/utils/datefix_spec.js +++ /dev/null @@ -1,27 +0,0 @@ -import { pad, pikadayToString } from '~/lib/utils/datefix'; - -describe('datefix', () => { - describe('pad', () => { - it('should add a 0 when length is smaller than 2', () => { - expect(pad(2)).toEqual('02'); - }); - - it('should not add a zero when lenght matches the default', () => { - expect(pad(12)).toEqual('12'); - }); - - it('should add a 0 when lenght is smaller than the provided', () => { - expect(pad(12, 3)).toEqual('012'); - }); - }); - - describe('parsePikadayDate', () => { - // removed because of https://gitlab.com/gitlab-org/gitlab-ce/issues/39834 - }); - - describe('pikadayToString', () => { - it('should format a UTC date into yyyy-mm-dd format', () => { - expect(pikadayToString(new Date('2020-01-29:00:00'))).toEqual('2020-01-29'); - }); - }); -}); diff --git a/spec/javascripts/pretty_time_spec.js b/spec/javascripts/pretty_time_spec.js deleted file mode 100644 index 158cd76dd13..00000000000 --- a/spec/javascripts/pretty_time_spec.js +++ /dev/null @@ -1,135 +0,0 @@ -import { parseSeconds, abbreviateTime, stringifyTime } from '~/lib/utils/pretty_time'; - -function assertTimeUnits(obj, minutes, hours, days, weeks) { - expect(obj.minutes).toBe(minutes); - expect(obj.hours).toBe(hours); - expect(obj.days).toBe(days); - expect(obj.weeks).toBe(weeks); -} - -describe('prettyTime methods', () => { - describe('parseSeconds', () => { - it('should correctly parse a negative value', () => { - const zeroSeconds = parseSeconds(-1000); - - assertTimeUnits(zeroSeconds, 16, 0, 0, 0); - }); - - it('should correctly parse a zero value', () => { - const zeroSeconds = parseSeconds(0); - - assertTimeUnits(zeroSeconds, 0, 0, 0, 0); - }); - - it('should correctly parse a small non-zero second values', () => { - const subOneMinute = parseSeconds(10); - const aboveOneMinute = parseSeconds(100); - const manyMinutes = parseSeconds(1000); - - assertTimeUnits(subOneMinute, 0, 0, 0, 0); - assertTimeUnits(aboveOneMinute, 1, 0, 0, 0); - assertTimeUnits(manyMinutes, 16, 0, 0, 0); - }); - - it('should correctly parse large second values', () => { - const aboveOneHour = parseSeconds(4800); - const aboveOneDay = parseSeconds(110000); - const aboveOneWeek = parseSeconds(25000000); - - assertTimeUnits(aboveOneHour, 20, 1, 0, 0); - assertTimeUnits(aboveOneDay, 33, 6, 3, 0); - assertTimeUnits(aboveOneWeek, 26, 0, 3, 173); - }); - - it('should correctly accept a custom param for hoursPerDay', () => { - const config = { hoursPerDay: 24 }; - - const aboveOneHour = parseSeconds(4800, config); - const aboveOneDay = parseSeconds(110000, config); - const aboveOneWeek = parseSeconds(25000000, config); - - assertTimeUnits(aboveOneHour, 20, 1, 0, 0); - assertTimeUnits(aboveOneDay, 33, 6, 1, 0); - assertTimeUnits(aboveOneWeek, 26, 8, 4, 57); - }); - - it('should correctly accept a custom param for daysPerWeek', () => { - const config = { daysPerWeek: 7 }; - - const aboveOneHour = parseSeconds(4800, config); - const aboveOneDay = parseSeconds(110000, config); - const aboveOneWeek = parseSeconds(25000000, config); - - assertTimeUnits(aboveOneHour, 20, 1, 0, 0); - assertTimeUnits(aboveOneDay, 33, 6, 3, 0); - assertTimeUnits(aboveOneWeek, 26, 0, 0, 124); - }); - - it('should correctly accept custom params for daysPerWeek and hoursPerDay', () => { - const config = { daysPerWeek: 55, hoursPerDay: 14 }; - - const aboveOneHour = parseSeconds(4800, config); - const aboveOneDay = parseSeconds(110000, config); - const aboveOneWeek = parseSeconds(25000000, config); - - assertTimeUnits(aboveOneHour, 20, 1, 0, 0); - assertTimeUnits(aboveOneDay, 33, 2, 2, 0); - assertTimeUnits(aboveOneWeek, 26, 0, 1, 9); - }); - }); - - describe('stringifyTime', () => { - it('should stringify values with all non-zero units', () => { - const timeObject = { - weeks: 1, - days: 4, - hours: 7, - minutes: 20, - }; - - const timeString = stringifyTime(timeObject); - - expect(timeString).toBe('1w 4d 7h 20m'); - }); - - it('should stringify values with some non-zero units', () => { - const timeObject = { - weeks: 0, - days: 4, - hours: 0, - minutes: 20, - }; - - const timeString = stringifyTime(timeObject); - - expect(timeString).toBe('4d 20m'); - }); - - it('should stringify values with no non-zero units', () => { - const timeObject = { - weeks: 0, - days: 0, - hours: 0, - minutes: 0, - }; - - const timeString = stringifyTime(timeObject); - - expect(timeString).toBe('0m'); - }); - }); - - describe('abbreviateTime', () => { - it('should abbreviate stringified times for weeks', () => { - const fullTimeString = '1w 3d 4h 5m'; - - expect(abbreviateTime(fullTimeString)).toBe('1w'); - }); - - it('should abbreviate stringified times for non-weeks', () => { - const fullTimeString = '0w 3d 4h 5m'; - - expect(abbreviateTime(fullTimeString)).toBe('3d'); - }); - }); -}); diff --git a/spec/lib/gitlab/kubernetes/kube_client_spec.rb b/spec/lib/gitlab/kubernetes/kube_client_spec.rb index 53c5a4e7c94..eed4135d8a2 100644 --- a/spec/lib/gitlab/kubernetes/kube_client_spec.rb +++ b/spec/lib/gitlab/kubernetes/kube_client_spec.rb @@ -6,104 +6,63 @@ describe Gitlab::Kubernetes::KubeClient do include KubernetesHelpers let(:api_url) { 'https://kubernetes.example.com/prefix' } - let(:api_groups) { ['api', 'apis/rbac.authorization.k8s.io'] } - let(:api_version) { 'v1' } let(:kubeclient_options) { { auth_options: { bearer_token: 'xyz' } } } - let(:client) { described_class.new(api_url, api_groups, api_version, kubeclient_options) } + let(:client) { described_class.new(api_url, kubeclient_options) } before do stub_kubeclient_discover(api_url) end - describe '#hashed_clients' do - subject { client.hashed_clients } - - it 'has keys from api groups' do - expect(subject.keys).to match_array api_groups - end - - it 'has values of Kubeclient::Client' do - expect(subject.values).to all(be_an_instance_of Kubeclient::Client) - end - end - - describe '#clients' do - subject { client.clients } - - it 'is not empty' do - is_expected.to be_present - end - - it 'is an array of Kubeclient::Client objects' do - is_expected.to all(be_an_instance_of Kubeclient::Client) - end - - it 'has each API group url' do - expected_urls = api_groups.map { |group| "#{api_url}/#{group}" } - - expect(subject.map(&:api_endpoint).map(&:to_s)).to match_array(expected_urls) + shared_examples 'a Kubeclient' do + it 'is a Kubeclient::Client' do + is_expected.to be_an_instance_of Kubeclient::Client end it 'has the kubeclient options' do - subject.each do |client| - expect(client.auth_options).to eq({ bearer_token: 'xyz' }) - end - end - - it 'has the api_version' do - subject.each do |client| - expect(client.instance_variable_get(:@api_version)).to eq('v1') - end + expect(subject.auth_options).to eq({ bearer_token: 'xyz' }) end end describe '#core_client' do subject { client.core_client } - it 'is a Kubeclient::Client' do - is_expected.to be_an_instance_of Kubeclient::Client - end + it_behaves_like 'a Kubeclient' it 'has the core API endpoint' do expect(subject.api_endpoint.to_s).to match(%r{\/api\Z}) end + + it 'has the api_version' do + expect(subject.instance_variable_get(:@api_version)).to eq('v1') + end end describe '#rbac_client' do subject { client.rbac_client } - it 'is a Kubeclient::Client' do - is_expected.to be_an_instance_of Kubeclient::Client - end + it_behaves_like 'a Kubeclient' it 'has the RBAC API group endpoint' do expect(subject.api_endpoint.to_s).to match(%r{\/apis\/rbac.authorization.k8s.io\Z}) end + + it 'has the api_version' do + expect(subject.instance_variable_get(:@api_version)).to eq('v1') + end end describe '#extensions_client' do subject { client.extensions_client } - let(:api_groups) { ['apis/extensions'] } - - it 'is a Kubeclient::Client' do - is_expected.to be_an_instance_of Kubeclient::Client - end + it_behaves_like 'a Kubeclient' it 'has the extensions API group endpoint' do expect(subject.api_endpoint.to_s).to match(%r{\/apis\/extensions\Z}) end - end - describe '#discover!' do - it 'makes a discovery request for each API group' do - client.discover! - - api_groups.each do |api_group| - discovery_url = api_url + '/' + api_group + '/v1' - expect(WebMock).to have_requested(:get, discovery_url).once - end + it 'has the api_version' do + expect(subject.instance_variable_get(:@api_version)).to eq('v1beta1') end end @@ -156,21 +115,12 @@ describe Gitlab::Kubernetes::KubeClient do it 'responds to the method' do expect(client).to respond_to method end - - context 'no rbac client' do - let(:api_groups) { ['api'] } - - it 'throws an error' do - expect { client.public_send(method) }.to raise_error(Module::DelegationError) - end - end end end end describe 'extensions API group' do let(:api_groups) { ['apis/extensions'] } - let(:api_version) { 'v1beta1' } let(:extensions_client) { client.extensions_client } describe '#get_deployments' do @@ -181,22 +131,11 @@ describe Gitlab::Kubernetes::KubeClient do it 'responds to the method' do expect(client).to respond_to :get_deployments end - - context 'no extensions client' do - let(:api_groups) { ['api'] } - let(:api_version) { 'v1' } - - it 'throws an error' do - expect { client.get_deployments }.to raise_error(Module::DelegationError) - end - end end end describe 'non-entity methods' do it 'does not proxy for non-entity methods' do - expect(client.clients.first).to respond_to :proxy_url - expect(client).not_to respond_to :proxy_url end @@ -211,14 +150,6 @@ describe Gitlab::Kubernetes::KubeClient do it 'is delegated to the core client' do expect(client).to delegate_method(:get_pod_log).to(:core_client) end - - context 'when no core client' do - let(:api_groups) { ['apis/extensions'] } - - it 'throws an error' do - expect { client.get_pod_log('pod-name') }.to raise_error(Module::DelegationError) - end - end end describe '#watch_pod_log' do @@ -227,14 +158,6 @@ describe Gitlab::Kubernetes::KubeClient do it 'is delegated to the core client' do expect(client).to delegate_method(:watch_pod_log).to(:core_client) end - - context 'when no core client' do - let(:api_groups) { ['apis/extensions'] } - - it 'throws an error' do - expect { client.watch_pod_log('pod-name') }.to raise_error(Module::DelegationError) - end - end end describe 'methods that do not exist on any client' do diff --git a/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb b/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb index 065d021db5e..b096f1fa4fb 100644 --- a/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb +++ b/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb @@ -16,7 +16,6 @@ describe Clusters::Gcp::Kubernetes::CreateServiceAccountService do let(:kubeclient) do Gitlab::Kubernetes::KubeClient.new( api_url, - ['api', 'apis/rbac.authorization.k8s.io'], auth_options: { username: username, password: password } ) end diff --git a/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb b/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb index c543de21d5b..2355827fa5a 100644 --- a/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb +++ b/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb @@ -11,7 +11,6 @@ describe Clusters::Gcp::Kubernetes::FetchKubernetesTokenService do let(:kubeclient) do Gitlab::Kubernetes::KubeClient.new( api_url, - ['api', 'apis/rbac.authorization.k8s.io'], auth_options: { username: username, password: password } ) end |