diff options
92 files changed, 1266 insertions, 534 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5f0aa51a805..6a06f3ca325 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.12-git-2.24-lfs-2.9-chrome-73.0-node-12.x-yarn-1.16-postgresql-9.6-graphicsmagick-1.3.33" +image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.12-git-2.24-lfs-2.9-chrome-73.0-node-12.x-yarn-1.16-postgresql-9.6-graphicsmagick-1.3.34" stages: - sync diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml index 15479bd59d5..50358d65513 100644 --- a/.gitlab/ci/frontend.gitlab-ci.yml +++ b/.gitlab/ci/frontend.gitlab-ci.yml @@ -78,7 +78,7 @@ - .default-retry - .default-before_script - .assets-compile-cache - image: registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-git-2.24-lfs-2.9-chrome-73.0-node-12.x-yarn-1.16-graphicsmagick-1.3.33-docker-19.03.1 + image: registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-git-2.24-lfs-2.9-chrome-73.0-node-12.x-yarn-1.16-graphicsmagick-1.3.34-docker-19.03.1 stage: prepare services: - docker:19.03.0-dind diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml index d6fcddee6e8..8253e74b2e8 100644 --- a/.gitlab/ci/global.gitlab-ci.yml +++ b/.gitlab/ci/global.gitlab-ci.yml @@ -203,7 +203,7 @@ - name: redis:alpine .use-pg10: - image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.12-git-2.24-lfs-2.9-chrome-73.0-node-12.x-yarn-1.16-postgresql-10-graphicsmagick-1.3.33" + image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.12-git-2.24-lfs-2.9-chrome-73.0-node-12.x-yarn-1.16-postgresql-10-graphicsmagick-1.3.34" services: - name: postgres:10.9 command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] @@ -217,7 +217,7 @@ - name: elasticsearch:6.4.2 .use-pg10-ee: - image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.12-git-2.24-lfs-2.9-chrome-73.0-node-12.x-yarn-1.16-postgresql-10-graphicsmagick-1.3.33" + image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.12-git-2.24-lfs-2.9-chrome-73.0-node-12.x-yarn-1.16-postgresql-10-graphicsmagick-1.3.34" services: - name: postgres:10.9 command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index 463d1427805..878b54f7d53 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -374,7 +374,7 @@ export default { <div :data-can-create-note="getNoteableData.current_user.can_create_note" - class="files d-flex prepend-top-default" + class="files d-flex" > <div v-show="showTreeList" diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue index 24542126b07..63ce43a193d 100644 --- a/app/assets/javascripts/diffs/components/compare_versions.vue +++ b/app/assets/javascripts/diffs/components/compare_versions.vue @@ -1,5 +1,4 @@ <script> -/* eslint-disable @gitlab/vue-i18n/no-bare-strings */ import { mapActions, mapGetters, mapState } from 'vuex'; import { GlTooltipDirective, GlLink, GlButton } from '@gitlab/ui'; import { __ } from '~/locale'; @@ -63,9 +62,6 @@ export default { showDropdowns() { return !this.commit && this.mergeRequestDiffs.length; }, - fileTreeIcon() { - return this.showTreeList ? 'collapse-left' : 'expand-left'; - }, toggleFileBrowserTitle() { return this.showTreeList ? __('Hide file browser') : __('Show file browser'); }, @@ -91,7 +87,7 @@ export default { </script> <template> - <div class="mr-version-controls border-top border-bottom"> + <div class="mr-version-controls border-top"> <div class="mr-version-menus-container content-block" :class="{ @@ -108,17 +104,17 @@ export default { :title="toggleFileBrowserTitle" @click="toggleShowTreeList" > - <icon :name="fileTreeIcon" /> + <icon name="file-tree" /> </button> <div v-if="showDropdowns" class="d-flex align-items-center compare-versions-container"> - Changes between + {{ __('Compare') }} <compare-versions-dropdown :other-versions="mergeRequestDiffs" :merge-request-version="mergeRequestDiff" :show-commit-count="true" class="mr-version-dropdown" /> - and + {{ __('and') }} <compare-versions-dropdown :other-versions="comparableDiffs" :base-version-path="baseVersionPath" diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index ee10a1e92fc..48114f9919c 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -224,7 +224,7 @@ export default { <div v-if="!diffFile.submodule && addMergeRequestButtons" - class="file-actions d-none d-sm-block" + class="file-actions d-none d-sm-flex align-items-center flex-wrap" > <diff-stats :added-lines="diffFile.added_lines" :removed-lines="diffFile.removed_lines" /> <div class="btn-group" role="group"> diff --git a/app/assets/javascripts/diffs/components/diff_stats.vue b/app/assets/javascripts/diffs/components/diff_stats.vue index 0138790cf0e..9d362ceb429 100644 --- a/app/assets/javascripts/diffs/components/diff_stats.vue +++ b/app/assets/javascripts/diffs/components/diff_stats.vue @@ -22,7 +22,7 @@ export default { }, computed: { filesText() { - return n__('File', 'Files', this.diffFilesLength); + return n__('file', 'files', this.diffFilesLength); }, isCompareVersionsHeader() { return Boolean(this.diffFilesLength); @@ -44,13 +44,21 @@ export default { > <div v-if="hasDiffFiles" class="diff-stats-group"> <icon name="doc-code" class="diff-stats-icon text-secondary" /> - <strong>{{ diffFilesLength }} {{ filesText }}</strong> + <span class="text-secondary bold">{{ diffFilesLength }} {{ filesText }}</span> </div> - <div class="diff-stats-group cgreen"> - <icon name="file-addition" class="diff-stats-icon" /> <strong>{{ addedLines }}</strong> + <div + class="diff-stats-group cgreen d-flex align-items-center" + :class="{ bold: isCompareVersionsHeader }" + > + <span>+</span> + <span class="js-file-addition-line">{{ addedLines }}</span> </div> - <div class="diff-stats-group cred"> - <icon name="file-deletion" class="diff-stats-icon" /> <strong>{{ removedLines }}</strong> + <div + class="diff-stats-group cred d-flex align-items-center" + :class="{ bold: isCompareVersionsHeader }" + > + <span>-</span> + <span class="js-file-deletion-line">{{ removedLines }}</span> </div> </div> </template> diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue index 30be2e68e76..b13619a5471 100644 --- a/app/assets/javascripts/diffs/components/tree_list.vue +++ b/app/assets/javascripts/diffs/components/tree_list.vue @@ -58,8 +58,8 @@ export default { this.search = ''; }, }, - searchPlaceholder: sprintf(s__('MergeRequest|Filter files or search with %{modifier_key}+p'), { - modifier_key: /Mac/i.test(navigator.userAgent) ? 'cmd' : 'ctrl', + searchPlaceholder: sprintf(s__('MergeRequest|Search files (%{modifier_key}P)'), { + modifier_key: /Mac/i.test(navigator.userAgent) ? '⌘' : 'Ctrl+', }), }; </script> diff --git a/app/assets/javascripts/error_tracking/components/error_details.vue b/app/assets/javascripts/error_tracking/components/error_details.vue index 88861b7da0e..7abe3be3e99 100644 --- a/app/assets/javascripts/error_tracking/components/error_details.vue +++ b/app/assets/javascripts/error_tracking/components/error_details.vue @@ -54,10 +54,6 @@ export default { type: String, required: true, }, - issueDetailsPath: { - type: String, - required: true, - }, issueStackTracePath: { type: String, required: true, @@ -72,7 +68,7 @@ export default { }, }, apollo: { - GQLerror: { + error: { query, variables() { return { @@ -81,19 +77,19 @@ export default { }; }, pollInterval: 2000, - update: data => data.project.sentryDetailedError, + update: data => data.project.sentryErrors.detailedError, error: () => createFlash(__('Failed to load error details from Sentry.')), result(res) { - if (res.data.project?.sentryDetailedError) { - this.$apollo.queries.GQLerror.stopPolling(); - this.setStatus(this.GQLerror.status); + if (res.data.project?.sentryErrors?.detailedError) { + this.$apollo.queries.error.stopPolling(); + this.setStatus(this.error.status); } }, }, }, data() { return { - GQLerror: null, + error: null, issueCreationInProgress: false, isAlertVisible: false, closedIssueId: null, @@ -101,8 +97,6 @@ export default { }, computed: { ...mapState('details', [ - 'error', - 'loading', 'loadingStacktrace', 'stacktraceData', 'updatingResolveStatus', @@ -114,28 +108,23 @@ export default { return sprintf( __('Reported %{timeAgo} by %{reportedBy}'), { - reportedBy: `<strong>${this.GQLerror.culprit}</strong>`, + reportedBy: `<strong>${this.error.culprit}</strong>`, timeAgo: this.timeFormatted(this.stacktraceData.date_received), }, false, ); }, firstReleaseLink() { - return `${this.error.external_base_url}/releases/${this.GQLerror.firstReleaseShortVersion}`; + return `${this.error.externalBaseUrl}/releases/${this.error.firstReleaseShortVersion}`; }, lastReleaseLink() { - return `${this.error.external_base_url}releases/${this.GQLerror.lastReleaseShortVersion}`; - }, - showDetails() { - return Boolean( - !this.loading && !this.$apollo.queries.GQLerror.loading && this.error && this.GQLerror, - ); + return `${this.error.externalBaseUrl}/releases/${this.error.lastReleaseShortVersion}`; }, showStacktrace() { - return Boolean(!this.loadingStacktrace && this.stacktrace && this.stacktrace.length); + return Boolean(this.stacktrace?.length); }, issueTitle() { - return this.GQLerror.title; + return this.error.title; }, issueDescription() { return sprintf( @@ -144,13 +133,13 @@ export default { ), { description: '# Error Details:\n', - errorUrl: `${this.GQLerror.externalUrl}\n`, - firstSeen: `\n${this.GQLerror.firstSeen}\n`, - lastSeen: `${this.GQLerror.lastSeen}\n`, - countLabel: n__('- Event', '- Events', this.GQLerror.count), - count: `${this.GQLerror.count}\n`, - userCountLabel: n__('- User', '- Users', this.GQLerror.userCount), - userCount: `${this.GQLerror.userCount}\n`, + errorUrl: `${this.error.externalUrl}\n`, + firstSeen: `\n${this.error.firstSeen}\n`, + lastSeen: `${this.error.lastSeen}\n`, + countLabel: n__('- Event', '- Events', this.error.count), + count: `${this.error.count}\n`, + userCountLabel: n__('- User', '- Users', this.error.userCount), + userCount: `${this.error.userCount}\n`, }, false, ); @@ -171,12 +160,10 @@ export default { }, }, mounted() { - this.startPollingDetails(this.issueDetailsPath); this.startPollingStacktrace(this.issueStackTracePath); }, methods: { ...mapActions('details', [ - 'startPollingDetails', 'startPollingStacktrace', 'updateStatus', 'setStatus', @@ -214,10 +201,10 @@ export default { <template> <div> - <div v-if="$apollo.queries.GQLerror.loading || loading" class="py-3"> + <div v-if="$apollo.queries.error.loading" class="py-3"> <gl-loading-icon :size="3" /> </div> - <div v-else-if="showDetails" class="error-details"> + <div v-else-if="error" class="error-details"> <gl-alert v-if="isAlertVisible" @dismiss="isAlertVisible = false"> <gl-sprintf :message=" @@ -232,7 +219,7 @@ export default { <div class="top-area align-items-center justify-content-between py-3"> <span v-if="!loadingStacktrace && stacktrace" v-html="reported"></span> - <div class="d-inline-flex"> + <div class="d-inline-flex ml-lg-auto"> <loading-button :label="ignoreBtnLabel" :loading="updatingIgnoreStatus" @@ -247,10 +234,10 @@ export default { @click="onResolveStatusUpdate" /> <gl-button - v-if="error.gitlab_issue" + v-if="error.gitlabIssuePath" class="ml-2" data-qa-selector="view_issue_button" - :href="error.gitlab_issue" + :href="error.gitlabIssuePath" variant="success" > {{ __('View issue') }} @@ -264,13 +251,13 @@ export default { <gl-form-input class="hidden" name="issue[title]" :value="issueTitle" /> <input name="issue[description]" :value="issueDescription" type="hidden" /> <gl-form-input - :value="GQLerror.sentryId" + :value="error.sentryId" class="hidden" name="issue[sentry_issue_attributes][sentry_issue_identifier]" /> <gl-form-input :value="csrfToken" class="hidden" name="authenticity_token" /> <loading-button - v-if="!error.gitlab_issue" + v-if="!error.gitlabIssuePath" class="btn-success" :label="__('Create issue')" :loading="issueCreationInProgress" @@ -281,8 +268,8 @@ export default { </div> </div> <div> - <tooltip-on-truncate :title="GQLerror.title" truncate-target="child" placement="top"> - <h2 class="text-truncate">{{ GQLerror.title }}</h2> + <tooltip-on-truncate :title="error.title" truncate-target="child" placement="top"> + <h2 class="text-truncate">{{ error.title }}</h2> </tooltip-on-truncate> <template v-if="error.tags"> <gl-badge @@ -297,53 +284,51 @@ export default { </gl-badge> </template> <ul> - <li v-if="GQLerror.gitlabCommit"> + <li v-if="error.gitlabCommit"> <strong class="bold">{{ __('GitLab commit') }}:</strong> - <gl-link :href="GQLerror.gitlabCommitPath"> - <span>{{ GQLerror.gitlabCommit.substr(0, 10) }}</span> + <gl-link :href="error.gitlabCommitPath"> + <span>{{ error.gitlabCommit.substr(0, 10) }}</span> </gl-link> </li> - <li v-if="error.gitlab_issue"> + <li v-if="error.gitlabIssuePath"> <strong class="bold">{{ __('GitLab Issue') }}:</strong> - <gl-link :href="error.gitlab_issue"> - <span>{{ error.gitlab_issue }}</span> + <gl-link :href="error.gitlabIssuePath"> + <span>{{ error.gitlabIssuePath }}</span> </gl-link> </li> <li> <strong class="bold">{{ __('Sentry event') }}:</strong> <gl-link - v-track-event="trackClickErrorLinkToSentryOptions(GQLerror.externalUrl)" + v-track-event="trackClickErrorLinkToSentryOptions(error.externalUrl)" class="d-inline-flex align-items-center" - :href="GQLerror.externalUrl" + :href="error.externalUrl" target="_blank" > - <span class="text-truncate">{{ GQLerror.externalUrl }}</span> + <span class="text-truncate">{{ error.externalUrl }}</span> <icon name="external-link" class="ml-1 flex-shrink-0" /> </gl-link> </li> - <li v-if="GQLerror.firstReleaseShortVersion"> + <li v-if="error.firstReleaseShortVersion"> <strong class="bold">{{ __('First seen') }}:</strong> - {{ formatDate(GQLerror.firstSeen) }} + {{ formatDate(error.firstSeen) }} <gl-link :href="firstReleaseLink" target="_blank"> - <span> - {{ __('Release') }}: {{ GQLerror.firstReleaseShortVersion.substr(0, 10) }} - </span> + <span>{{ __('Release') }}: {{ error.firstReleaseShortVersion.substr(0, 10) }}</span> </gl-link> </li> - <li v-if="GQLerror.lastReleaseShortVersion"> + <li v-if="error.lastReleaseShortVersion"> <strong class="bold">{{ __('Last seen') }}:</strong> - {{ formatDate(GQLerror.lastSeen) }} + {{ formatDate(error.lastSeen) }} <gl-link :href="lastReleaseLink" target="_blank"> - <span>{{ __('Release') }}: {{ GQLerror.lastReleaseShortVersion.substr(0, 10) }}</span> + <span>{{ __('Release') }}: {{ error.lastReleaseShortVersion.substr(0, 10) }}</span> </gl-link> </li> <li> <strong class="bold">{{ __('Events') }}:</strong> - <span>{{ GQLerror.count }}</span> + <span>{{ error.count }}</span> </li> <li> <strong class="bold">{{ __('Users') }}:</strong> - <span>{{ GQLerror.userCount }}</span> + <span>{{ error.userCount }}</span> </li> </ul> @@ -351,7 +336,7 @@ export default { <gl-loading-icon :size="3" /> </div> - <template v-if="showStacktrace"> + <template v-else-if="showStacktrace"> <h3 class="my-4">{{ __('Stack trace') }}</h3> <stacktrace :entries="stacktrace" /> </template> diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue index 6f3a5f683f8..1c996cbc13b 100644 --- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue +++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue @@ -13,6 +13,7 @@ import { GlDropdownDivider, GlTooltipDirective, GlPagination, + GlButtonGroup, } from '@gitlab/ui'; import AccessorUtils from '~/lib/utils/accessor'; import Icon from '~/vue_shared/components/icon.vue'; @@ -20,12 +21,16 @@ import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import { __ } from '~/locale'; import _ from 'underscore'; -export const tableDataClass = 'table-col d-flex d-sm-table-cell'; +export const tableDataClass = 'table-col d-flex d-sm-table-cell align-items-center'; export default { FIRST_PAGE: 1, PREV_PAGE: 1, NEXT_PAGE: 2, + statusButtons: [ + { status: 'ignored', icon: 'eye-slash', title: __('Ignore') }, + { status: 'resolved', icon: 'check-circle', title: __('Resolve') }, + ], fields: [ { key: 'error', @@ -48,20 +53,13 @@ export default { { key: 'lastSeen', label: __('Last seen'), - thClass: '', + thClass: 'w-15p', tdClass: `${tableDataClass}`, }, { - key: 'ignore', - label: '', - thClass: 'w-3rem', - tdClass: `${tableDataClass} pl-0`, - }, - { - key: 'resolved', + key: 'status', label: '', - thClass: 'w-3rem', - tdClass: `${tableDataClass} pl-0`, + tdClass: `${tableDataClass} text-right`, }, { key: 'details', @@ -88,6 +86,7 @@ export default { Icon, GlPagination, TimeAgo, + GlButtonGroup, }, directives: { GlTooltip: GlTooltipDirective, @@ -332,25 +331,19 @@ export default { <time-ago :time="errors.item.lastSeen" class="text-secondary" /> </div> </template> - <template #cell(ignore)="errors"> - <gl-button - ref="ignoreError" - v-gl-tooltip.hover - :title="__('Ignore')" - @click="updateIssueStatus(errors.item.id, 'ignored')" - > - <gl-icon name="eye-slash" :size="12" /> - </gl-button> - </template> - <template #cell(resolved)="errors"> - <gl-button - ref="resolveError" - v-gl-tooltip - :title="__('Resolve')" - @click="updateIssueStatus(errors.item.id, 'resolved')" - > - <gl-icon name="check-circle" :size="12" /> - </gl-button> + <template #cell(status)="errors"> + <gl-button-group> + <gl-button + v-for="button in $options.statusButtons" + :key="button.status" + :ref="button.title.toLowerCase() + 'Error'" + v-gl-tooltip.hover + :title="button.title" + @click="updateIssueStatus(errors.item.id, button.status)" + > + <gl-icon :name="button.icon" :size="12" /> + </gl-button> + </gl-button-group> </template> <template #cell(details)="errors"> <gl-button diff --git a/app/assets/javascripts/error_tracking/details.js b/app/assets/javascripts/error_tracking/details.js index a5a7ddc907b..1a92681374b 100644 --- a/app/assets/javascripts/error_tracking/details.js +++ b/app/assets/javascripts/error_tracking/details.js @@ -26,7 +26,6 @@ export default () => { issueId, projectPath, issueUpdatePath, - issueDetailsPath, issueStackTracePath, projectIssuesPath, } = domEl.dataset; @@ -36,7 +35,6 @@ export default () => { issueId, projectPath, issueUpdatePath, - issueDetailsPath, issueStackTracePath, projectIssuesPath, csrfToken: csrf.token, diff --git a/app/assets/javascripts/error_tracking/queries/details.query.graphql b/app/assets/javascripts/error_tracking/queries/details.query.graphql index 488a3ecc3ab..fa579c94257 100644 --- a/app/assets/javascripts/error_tracking/queries/details.query.graphql +++ b/app/assets/javascripts/error_tracking/queries/details.query.graphql @@ -1,21 +1,29 @@ query errorDetails($fullPath: ID!, $errorId: ID!) { project(fullPath: $fullPath) { - sentryDetailedError(id: $errorId) { - id - sentryId - title - userCount - count - status - firstSeen - lastSeen - message - culprit - externalUrl - firstReleaseShortVersion - lastReleaseShortVersion - gitlabCommit - gitlabCommitPath + sentryErrors { + detailedError(id: $errorId) { + id + sentryId + title + userCount + count + status + firstSeen + lastSeen + message + culprit + tags { + level + logger + } + externalUrl + externalBaseUrl + firstReleaseShortVersion + lastReleaseShortVersion + gitlabCommit + gitlabCommitPath + gitlabIssuePath + } } } } diff --git a/app/assets/javascripts/error_tracking/store/details/actions.js b/app/assets/javascripts/error_tracking/store/details/actions.js index 2b216d910ce..5914a79f092 100644 --- a/app/assets/javascripts/error_tracking/store/details/actions.js +++ b/app/assets/javascripts/error_tracking/store/details/actions.js @@ -5,36 +5,11 @@ import Poll from '~/lib/utils/poll'; import { __ } from '~/locale'; let stackTracePoll; -let detailPoll; const stopPolling = poll => { if (poll) poll.stop(); }; -export function startPollingDetails({ commit }, endpoint) { - detailPoll = new Poll({ - resource: service, - method: 'getSentryData', - data: { endpoint }, - successCallback: ({ data }) => { - if (!data) { - return; - } - - commit(types.SET_ERROR, data.error); - commit(types.SET_LOADING, false); - - stopPolling(detailPoll); - }, - errorCallback: () => { - commit(types.SET_LOADING, false); - createFlash(__('Failed to load error details from Sentry.')); - }, - }); - - detailPoll.makeRequest(); -} - export function startPollingStacktrace({ commit }, endpoint) { stackTracePoll = new Poll({ resource: service, diff --git a/app/assets/javascripts/error_tracking/store/details/mutation_types.js b/app/assets/javascripts/error_tracking/store/details/mutation_types.js index a2592253a2d..0dd49e727e6 100644 --- a/app/assets/javascripts/error_tracking/store/details/mutation_types.js +++ b/app/assets/javascripts/error_tracking/store/details/mutation_types.js @@ -1,4 +1,2 @@ -export const SET_ERROR = 'SET_ERRORS'; -export const SET_LOADING = 'SET_LOADING'; export const SET_LOADING_STACKTRACE = 'SET_LOADING_STACKTRACE'; export const SET_STACKTRACE_DATA = 'SET_STACKTRACE_DATA'; diff --git a/app/assets/javascripts/error_tracking/store/details/mutations.js b/app/assets/javascripts/error_tracking/store/details/mutations.js index 6f4720444e0..b2bde96c6a9 100644 --- a/app/assets/javascripts/error_tracking/store/details/mutations.js +++ b/app/assets/javascripts/error_tracking/store/details/mutations.js @@ -1,12 +1,6 @@ import * as types from './mutation_types'; export default { - [types.SET_ERROR](state, data) { - state.error = data; - }, - [types.SET_LOADING](state, loading) { - state.loading = loading; - }, [types.SET_LOADING_STACKTRACE](state, data) { state.loadingStacktrace = data; }, diff --git a/app/assets/javascripts/error_tracking/store/details/state.js b/app/assets/javascripts/error_tracking/store/details/state.js index f53cbe29c67..4a6bafe3114 100644 --- a/app/assets/javascripts/error_tracking/store/details/state.js +++ b/app/assets/javascripts/error_tracking/store/details/state.js @@ -1,7 +1,5 @@ export default () => ({ - error: {}, stacktraceData: {}, - loading: true, loadingStacktrace: true, updatingResolveStatus: false, updatingIgnoreStatus: false, diff --git a/app/assets/javascripts/ide/components/nav_form.vue b/app/assets/javascripts/ide/components/nav_form.vue index 23c068f329d..2ccc84ea5d5 100644 --- a/app/assets/javascripts/ide/components/nav_form.vue +++ b/app/assets/javascripts/ide/components/nav_form.vue @@ -19,15 +19,15 @@ export default { <tabs stop-propagation> <tab active> <template slot="title"> - {{ __('Merge Requests') }} + {{ __('Branches') }} </template> - <merge-request-search-list /> + <branches-search-list /> </tab> <tab> <template slot="title"> - {{ __('Branches') }} + {{ __('Merge Requests') }} </template> - <branches-search-list /> + <merge-request-search-list /> </tab> </tabs> </div> diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue index bcaaa8e09c2..b2fa020fb00 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/index.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue @@ -51,7 +51,7 @@ export default { </script> <template> - <div class="ide-new-btn d-none"> + <div class="ide-new-btn"> <div :class="{ show: isOpen, diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss index 5eaff7702f6..7fa48c70f41 100644 --- a/app/assets/stylesheets/page_bundles/ide.scss +++ b/app/assets/stylesheets/page_bundles/ide.scss @@ -1224,6 +1224,8 @@ $ide-commit-header-height: 48px; } .ide-new-btn { + display: none; + .btn { padding: 2px 5px; } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index d30f113c01f..24c6fec064a 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -14,9 +14,9 @@ cursor: pointer; @media (min-width: map-get($grid-breakpoints, md)) { - // The `-1` below is to prevent two borders from clashing up against eachother - + // The `+11` is to ensure the file header border shows when scrolled - // the bottom of the compare-versions header and the top of the file header - $mr-file-header-top: $mr-version-controls-height + $header-height + $mr-tabs-height - 1; + $mr-file-header-top: $mr-version-controls-height + $header-height + $mr-tabs-height + 11; position: -webkit-sticky; position: sticky; @@ -547,7 +547,7 @@ table.code { .diff-stats { align-items: center; - padding: 0 0.25rem; + padding: 0 1rem; .diff-stats-group { padding: 0 0.25rem; @@ -559,7 +559,7 @@ table.code { &.is-compare-versions-header { .diff-stats-group { - padding: 0 0.5rem; + padding: 0 0.25rem; } } } @@ -1054,8 +1054,8 @@ table.code { .diff-tree-list { position: -webkit-sticky; position: sticky; - $top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + 10px; - top: $header-height + $mr-tabs-height + $mr-version-controls-height + 10px; + $top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + 11px; + top: $top-pos; max-height: calc(100vh - #{$top-pos}); z-index: 202; @@ -1092,10 +1092,7 @@ table.code { .tree-list-scroll { max-height: 100%; - padding-top: $grid-size; padding-bottom: $grid-size; - border-top: 1px solid $border-color; - border-bottom: 1px solid $border-color; overflow-y: scroll; overflow-x: auto; } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index c023c9e5cbd..84daec4fb43 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -708,7 +708,7 @@ .mr-version-controls { position: relative; z-index: 203; - background: $gray-light; + background: $white-light; color: $gl-text-color; margin-top: -1px; @@ -732,7 +732,7 @@ } .content-block { - padding: $gl-padding-top $gl-padding; + padding: $gl-padding; border-bottom: 0; } diff --git a/app/controllers/concerns/confirm_email_warning.rb b/app/controllers/concerns/confirm_email_warning.rb index 32e1a46e580..f1c0bcd491d 100644 --- a/app/controllers/concerns/confirm_email_warning.rb +++ b/app/controllers/concerns/confirm_email_warning.rb @@ -10,7 +10,7 @@ module ConfirmEmailWarning protected def show_confirm_warning? - html_request? && request.get? && Feature.enabled?(:soft_email_confirmation) + html_request? && request.get? end def set_confirm_warning diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb index 21ee76d31b2..f99345fa99d 100644 --- a/app/controllers/confirmations_controller.rb +++ b/app/controllers/confirmations_controller.rb @@ -11,7 +11,7 @@ class ConfirmationsController < Devise::ConfirmationsController protected def after_resending_confirmation_instructions_path_for(resource) - Feature.enabled?(:soft_email_confirmation) ? stored_location_for(resource) || dashboard_projects_path : users_almost_there_path + stored_location_for(resource) || dashboard_projects_path end def after_confirmation_path_for(resource_name, resource) diff --git a/app/controllers/dashboard/snippets_controller.rb b/app/controllers/dashboard/snippets_controller.rb index 6feade3df03..aa09fcdbe61 100644 --- a/app/controllers/dashboard/snippets_controller.rb +++ b/app/controllers/dashboard/snippets_controller.rb @@ -7,6 +7,10 @@ class Dashboard::SnippetsController < Dashboard::ApplicationController skip_cross_project_access_check :index def index + @snippet_counts = Snippets::CountService + .new(current_user, author: current_user) + .execute + @snippets = SnippetsFinder.new(current_user, author: current_user, scope: params[:scope]) .execute .page(params[:page]) diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index d7ae6d2cbb4..b9c7468890b 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -30,6 +30,10 @@ class Projects::SnippetsController < Projects::ApplicationController respond_to :html def index + @snippet_counts = Snippets::CountService + .new(current_user, project: @project) + .execute + @snippets = SnippetsFinder.new(current_user, project: @project, scope: params[:scope]) .execute .page(params[:page]) diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index b40264bfdf4..29b0c6b29ae 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -53,7 +53,7 @@ class RegistrationsController < Devise::RegistrationsController def welcome return redirect_to new_user_registration_path unless current_user - return redirect_to stored_location_or_dashboard_or_almost_there_path(current_user) if current_user.role.present? && !current_user.setup_for_company.nil? + return redirect_to stored_location_or_dashboard(current_user) if current_user.role.present? && !current_user.setup_for_company.nil? current_user.name = nil if current_user.name == current_user.username end @@ -65,7 +65,7 @@ class RegistrationsController < Devise::RegistrationsController if result[:status] == :success track_experiment_event(:signup_flow, 'end') # We want this event to be tracked when the user is _in_ the experimental group set_flash_message! :notice, :signed_up - redirect_to stored_location_or_dashboard_or_almost_there_path(current_user) + redirect_to stored_location_or_dashboard(current_user) else render :welcome end @@ -112,12 +112,12 @@ class RegistrationsController < Devise::RegistrationsController return users_sign_up_welcome_path if experiment_enabled?(:signup_flow) - stored_location_or_dashboard_or_almost_there_path(user) + stored_location_or_dashboard(user) end def after_inactive_sign_up_path_for(resource) Gitlab::AppLogger.info(user_created_message) - Feature.enabled?(:soft_email_confirmation) ? dashboard_projects_path : users_almost_there_path + dashboard_projects_path end private @@ -179,18 +179,10 @@ class RegistrationsController < Devise::RegistrationsController Gitlab::Utils.to_boolean(params[:terms_opt_in]) end - def confirmed_or_unconfirmed_access_allowed(user) - user.confirmed? || Feature.enabled?(:soft_email_confirmation) || experiment_enabled?(:signup_flow) - end - def stored_location_or_dashboard(user) stored_location_for(user) || dashboard_projects_path end - def stored_location_or_dashboard_or_almost_there_path(user) - confirmed_or_unconfirmed_access_allowed(user) ? stored_location_or_dashboard(user) : users_almost_there_path - end - # Part of an experiment to build a new sign up flow. Will be resolved # with https://gitlab.com/gitlab-org/growth/engineering/issues/64 def choose_layout diff --git a/app/helpers/projects/error_tracking_helper.rb b/app/helpers/projects/error_tracking_helper.rb index 91fc18f4312..5be4f67bde8 100644 --- a/app/helpers/projects/error_tracking_helper.rb +++ b/app/helpers/projects/error_tracking_helper.rb @@ -22,7 +22,6 @@ module Projects::ErrorTrackingHelper { 'issue-id' => issue_id, 'project-path' => project.full_path, - 'issue-details-path' => details_project_error_tracking_index_path(*opts), 'issue-update-path' => update_project_error_tracking_index_path(*opts), 'project-issues-path' => project_issues_path(project), 'issue-stack-trace-path' => stack_trace_project_error_tracking_index_path(*opts) diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb index 2602bb2cf97..eebcbcba2d3 100644 --- a/app/models/clusters/applications/knative.rb +++ b/app/models/clusters/applications/knative.rb @@ -74,7 +74,7 @@ module Clusters end def ingress_service - cluster.kubeclient.get_service('istio-ingressgateway', 'istio-system') + cluster.kubeclient.get_service('istio-ingressgateway', Clusters::Kubernetes::ISTIO_SYSTEM_NAMESPACE) end def uninstall_command diff --git a/app/models/serverless/domain_cluster.rb b/app/models/serverless/domain_cluster.rb index 9a1acf7e5c3..94d90d3e305 100644 --- a/app/models/serverless/domain_cluster.rb +++ b/app/models/serverless/domain_cluster.rb @@ -10,6 +10,11 @@ module Serverless belongs_to :knative, class_name: 'Clusters::Applications::Knative', foreign_key: 'clusters_applications_knative_id' belongs_to :creator, class_name: 'User', optional: true + attr_encrypted :key, + mode: :per_attribute_iv, + key: Settings.attr_encrypted_db_key_base_truncated, + algorithm: 'aes-256-gcm' + validates :pages_domain, :knative, presence: true validates :uuid, presence: true, uniqueness: true, length: { is: Gitlab::Serverless::Domain::UUID_LENGTH }, format: { with: HEX_REGEXP, message: 'only allows hex characters' } diff --git a/app/models/user.rb b/app/models/user.rb index d44e8162abe..a5ef03215d3 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1638,13 +1638,6 @@ class User < ApplicationRecord super end - # override from Devise::Confirmable - def confirmation_period_valid? - return false if Feature.disabled?(:soft_email_confirmation) - - super - end - private def default_private_profile_to_false diff --git a/app/services/clusters/kubernetes.rb b/app/services/clusters/kubernetes.rb index d29519999b2..aafea64c820 100644 --- a/app/services/clusters/kubernetes.rb +++ b/app/services/clusters/kubernetes.rb @@ -12,5 +12,7 @@ module Clusters GITLAB_KNATIVE_SERVING_ROLE_BINDING_NAME = 'gitlab-knative-serving-rolebinding' GITLAB_CROSSPLANE_DATABASE_ROLE_NAME = 'gitlab-crossplane-database-role' GITLAB_CROSSPLANE_DATABASE_ROLE_BINDING_NAME = 'gitlab-crossplane-database-rolebinding' + KNATIVE_SERVING_NAMESPACE = 'knative-serving' + ISTIO_SYSTEM_NAMESPACE = 'istio-system' end end diff --git a/app/services/clusters/kubernetes/configure_istio_ingress_service.rb b/app/services/clusters/kubernetes/configure_istio_ingress_service.rb new file mode 100644 index 00000000000..fe577beaa8a --- /dev/null +++ b/app/services/clusters/kubernetes/configure_istio_ingress_service.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +require 'openssl' + +module Clusters + module Kubernetes + class ConfigureIstioIngressService + PASSTHROUGH_RESOURCE = Kubeclient::Resource.new( + mode: 'PASSTHROUGH' + ).freeze + + MTLS_RESOURCE = Kubeclient::Resource.new( + mode: 'MUTUAL', + privateKey: '/etc/istio/ingressgateway-certs/tls.key', + serverCertificate: '/etc/istio/ingressgateway-certs/tls.crt', + caCertificates: '/etc/istio/ingressgateway-ca-certs/cert.pem' + ).freeze + + def initialize(cluster:) + @cluster = cluster + @platform = cluster.platform + @kubeclient = platform.kubeclient + @knative = cluster.application_knative + end + + def execute + return configure_certificates if serverless_domain_cluster + + configure_passthrough + end + + private + + attr_reader :cluster, :platform, :kubeclient, :knative + + def serverless_domain_cluster + knative&.serverless_domain_cluster + end + + def configure_certificates + create_or_update_istio_cert_and_key + set_gateway_wildcard_https(MTLS_RESOURCE) + end + + def create_or_update_istio_cert_and_key + name = OpenSSL::X509::Name.parse("CN=#{knative.hostname}") + + key = OpenSSL::PKey::RSA.new(2048) + + cert = OpenSSL::X509::Certificate.new + cert.version = 2 + cert.serial = 0 + cert.not_before = Time.now + cert.not_after = Time.now + 1000.years + + cert.public_key = key.public_key + cert.subject = name + cert.issuer = name + cert.sign(key, OpenSSL::Digest::SHA256.new) + + serverless_domain_cluster.update!( + key: key.to_pem, + certificate: cert.to_pem + ) + + kubeclient.create_or_update_secret(istio_ca_certs_resource) + kubeclient.create_or_update_secret(istio_certs_resource) + end + + def istio_ca_certs_resource + Gitlab::Kubernetes::GenericSecret.new( + 'istio-ingressgateway-ca-certs', + { + 'cert.pem': Base64.strict_encode64(serverless_domain_cluster.certificate) + }, + Clusters::Kubernetes::ISTIO_SYSTEM_NAMESPACE + ).generate + end + + def istio_certs_resource + Gitlab::Kubernetes::TlsSecret.new( + 'istio-ingressgateway-certs', + serverless_domain_cluster.certificate, + serverless_domain_cluster.key, + Clusters::Kubernetes::ISTIO_SYSTEM_NAMESPACE + ).generate + end + + def set_gateway_wildcard_https(tls_resource) + gateway_resource = gateway + gateway_resource.spec.servers.each do |server| + next unless server.hosts == ['*'] && server.port.name == 'https' + + server.tls = tls_resource + end + kubeclient.update_gateway(gateway_resource) + end + + def configure_passthrough + set_gateway_wildcard_https(PASSTHROUGH_RESOURCE) + end + + def gateway + kubeclient.get_gateway('knative-ingress-gateway', Clusters::Kubernetes::KNATIVE_SERVING_NAMESPACE) + end + end + end +end diff --git a/app/services/snippets/count_service.rb b/app/services/snippets/count_service.rb new file mode 100644 index 00000000000..9a3d33c75cf --- /dev/null +++ b/app/services/snippets/count_service.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +# Service for calculating visible Snippet counts via one query +# for the given user or project. +# +# Authorisation level checks will be included, ensuring the correct +# counts will be returned for the given user (if any). +# +# Basic usage: +# +# user = User.find(1) +# +# Snippets::CountService.new(user, author: user).execute +# #=> { +# are_public: 1, +# are_internal: 1, +# are_private: 1, +# all: 3 +# } +# +# Counts can be scoped to a project: +# +# user = User.find(1) +# project = Project.find(1) +# +# Snippets::CountService.new(user, project: project).execute +# #=> { +# are_public: 1, +# are_internal: 1, +# are_private: 0, +# all: 2 +# } +# +# Either a project or an author *must* be supplied. +module Snippets + class CountService + def initialize(current_user, author: nil, project: nil) + if !author && !project + raise( + ArgumentError, 'Must provide either an author or a project' + ) + end + + @snippets_finder = SnippetsFinder.new(current_user, author: author, project: project) + end + + def execute + counts = snippet_counts + return {} unless counts + + counts.slice( + :are_public, + :are_private, + :are_internal, + :are_public_or_internal, + :total + ) + end + + private + + # rubocop: disable CodeReuse/ActiveRecord + def snippet_counts + @snippets_finder.execute + .reorder(nil) + .select(" + count(case when snippets.visibility_level=#{Snippet::PUBLIC} and snippets.secret is FALSE then 1 else null end) as are_public, + count(case when snippets.visibility_level=#{Snippet::INTERNAL} then 1 else null end) as are_internal, + count(case when snippets.visibility_level=#{Snippet::PRIVATE} then 1 else null end) as are_private, + count(case when visibility_level=#{Snippet::PUBLIC} OR visibility_level=#{Snippet::INTERNAL} then 1 else null end) as are_public_or_internal, + count(*) as total + ") + .first + end + # rubocop: enable CodeReuse/ActiveRecord + end +end diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml index 69155b6c04d..05214346496 100644 --- a/app/views/dashboard/snippets/index.html.haml +++ b/app/views/dashboard/snippets/index.html.haml @@ -5,7 +5,7 @@ = render 'dashboard/snippets_head' - if current_user.snippets.exists? - = render partial: 'snippets/snippets_scope_menu', locals: { include_private: true } + = render partial: 'snippets/snippets_scope_menu', locals: { include_private: true, counts: @snippet_counts } - if current_user.snippets.exists? = render partial: 'shared/snippets/list', locals: { link_project: true } diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml index ed56cc8289c..a505b34f46c 100644 --- a/app/views/projects/snippets/index.html.haml +++ b/app/views/projects/snippets/index.html.haml @@ -5,7 +5,7 @@ - if current_user .top-area - include_private = @project.team.member?(current_user) || current_user.admin? - = render partial: 'snippets/snippets_scope_menu', locals: { subject: @project, include_private: include_private } + = render partial: 'snippets/snippets_scope_menu', locals: { subject: @project, include_private: include_private, counts: @snippet_counts } - if new_project_snippet_link.present? .nav-controls diff --git a/app/views/snippets/_snippets_scope_menu.html.haml b/app/views/snippets/_snippets_scope_menu.html.haml index cb59b11ca2b..e9c9ca6e856 100644 --- a/app/views/snippets/_snippets_scope_menu.html.haml +++ b/app/views/snippets/_snippets_scope_menu.html.haml @@ -7,25 +7,25 @@ = _("All") %span.badge.badge-pill - if include_private - = subject.snippets.count + = counts[:total] - else - = subject.snippets.public_and_internal_only.count + = counts[:are_public_or_internal] - if include_private %li{ class: active_when(params[:scope] == "are_private") } = link_to subject_snippets_path(subject, scope: 'are_private') do = _("Private") %span.badge.badge-pill - = subject.snippets.are_private.count + = counts[:are_private] %li{ class: active_when(params[:scope] == "are_internal") } = link_to subject_snippets_path(subject, scope: 'are_internal') do = _("Internal") %span.badge.badge-pill - = subject.snippets.are_internal.count + = counts[:are_internal] %li{ class: active_when(params[:scope] == "are_public") } = link_to subject_snippets_path(subject, scope: 'are_public') do = _("Public") %span.badge.badge-pill - = subject.snippets.are_public.count + = counts[:are_public] diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index c93c312438b..0426a0b8fbb 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -231,6 +231,12 @@ :latency_sensitive: :resource_boundary: :unknown :weight: 1 +- :name: gcp_cluster:cluster_configure_istio + :feature_category: :kubernetes_management + :has_external_dependencies: true + :latency_sensitive: + :resource_boundary: :unknown + :weight: 1 - :name: gcp_cluster:cluster_install_app :feature_category: :kubernetes_management :has_external_dependencies: true diff --git a/app/workers/cluster_configure_istio_worker.rb b/app/workers/cluster_configure_istio_worker.rb new file mode 100644 index 00000000000..dfdd408f286 --- /dev/null +++ b/app/workers/cluster_configure_istio_worker.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class ClusterConfigureIstioWorker + include ApplicationWorker + include ClusterQueue + + worker_has_external_dependencies! + + def perform(cluster_id) + Clusters::Cluster.find_by_id(cluster_id).try do |cluster| + Clusters::Kubernetes::ConfigureIstioIngressService.new(cluster: cluster).execute + end + end +end diff --git a/changelogs/unreleased/118442-restyle-changes-header-and-file-tree.yml b/changelogs/unreleased/118442-restyle-changes-header-and-file-tree.yml new file mode 100644 index 00000000000..8b82d03a503 --- /dev/null +++ b/changelogs/unreleased/118442-restyle-changes-header-and-file-tree.yml @@ -0,0 +1,5 @@ +--- +title: Restyle changes header & file tree +merge_request: 22364 +author: +type: changed diff --git a/changelogs/unreleased/201893-resolve-soft-email-confirmation-flow.yml b/changelogs/unreleased/201893-resolve-soft-email-confirmation-flow.yml new file mode 100644 index 00000000000..3224448b00e --- /dev/null +++ b/changelogs/unreleased/201893-resolve-soft-email-confirmation-flow.yml @@ -0,0 +1,5 @@ +--- +title: Allow a grace period for new users to confirm their email +merge_request: 24371 +author: +type: added diff --git a/changelogs/unreleased/21705-number-of-snippets-in-the-snippets-page-is-wrong.yml b/changelogs/unreleased/21705-number-of-snippets-in-the-snippets-page-is-wrong.yml new file mode 100644 index 00000000000..974d9bde815 --- /dev/null +++ b/changelogs/unreleased/21705-number-of-snippets-in-the-snippets-page-is-wrong.yml @@ -0,0 +1,5 @@ +--- +title: Fix bug with snippet counts not being scoped to current authorisation +merge_request: 21705 +author: +type: fixed diff --git a/changelogs/unreleased/fe-ide-nav-switch-branches-and-mr-tab-order.yml b/changelogs/unreleased/fe-ide-nav-switch-branches-and-mr-tab-order.yml new file mode 100644 index 00000000000..7f7db73b720 --- /dev/null +++ b/changelogs/unreleased/fe-ide-nav-switch-branches-and-mr-tab-order.yml @@ -0,0 +1,5 @@ +--- +title: Switch order of tabs in Web IDE nav dropdown +merge_request: 24199 +author: +type: changed diff --git a/changelogs/unreleased/update-graphicsmagick-to-1-3-34.yml b/changelogs/unreleased/update-graphicsmagick-to-1-3-34.yml new file mode 100644 index 00000000000..03973c72c1e --- /dev/null +++ b/changelogs/unreleased/update-graphicsmagick-to-1-3-34.yml @@ -0,0 +1,5 @@ +--- +title: Update GraphicsMagick from 1.3.33 to 1.3.34 +merge_request: 24225 +author: Takuya Noguchi +type: security diff --git a/db/migrate/20200130134335_add_cert_and_key_to_serverless_domain_cluster.rb b/db/migrate/20200130134335_add_cert_and_key_to_serverless_domain_cluster.rb new file mode 100644 index 00000000000..39249053ee0 --- /dev/null +++ b/db/migrate/20200130134335_add_cert_and_key_to_serverless_domain_cluster.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class AddCertAndKeyToServerlessDomainCluster < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :serverless_domain_cluster, :encrypted_key, :text + add_column :serverless_domain_cluster, :encrypted_key_iv, :string, limit: 255 + add_column :serverless_domain_cluster, :certificate, :text + end +end diff --git a/db/schema.rb b/db/schema.rb index a61a5533ccf..9617c595a65 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -3786,6 +3786,9 @@ ActiveRecord::Schema.define(version: 2020_02_04_131054) do t.bigint "creator_id" t.datetime_with_timezone "created_at", null: false t.datetime_with_timezone "updated_at", null: false + t.text "encrypted_key" + t.string "encrypted_key_iv", limit: 255 + t.text "certificate" t.index ["clusters_applications_knative_id"], name: "idx_serverless_domain_cluster_on_clusters_applications_knative", unique: true t.index ["creator_id"], name: "index_serverless_domain_cluster_on_creator_id" t.index ["pages_domain_id"], name: "index_serverless_domain_cluster_on_pages_domain_id" diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md index 6f6c2e0068e..339710624fa 100644 --- a/doc/administration/auth/ldap.md +++ b/doc/administration/auth/ldap.md @@ -609,3 +609,16 @@ Could not authenticate you from Ldapmain because "Connection timed out - user sp If your configured LDAP provider and/or endpoint is offline or otherwise unreachable by GitLab, no LDAP user will be able to authenticate and log in. GitLab does not cache or store credentials for LDAP users to provide authentication during an LDAP outage. Contact your LDAP provider or administrator if you are seeing this error. + +### No file specified as Settingslogic source + +If `sudo gitlab-ctl reconfigure` fails with the following error, or you are seeing it in +the logs, you may have malformed YAML in `/etc/gitlab/gitlab.rb`: + +```plaintext +Errno::ENOENT: No such file or directory - No file specified as Settingslogic source +``` + +This issue is frequently due to the spacing in your YAML file. To fix the problem, +verify the syntax with **spacing** against the +[documentation for the configuration of LDAP](#configuration). diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md index 5ed0965115e..6165bfe24f7 100644 --- a/doc/administration/pages/index.md +++ b/doc/administration/pages/index.md @@ -83,7 +83,7 @@ GitLab Pages expect to run on their own virtual host. In your DNS server/provide you need to add a [wildcard DNS A record][wiki-wildcard-dns] pointing to the host that GitLab runs. For example, an entry would look like this: -``` +```plaintext *.example.io. 1800 IN A 192.0.2.1 *.example.io. 1800 IN AAAA 2001::1 ``` diff --git a/doc/administration/pages/source.md b/doc/administration/pages/source.md index 1f38a71efe1..04c8a478f50 100644 --- a/doc/administration/pages/source.md +++ b/doc/administration/pages/source.md @@ -64,7 +64,7 @@ GitLab Pages expect to run on their own virtual host. In your DNS server/provide you need to add a [wildcard DNS A record][wiki-wildcard-dns] pointing to the host that GitLab runs. For example, an entry would look like this: -``` +```plaintext *.example.io. 1800 IN A 192.0.2.1 ``` @@ -131,7 +131,7 @@ The Pages daemon doesn't listen to the outside world. order to enable the pages daemon. In `gitlab_pages_options` the `-pages-domain` must match the `host` setting that you set above. - ``` + ```ini gitlab_pages_enabled=true gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090" ``` @@ -170,7 +170,7 @@ outside world. 1. In `gitlab.yml`, set the port to `443` and https to `true`: - ```shell + ```yaml ## GitLab Pages pages: enabled: true @@ -188,9 +188,9 @@ outside world. The `-root-cert` and `-root-key` settings are the wildcard TLS certificates of the `example.io` domain: - ``` + ```ini gitlab_pages_enabled=true - gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -root-cert /path/to/example.io.crt -root-key /path/to/example.io.key + gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -root-cert /path/to/example.io.crt -root-key /path/to/example.io.key" ``` 1. Copy the `gitlab-pages-ssl` NGINX configuration file: @@ -256,7 +256,7 @@ world. Custom domains are supported, but no TLS. `-pages-domain` and `-listen-http` must match the `host` and `external_http` settings that you set above respectively: - ``` + ```ini gitlab_pages_enabled=true gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 192.0.2.2:80" ``` @@ -325,9 +325,9 @@ world. Custom domains and TLS are supported. The `-root-cert` and `-root-key` settings are the wildcard TLS certificates of the `example.io` domain: - ``` + ```ini gitlab_pages_enabled=true - gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 192.0.2.2:80 -listen-https 192.0.2.2:443 -root-cert /path/to/example.io.crt -root-key /path/to/example.io.key + gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 192.0.2.2:80 -listen-https 192.0.2.2:443 -root-cert /path/to/example.io.crt -root-key /path/to/example.io.key" ``` 1. Copy the `gitlab-pages-ssl` NGINX configuration file: diff --git a/doc/administration/troubleshooting/kubernetes_cheat_sheet.md b/doc/administration/troubleshooting/kubernetes_cheat_sheet.md index 4a7d677723a..48d415c6bdf 100644 --- a/doc/administration/troubleshooting/kubernetes_cheat_sheet.md +++ b/doc/administration/troubleshooting/kubernetes_cheat_sheet.md @@ -199,6 +199,13 @@ and they will assist you with any issues you are having. helm upgrade <release name> <chart path> -f gitlab.yaml ``` +- How to get the manifest for a release. It can be useful because it contains the info about +all Kubernetes resources and dependent charts: + + ```shell + helm get manifest <release name> + ``` + ## Installation of minimal GitLab config via Minukube on macOS This section is based on [Developing for Kubernetes with Minikube](https://docs.gitlab.com/charts/development/minikube/index.html) diff --git a/doc/ci/ci_cd_for_external_repos/github_integration.md b/doc/ci/ci_cd_for_external_repos/github_integration.md index 7d2b17afe31..399205d66cf 100644 --- a/doc/ci/ci_cd_for_external_repos/github_integration.md +++ b/doc/ci/ci_cd_for_external_repos/github_integration.md @@ -85,9 +85,9 @@ To manually enable GitLab CI/CD for your repository: The web hook URL should be set to the GitLab API to [trigger pull mirroring](../../api/projects.md#start-the-pull-mirroring-process-for-a-project-starter), - using the GitLab personal access token we just created. + using the GitLab personal access token we just created: - ``` + ```plaintext https://gitlab.com/api/v4/projects/<NAMESPACE>%2F<PROJECT>/mirror/pull?private_token=<PERSONAL_ACCESS_TOKEN> ``` diff --git a/doc/ci/directed_acyclic_graph/index.md b/doc/ci/directed_acyclic_graph/index.md index 7215dec287e..b6b53737dde 100644 --- a/doc/ci/directed_acyclic_graph/index.md +++ b/doc/ci/directed_acyclic_graph/index.md @@ -23,7 +23,7 @@ requiring a single keyword to enable the feature for any job. Consider a monorepo as follows: -``` +```plaintext ./service_a ./service_b ./service_c diff --git a/doc/ci/examples/artifactory_and_gitlab/index.md b/doc/ci/examples/artifactory_and_gitlab/index.md index e5f307e570d..aceceb7881a 100644 --- a/doc/ci/examples/artifactory_and_gitlab/index.md +++ b/doc/ci/examples/artifactory_and_gitlab/index.md @@ -39,7 +39,7 @@ project: 1. Create a new project by selecting **Import project from ➔ Repo by URL** 1. Add the following URL: - ``` + ```plaintext https://gitlab.com/gitlab-examples/maven/simple-maven-dep.git ``` @@ -164,7 +164,7 @@ The deployment occurs only if we're pushing or merging to `master` branch, so th Done! Now you have all the changes in the GitLab repo, and a pipeline has already been started for this commit. In the **Pipelines** tab you can see what's happening. If the deployment has been successful, the deploy job log will output: -``` +```plaintext [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ @@ -188,7 +188,7 @@ We'll use again a Maven app that can be cloned from our example project: 1. Create a new project by selecting **Import project from ➔ Repo by URL** 1. Add the following URL: - ``` + ```plaintext https://gitlab.com/gitlab-examples/maven/simple-maven-app.git ``` diff --git a/doc/ci/examples/deployment/README.md b/doc/ci/examples/deployment/README.md index dca11c40524..01dca801fbc 100644 --- a/doc/ci/examples/deployment/README.md +++ b/doc/ci/examples/deployment/README.md @@ -18,7 +18,7 @@ To use Dpl you need at least Ruby 1.9.3 with ability to install gems. Dpl can be installed on any machine with: -``` +```shell gem install dpl ``` @@ -27,7 +27,7 @@ having to test it on a CI server. If you don't have Ruby installed you can do it on Debian-compatible Linux with: -``` +```shell apt-get update apt-get install ruby-dev ``` diff --git a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md index b8f191570bc..bf7aec7107f 100644 --- a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md +++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md @@ -163,7 +163,7 @@ sudo nano /etc/nginx/sites-available/default The configuration should be like this. -``` +```nginx server { root /var/www/app/current/public; server_name example.com; diff --git a/doc/ci/examples/php.md b/doc/ci/examples/php.md index db039b746e3..04a6a4ddef2 100644 --- a/doc/ci/examples/php.md +++ b/doc/ci/examples/php.md @@ -179,7 +179,7 @@ user following [the upstream installation guide][phpenv-installation]. Using phpenv also allows to easily configure the PHP environment with: -``` +```shell phpenv config-add my_config.ini ``` diff --git a/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md b/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md index 557ebad72ba..4ffb29af512 100644 --- a/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md +++ b/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md @@ -115,7 +115,7 @@ mix ecto.create We expect to see this output at the end of the command: -```shell +```plaintext Generated hello_gitlab_ci app The database for HelloGitlabCi.Repo has been created ``` @@ -136,7 +136,7 @@ mix phoenix.server This will be the output to this command: -```shell +```plaintext [info] Running HelloGitlabCi.Endpoint with Cowboy using http://localhost:4000 23 May 11:44:35 - info: compiling 23 May 11:44:37 - info: compiled 6 files into 2 files, copied 3 in 9.8 sec @@ -229,7 +229,7 @@ mix test Our expected result is this: -```shell +```plaintext .... Finished in 0.7 seconds @@ -265,20 +265,20 @@ project. As we are focusing on testing (not deploying), you can use the [elixir:latest](https://hub.docker.com/_/elixir) docker image, which already has the dependencies for running Phoenix tests installed, such as Elixir and Erlang: - ```yml + ```yaml image: elixir:latest ``` - We'll only use `postgres`, so we can delete the `mysql` and `redis` lines from the `services` section: - ```yml + ```yaml services: - postgres:latest ``` - Now, we'll create a new section called `variables`, before the `before_script` section: - ```yml + ```yaml variables: POSTGRES_DB: hello_gitlab_ci_test POSTGRES_HOST: postgres @@ -293,7 +293,7 @@ project. - In the `before_script` section, we'll add some commands to prepare everything for the test: - ```yml + ```yaml before_script: - mix local.rebar --force - mix local.hex --force @@ -310,7 +310,7 @@ project. Let's take a look at the updated file after the changes: -```yml +```yaml image: elixir:latest services: @@ -353,7 +353,7 @@ actual running build job. Click on build's ID to watch the entire process. If everything went as expected, we can wait for the **Build succeeded** at the end of the process! :) -``` +```shell $ mix test .... diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md index 62a8bb23e08..804970e08f2 100644 --- a/doc/ci/ssh_keys/README.md +++ b/doc/ci/ssh_keys/README.md @@ -114,11 +114,11 @@ SSH key. You can generate the SSH key from the machine that GitLab Runner is installed on, and use that key for all projects that are run on this machine. -1. First, you need to login to the server that runs your jobs. +1. First, log in to the server that runs your jobs. -1. Then from the terminal login as the `gitlab-runner` user: +1. Then, from the terminal, log in as the `gitlab-runner` user: - ``` + ```shell sudo su - gitlab-runner ``` @@ -132,7 +132,7 @@ on, and use that key for all projects that are run on this machine. If you are accessing a private GitLab repository you need to add it as a [deploy key](../../ssh/README.md#deploy-keys). -Once done, try to login to the remote server in order to accept the fingerprint: +Once done, try to log in to the remote server in order to accept the fingerprint: ```shell ssh example.com diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md index 08b2171f23b..65776156ef8 100644 --- a/doc/ci/triggers/README.md +++ b/doc/ci/triggers/README.md @@ -110,7 +110,7 @@ The action is irreversible. To trigger a job you need to send a `POST` request to GitLab's API endpoint: -``` +```plaintext POST /projects/:id/trigger/pipeline ``` @@ -185,7 +185,7 @@ Now, whenever a new tag is pushed on project A, the job will run and the To trigger a job from a webhook of another project you need to add the following webhook URL for Push and Tag events (change the project ID, ref and token): -``` +```plaintext https://gitlab.example.com/api/v4/projects/9/ref/master/trigger/pipeline?token=TOKEN ``` @@ -195,7 +195,7 @@ You can pass any number of arbitrary variables in the trigger API call and they will be available in GitLab CI so that they can be used in your `.gitlab-ci.yml` file. The parameter is of the form: -``` +```plaintext variables[key]=value ``` diff --git a/doc/development/pipelines.md b/doc/development/pipelines.md index 7da74ae8b35..af278dafb76 100644 --- a/doc/development/pipelines.md +++ b/doc/development/pipelines.md @@ -42,7 +42,7 @@ The current stages are: ## Default image The default image is currently -`registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.12-git-2.24-lfs-2.9-chrome-73.0-node-12.x-yarn-1.16-postgresql-9.6-graphicsmagick-1.3.33`. +`registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.12-git-2.24-lfs-2.9-chrome-73.0-node-12.x-yarn-1.16-postgresql-9.6-graphicsmagick-1.3.34`. It includes Ruby 2.6.5, Go 1.12, Git 2.24, Git LFS 2.9, Chrome 73, Node 12, Yarn 1.16, PostgreSQL 9.6, and Graphics Magick 1.3.33. diff --git a/doc/migrate_ci_to_ce/README.md b/doc/migrate_ci_to_ce/README.md index 9cb14272544..d3db485820f 100644 --- a/doc/migrate_ci_to_ce/README.md +++ b/doc/migrate_ci_to_ce/README.md @@ -51,7 +51,7 @@ Otherwise it's pretty likely that you will encounter problems described in the [ Make sure that the backup script on both servers can connect to the database. -``` +```shell # On your CI server: # Omnibus sudo chown gitlab-ci:gitlab-ci /var/opt/gitlab/gitlab-ci/builds @@ -64,7 +64,7 @@ sudo -u gitlab_ci -H bundle exec rake backup:create RAILS_ENV=production Also check on your GitLab server. -``` +```shell # On your GitLab server: # Omnibus sudo gitlab-backup create SKIP=repositories,uploads @@ -89,7 +89,7 @@ MySQL and your GitLab server uses PostgreSQL you need to pass a special option during the 'Moving data' part. **If your CI server uses PostgreSQL and your GitLab server uses MySQL you cannot migrate your CI data to GitLab 8.0.** -``` +```shell # On your CI server: # Omnibus sudo gitlab-ci-rake env:info @@ -99,7 +99,7 @@ cd /home/gitlab_ci/gitlab-ci sudo -u gitlab_ci -H bundle exec rake env:info RAILS_ENV=production ``` -``` +```shell # On your GitLab server: # Omnibus sudo gitlab-rake gitlab:env:info @@ -149,7 +149,7 @@ Now upgrade GitLab CI to version 8.0. If you are using Omnibus packages, Disable GitLab CI after upgrading to 8.0. -``` +```shell # On your CI server: # Omnibus sudo gitlab-ctl stop ci-unicorn @@ -171,7 +171,7 @@ GitLab server. On Omnibus GitLab servers you will have to add a line to `/etc/gitlab/gitlab.rb`. On GitLab servers installed from source you will have to replace the contents of `/home/git/gitlab/config/secrets.yml`. -``` +```shell # On your CI server: # Omnibus sudo gitlab-ci-rake backup:show_secrets @@ -188,7 +188,7 @@ PostgreSQL, add `MYSQL_TO_POSTGRESQL=1` to the end of the rake command. When the command finishes it will print the path to your data export archive; you will need this file later. -``` +```shell # On your CI server: # Omnibus sudo chown gitlab-ci:gitlab-ci /var/opt/gitlab/gitlab-ci/builds @@ -209,7 +209,7 @@ this, below we use SSH agent forwarding and 'scp', which will be easy and fast for most setups. You can also copy the data archive first from the CI server to your laptop and then from your laptop to the GitLab server. -``` +```shell # Start from your laptop ssh -A ci_admin@ci_server.example # Now on the CI server @@ -221,7 +221,7 @@ scp /path/to/12345_gitlab_ci_backup.tar gitlab_admin@gitlab_server.example:~ Make the CI data archive discoverable for GitLab. We assume below that you store backups in the default path, adjust the command if necessary. -``` +```shell # On your GitLab server: # Omnibus sudo mv /path/to/12345_gitlab_ci_backup.tar /var/opt/gitlab/backups/ @@ -235,7 +235,7 @@ sudo mv /path/to/12345_gitlab_ci_backup.tar /home/git/gitlab/tmp/backups/ This step will delete any existing CI data on your GitLab server. There should be no CI data yet because you turned CI on the GitLab server off earlier. -``` +```shell # On your GitLab server: # Omnibus sudo chown git:git /var/opt/gitlab/gitlab-ci/builds @@ -248,7 +248,7 @@ sudo -u git -H bundle exec rake ci:migrate RAILS_ENV=production ### 6. Restart GitLab -``` +```shell # On your GitLab server: # Omnibus sudo gitlab-ctl hup unicorn @@ -347,7 +347,7 @@ restoration](../raketasks/backup_restore.md) guide. If you see errors like this: -``` +```plaintext Missing `secret_key_base` or `db_key_base` for 'production' environment. The secrets will be generated and stored in `config/secrets.yml` rake aborted! Errno::EACCES: Permission denied @ rb_sysopen - config/secrets.yml @@ -360,13 +360,13 @@ The fix for this is to update to Omnibus 7.14 first and then update it to 8.0. To fix that issue you have to change builds/ folder permission before doing final backup: -``` +```shell sudo chown -R gitlab-ci:gitlab-ci /var/opt/gitlab/gitlab-ci/builds ``` Then before executing `ci:migrate` you need to fix builds folder permission: -``` +```shell sudo chown git:git /var/opt/gitlab/gitlab-ci/builds ``` @@ -450,7 +450,7 @@ EOF Source installations: -``` +```shell cd /home/gitlab_ci/gitlab-ci sudo -u gitlab_ci -H bundle exec rails dbconsole production <<EOF ... COPY SQL STATEMENTS FROM ABOVE ... diff --git a/doc/user/application_security/dast/index.md b/doc/user/application_security/dast/index.md index 5d7bba32ead..7e318a47f4d 100644 --- a/doc/user/application_security/dast/index.md +++ b/doc/user/application_security/dast/index.md @@ -291,15 +291,9 @@ As the DAST job belongs to a separate `dast` stage that runs after all [default stages](../../../ci/yaml/README.md#stages), don't forget to add `stage: dast` when you override the template job definition. -## Available variables +### Available variables DAST can be [configured](#customizing-the-dast-settings) using environment variables. -Since it's a wrapper around the ZAP scanning scripts -([baseline](https://github.com/zaproxy/zaproxy/wiki/ZAP-Baseline-Scan) -or [full](https://github.com/zaproxy/zaproxy/wiki/ZAP-Full-Scan) scan), it -accepts all arguments those scripts recognize (the arguments are the same). -The choice of the scan type depends on the `DAST_FULL_SCAN_ENABLED` environment -variable value. | Environment variable | Required | Description | |-----------------------------| ----------|--------------------------------------------------------------------------------| @@ -314,14 +308,83 @@ variable value. | `DAST_FULL_SCAN_ENABLED` | no | Switches the tool to execute [ZAP Full Scan](https://github.com/zaproxy/zaproxy/wiki/ZAP-Full-Scan) instead of [ZAP Baseline Scan](https://github.com/zaproxy/zaproxy/wiki/ZAP-Baseline-Scan). Boolean. `true`, `True`, or `1` are considered as true value, otherwise false. Defaults to `false`. | | `DAST_FULL_SCAN_DOMAIN_VALIDATION_REQUIRED` | no | Requires [domain validation](#domain-validation) when running DAST full scans. Boolean. `true`, `True`, or `1` are considered as true value, otherwise false. Defaults to `false`. | -## Reports JSON format +### DAST command-line options + +Not all DAST configuration is available via environment variables. To find out all possible options, run the following configuration. +Available command-line options will be printed to the job log: + +```yaml +include: + template: DAST.gitlab-ci.yml + +dast: + script: + - /analyze --help +``` + +You must then overwrite the `script` command to pass in the appropriate argument. For example, AJAX spidering can be enabled by using `-j`, as shown in the following configuration: + +```yaml +include: + template: DAST.gitlab-ci.yml + +dast: + script: + - export DAST_WEBSITE=${DAST_WEBSITE:-$(cat environment_url.txt)} + - /analyze -j -t $DAST_WEBSITE +``` + +### Custom ZAProxy configuration + +The ZAProxy server contains many [useful configurable values](https://gitlab.com/gitlab-org/gitlab/issues/36437#note_245801885). +Many key/values for `-config` remain undocumented, but there is an untested list of [possible keys](https://gitlab.com/gitlab-org/gitlab/issues/36437#note_244981023). +Note that these options are not supported by DAST, and may break the DAST scan when used. An example of how to rewrite the Authorization header value with `TOKEN` follows: + +```yaml +include: + template: DAST.gitlab-ci.yml + +dast: + script: + - export DAST_WEBSITE=${DAST_WEBSITE:-$(cat environment_url.txt)} + - /analyze -z"-config replacer.full_list\(0\).description=auth -config replacer.full_list\(0\).enabled=true -config replacer.full_list\(0\).matchtype=REQ_HEADER -config replacer.full_list\(0\).matchstr=Authorization -config replacer.full_list\(0\).regex=false -config replacer.full_list\(0\).replacement=TOKEN" -t $DAST_WEBSITE +``` + +## Reports + +The DAST job can emit various reports. + +### JSON CAUTION: **Caution:** -The JSON report artifacts are not a public API of DAST and their format may change in the future. +The JSON report artifacts are not a public API of DAST and their format is expected to change in the future. + +The DAST tool always emits a JSON report report file called `gl-dast-report.json` and sample reports can be found in the [DAST repository](https://gitlab.com/gitlab-org/security-products/dast/tree/master/test/end-to-end/expect). + +There are two formats of data in the JSON report that are used side by side: the proprietary ZAP format which will be eventually deprecated, and a "common" format which will be the default in the future. -The DAST tool emits a JSON report report file. Sample report files can be found in the [DAST repository](https://gitlab.com/gitlab-org/security-products/dast/tree/master/test/end-to-end/expect). +### Other formats -There are two formats of data in the JSON document that are used side by side: the proprietary ZAP format which will be eventually deprecated, and a "common" format which will be the default in the future. +Reports can also be generated in Markdown, HTML, and XML. + +Reports can be published as artifacts using the following configuration: + +```yaml +include: + template: DAST.gitlab-ci.yml + +dast: + script: + - export DAST_WEBSITE=${DAST_WEBSITE:-$(cat environment_url.txt)} + - /analyze -r report.html -w report.md -x report.xml -t $DAST_WEBSITE + - cp /zap/wrk/report.{html,md,xml} "$PWD" + artifacts: + paths: + - report.html + - report.md + - report.xml + - gl-dast-report.json +``` ## Security Dashboard @@ -329,6 +392,20 @@ The Security Dashboard is a good place to get an overview of all the security vulnerabilities in your groups, projects and pipelines. Read more about the [Security Dashboard](../security_dashboard/index.md). +## Bleeding-edge vulnerability definitions + +ZAProxy first creates rules in the `alpha` class. After a testing period with the community, they are promoted to `beta`. DAST uses `beta` definitions by default. To request `alpha` definitions, use `-a` as shown in the following configuration: + +```yaml +include: + template: DAST.gitlab-ci.yml + +dast: + script: + - export DAST_WEBSITE=${DAST_WEBSITE:-$(cat environment_url.txt)} + - /analyze -a -t $DAST_WEBSITE +``` + ## Interacting with the vulnerabilities Once a vulnerability is found, you can interact with it. Read more on how to diff --git a/doc/user/clusters/applications.md b/doc/user/clusters/applications.md index e8ef3c59b22..0950dd2bef0 100644 --- a/doc/user/clusters/applications.md +++ b/doc/user/clusters/applications.md @@ -695,14 +695,33 @@ Major upgrades might require additional setup steps, please consult the official [upgrade guide](https://docs.cilium.io/en/stable/install/upgrade/) for more information. -By default, the drop log for traffic is logged out by the +By default, Cilium will drop all non-whitelisted packets upon policy +deployment. The audit mode is scheduled for release in +[Cilium 1.8](https://github.com/cilium/cilium/pull/9970). In the audit +mode non-whitelisted packets will not be dropped, instead audit +notifications will be generated. GitLab provides alternative Docker +images for Cilium with the audit patch included. You can switch to the +custom build and enable the audit mode by adding the following to +`.gitlab/managed-apps/cilium/values.yaml`: + +```yml +global: + registry: registry.gitlab.com/gitlab-org/defend/cilium + policyAuditMode: true + +agent: + monitor: + eventTypes: ["drop", "audit"] +``` + +The Cilium monitor log for traffic is logged out by the `cilium-monitor` sidecar container. You can check these logs via: ```shell kubectl -n gitlab-managed-apps logs cilium-XXXX cilium-monitor ``` -Drop logging can be disabled via `.gitlab/managed-apps/cilium/values.yaml`: +You can disable the monitor log via `.gitlab/managed-apps/cilium/values.yaml`: ```yml agent: diff --git a/lib/gitlab/kubernetes/generic_secret.rb b/lib/gitlab/kubernetes/generic_secret.rb new file mode 100644 index 00000000000..45adf869da0 --- /dev/null +++ b/lib/gitlab/kubernetes/generic_secret.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Gitlab + module Kubernetes + class GenericSecret + attr_reader :name, :data, :namespace_name + + def initialize(name, data, namespace_name) + @name = name + @data = data + @namespace_name = namespace_name + end + + def generate + ::Kubeclient::Resource.new( + type: generic_secret_type, + metadata: metadata, + data: data + ) + end + + private + + def generic_secret_type + 'Opaque' + end + + def metadata + { + name: name, + namespace: namespace_name + } + end + end + end +end diff --git a/lib/gitlab/kubernetes/tls_secret.rb b/lib/gitlab/kubernetes/tls_secret.rb new file mode 100644 index 00000000000..2895f4df27c --- /dev/null +++ b/lib/gitlab/kubernetes/tls_secret.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Gitlab + module Kubernetes + class TlsSecret + attr_reader :name, :cert, :key, :namespace_name + + def initialize(name, cert, key, namespace_name) + @name = name + @cert = cert + @key = key + @namespace_name = namespace_name + end + + def generate + ::Kubeclient::Resource.new( + type: tls_secret_type, + metadata: metadata, + data: data + ) + end + + private + + def tls_secret_type + 'kubernetes.io/tls' + end + + def metadata + { + name: name, + namespace: namespace_name + } + end + + def data + { + 'tls.crt': Base64.strict_encode64(cert), + 'tls.key': Base64.strict_encode64(key) + } + end + end + end +end diff --git a/lib/gitlab/utils/deep_size.rb b/lib/gitlab/utils/deep_size.rb index ed2ceb8af7c..e185786e638 100644 --- a/lib/gitlab/utils/deep_size.rb +++ b/lib/gitlab/utils/deep_size.rb @@ -13,8 +13,8 @@ module Gitlab def initialize(root, max_size: DEFAULT_MAX_SIZE, max_depth: DEFAULT_MAX_DEPTH) @root = root - @max_size = max_size - @max_depth = max_depth + @max_size = max_size || DEFAULT_MAX_SIZE + @max_depth = max_depth || DEFAULT_MAX_DEPTH @size = 0 @depth = 0 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 7bf7cc9051f..773b4ff88a6 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8292,11 +8292,6 @@ msgstr "" msgid "Fetching licenses failed. You are not permitted to perform this action." msgstr "" -msgid "File" -msgid_plural "Files" -msgstr[0] "" -msgstr[1] "" - msgid "File Hooks" msgstr "" @@ -11931,10 +11926,10 @@ msgstr "" msgid "MergeRequest|Error loading full diff. Please try again." msgstr "" -msgid "MergeRequest|Filter files or search with %{modifier_key}+p" +msgid "MergeRequest|No files found" msgstr "" -msgid "MergeRequest|No files found" +msgid "MergeRequest|Search files (%{modifier_key}P)" msgstr "" msgid "Merged" @@ -22185,6 +22180,9 @@ msgstr "" msgid "among other things" msgstr "" +msgid "and" +msgstr "" + msgid "any-approver for the merge request already exists" msgstr "" @@ -22615,6 +22613,11 @@ msgstr "" msgid "failed to dismiss associated finding(id=%{finding_id}): %{message}" msgstr "" +msgid "file" +msgid_plural "files" +msgstr[0] "" +msgstr[1] "" + msgid "finding is not found or is already attached to a vulnerability" msgstr "" diff --git a/spec/controllers/concerns/confirm_email_warning_spec.rb b/spec/controllers/concerns/confirm_email_warning_spec.rb index 93e3423261c..2aad380203b 100644 --- a/spec/controllers/concerns/confirm_email_warning_spec.rb +++ b/spec/controllers/concerns/confirm_email_warning_spec.rb @@ -3,10 +3,6 @@ require 'spec_helper' describe ConfirmEmailWarning do - before do - stub_feature_flags(soft_email_confirmation: true) - end - controller(ApplicationController) do # `described_class` is not available in this context include ConfirmEmailWarning diff --git a/spec/controllers/dashboard/snippets_controller_spec.rb b/spec/controllers/dashboard/snippets_controller_spec.rb index 2d839094d34..d5e3a348cd2 100644 --- a/spec/controllers/dashboard/snippets_controller_spec.rb +++ b/spec/controllers/dashboard/snippets_controller_spec.rb @@ -17,5 +17,14 @@ describe Dashboard::SnippetsController do create(:personal_snippet, :public, author: user) end end + + it 'fetches snippet counts via the snippet count service' do + service = double(:count_service, execute: {}) + expect(Snippets::CountService) + .to receive(:new).with(user, author: user) + .and_return(service) + + get :index + end end end diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb index 0a8aa60ee02..4d1537ae787 100644 --- a/spec/controllers/projects/snippets_controller_spec.rb +++ b/spec/controllers/projects/snippets_controller_spec.rb @@ -27,6 +27,15 @@ describe Projects::SnippetsController do end end + it 'fetches snippet counts via the snippet count service' do + service = double(:count_service, execute: {}) + expect(Snippets::CountService) + .to receive(:new).with(nil, project: project) + .and_return(service) + + get :index, params: { namespace_id: project.namespace, project_id: project } + end + context 'when the project snippet is private' do let!(:project_snippet) { create(:project_snippet, :private, project: project, author: user) } diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb index d7fe3e87056..e36d87a7224 100644 --- a/spec/controllers/registrations_controller_spec.rb +++ b/spec/controllers/registrations_controller_spec.rb @@ -77,34 +77,14 @@ describe RegistrationsController do context 'when send_user_confirmation_email is true' do before do stub_application_setting(send_user_confirmation_email: true) + allow(User).to receive(:allow_unconfirmed_access_for).and_return 2.days end - context 'when soft email confirmation is not enabled' do - before do - stub_feature_flags(soft_email_confirmation: false) - allow(User).to receive(:allow_unconfirmed_access_for).and_return 0 - end - - it 'does not authenticate the user and sends a confirmation email' do - post(:create, params: user_params) - - expect(ActionMailer::Base.deliveries.last.to.first).to eq(user_params[:user][:email]) - expect(subject.current_user).to be_nil - end - end + it 'authenticates the user and sends a confirmation email' do + post(:create, params: user_params) - context 'when soft email confirmation is enabled' do - before do - stub_feature_flags(soft_email_confirmation: true) - allow(User).to receive(:allow_unconfirmed_access_for).and_return 2.days - end - - it 'authenticates the user and sends a confirmation email' do - post(:create, params: user_params) - - expect(ActionMailer::Base.deliveries.last.to.first).to eq(user_params[:user][:email]) - expect(response).to redirect_to(dashboard_projects_path) - end + expect(ActionMailer::Base.deliveries.last.to.first).to eq(user_params[:user][:email]) + expect(response).to redirect_to(dashboard_projects_path) end end diff --git a/spec/factories/serverless/domain_cluster.rb b/spec/factories/serverless/domain_cluster.rb index c44295e2405..bc32552d4c7 100644 --- a/spec/factories/serverless/domain_cluster.rb +++ b/spec/factories/serverless/domain_cluster.rb @@ -5,5 +5,41 @@ FactoryBot.define do pages_domain { create(:pages_domain) } knative { create(:clusters_applications_knative) } creator { create(:user) } + + certificate do + '-----BEGIN CERTIFICATE----- +MIICGzCCAYSgAwIBAgIBATANBgkqhkiG9w0BAQUFADAbMRkwFwYDVQQDExB0ZXN0 +LWNlcnRpZmljYXRlMB4XDTE2MDIxMjE0MzIwMFoXDTIwMDQxMjE0MzIwMFowGzEZ +MBcGA1UEAxMQdGVzdC1jZXJ0aWZpY2F0ZTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw +gYkCgYEApL4J9L0ZxFJ1hI1LPIflAlAGvm6ZEvoT4qKU5Xf2JgU7/2geNR1qlNFa +SvCc08Knupp5yTgmvyK/Xi09U0N82vvp4Zvr/diSc4A/RA6Mta6egLySNT438kdT +nY2tR5feoTLwQpX0t4IMlwGQGT5h6Of2fKmDxzuwuyffcIHqLdsCAwEAAaNvMG0w +DAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUxl9WSxBprB0z0ibJs3rXEk0+95AwCwYD +VR0PBAQDAgXgMBEGCWCGSAGG+EIBAQQEAwIGQDAeBglghkgBhvhCAQ0EERYPeGNh +IGNlcnRpZmljYXRlMA0GCSqGSIb3DQEBBQUAA4GBAGC4T8SlFHK0yPSa+idGLQFQ +joZp2JHYvNlTPkRJ/J4TcXxBTJmArcQgTIuNoBtC+0A/SwdK4MfTCUY4vNWNdese +5A4K65Nb7Oh1AdQieTBHNXXCdyFsva9/ScfQGEl7p55a52jOPs0StPd7g64uvjlg +YHi2yesCrOvVXt+lgPTd +-----END CERTIFICATE-----' + end + + key do + '-----BEGIN PRIVATE KEY----- +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKS+CfS9GcRSdYSN +SzyH5QJQBr5umRL6E+KilOV39iYFO/9oHjUdapTRWkrwnNPCp7qaeck4Jr8iv14t +PVNDfNr76eGb6/3YknOAP0QOjLWunoC8kjU+N/JHU52NrUeX3qEy8EKV9LeCDJcB +kBk+Yejn9nypg8c7sLsn33CB6i3bAgMBAAECgYA2D26w80T7WZvazYr86BNMePpd +j2mIAqx32KZHzt/lhh40J/SRtX9+Kl0Y7nBoRR5Ja9u/HkAIxNxLiUjwg9r6cpg/ +uITEF5nMt7lAk391BuI+7VOZZGbJDsq2ulPd6lO+C8Kq/PI/e4kXcIjeH6KwQsuR +5vrXfBZ3sQfflaiN4QJBANBt8JY2LIGQF8o89qwUpRL5vbnKQ4IzZ5+TOl4RLR7O +AQpJ81tGuINghO7aunctb6rrcKJrxmEH1whzComybrMCQQDKV49nOBudRBAIgG4K +EnLzsRKISUHMZSJiYTYnablof8cKw1JaQduw7zgrUlLwnroSaAGX88+Jw1f5n2Lh +Vlg5AkBDdUGnrDLtYBCDEQYZHblrkc7ZAeCllDOWjxUV+uMqlCv8A4Ey6omvY57C +m6I8DkWVAQx8VPtozhvHjUw80rZHAkB55HWHAM3h13axKG0htCt7klhPsZHpx6MH +EPjGlXIT+aW2XiPmK3ZlCDcWIenE+lmtbOpI159Wpk8BGXs/s/xBAkEAlAY3ymgx +63BDJEwvOb2IaP8lDDxNsXx9XJNVvQbv5n15vNsLHbjslHfAhAbxnLQ1fLhUPqSi +nNp/xedE1YxutQ== +-----END PRIVATE KEY-----' + end end end diff --git a/spec/features/dashboard/snippets_spec.rb b/spec/features/dashboard/snippets_spec.rb index 94dc8601abb..eb9d722e164 100644 --- a/spec/features/dashboard/snippets_spec.rb +++ b/spec/features/dashboard/snippets_spec.rb @@ -59,6 +59,10 @@ describe 'Dashboard snippets' do visit dashboard_snippets_path end + it_behaves_like 'tabs with counts' do + let_it_be(:counts) { { all: '3', public: '1', private: '1', internal: '1' } } + end + it 'contains all snippets of logged user' do expect(page).to have_selector('.snippet-row', count: 3) diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb index 2a1980346e9..0571ab007e9 100644 --- a/spec/features/invites_spec.rb +++ b/spec/features/invites_spec.rb @@ -153,24 +153,6 @@ describe 'Invites' do context 'email confirmation enabled' do let(:send_email_confirmation) { true } - context 'when soft email confirmation is not enabled' do - before do - # stub_feature_flags(soft_email_confirmation: false) - allow(User).to receive(:allow_unconfirmed_access_for).and_return 0 - end - - it 'signs up and redirects to root page with all the project/groups invitation automatically accepted' do - fill_in_sign_up_form(new_user) - confirm_email(new_user) - fill_in_sign_in_form(new_user) - - expect(current_path).to eq(root_path) - expect(page).to have_content(project.full_name) - visit group_path(group) - expect(page).to have_content(group.full_name) - end - end - context 'when soft email confirmation is enabled' do before do allow(User).to receive(:allow_unconfirmed_access_for).and_return 2.days @@ -198,32 +180,14 @@ describe 'Invites' do context 'the user sign-up using a different email address' do let(:invite_email) { build_stubbed(:user).email } - context 'when soft email confirmation is not enabled' do - before do - stub_feature_flags(soft_email_confirmation: false) - allow(User).to receive(:allow_unconfirmed_access_for).and_return 0 - end - - it 'signs up and redirects to the invitation page' do - fill_in_sign_up_form(new_user) - confirm_email(new_user) - fill_in_sign_in_form(new_user) - - expect(current_path).to eq(invite_path(group_invite.raw_invite_token)) - end + before do + allow(User).to receive(:allow_unconfirmed_access_for).and_return 2.days end - context 'when soft email confirmation is enabled' do - before do - stub_feature_flags(soft_email_confirmation: true) - allow(User).to receive(:allow_unconfirmed_access_for).and_return 2.days - end - - it 'signs up and redirects to the invitation page' do - fill_in_sign_up_form(new_user) + it 'signs up and redirects to the invitation page' do + fill_in_sign_up_form(new_user) - expect(current_path).to eq(invite_path(group_invite.raw_invite_token)) - end + expect(current_path).to eq(invite_path(group_invite.raw_invite_token)) end end end diff --git a/spec/features/merge_request/user_sees_versions_spec.rb b/spec/features/merge_request/user_sees_versions_spec.rb index cab86f3fd94..cd62bab412a 100644 --- a/spec/features/merge_request/user_sees_versions_spec.rb +++ b/spec/features/merge_request/user_sees_versions_spec.rb @@ -50,7 +50,7 @@ describe 'Merge request > User sees versions', :js do expect(page).to have_content 'latest version' end - expect(page).to have_content '8 Files' + expect(page).to have_content '8 files' end it_behaves_like 'allows commenting', @@ -84,7 +84,7 @@ describe 'Merge request > User sees versions', :js do end it 'shows comments that were last relevant at that version' do - expect(page).to have_content '5 Files' + expect(page).to have_content '5 files' position = Gitlab::Diff::Position.new( old_path: ".gitmodules", @@ -128,12 +128,10 @@ describe 'Merge request > User sees versions', :js do diff_id: merge_request_diff3.id, start_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' ) - expect(page).to have_content '4 Files' + expect(page).to have_content '4 files' - additions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group svg.ic-file-addition') - .ancestor('.diff-stats-group').text - deletions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group svg.ic-file-deletion') - .ancestor('.diff-stats-group').text + additions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group .js-file-addition-line').text + deletions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group .js-file-deletion-line').text expect(additions_content).to eq '15' expect(deletions_content).to eq '6' @@ -156,12 +154,10 @@ describe 'Merge request > User sees versions', :js do end it 'show diff between new and old version' do - additions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group svg.ic-file-addition') - .ancestor('.diff-stats-group').text - deletions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group svg.ic-file-deletion') - .ancestor('.diff-stats-group').text + additions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group .js-file-addition-line').text + deletions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group .js-file-deletion-line').text - expect(page).to have_content '4 Files' + expect(page).to have_content '4 files' expect(additions_content).to eq '15' expect(deletions_content).to eq '6' end @@ -171,7 +167,7 @@ describe 'Merge request > User sees versions', :js do page.within '.mr-version-dropdown' do expect(page).to have_content 'latest version' end - expect(page).to have_content '8 Files' + expect(page).to have_content '8 files' end it_behaves_like 'allows commenting', @@ -197,7 +193,7 @@ describe 'Merge request > User sees versions', :js do find('.btn-default').click click_link 'version 1' end - expect(page).to have_content '0 Files' + expect(page).to have_content '0 files' end end @@ -223,7 +219,7 @@ describe 'Merge request > User sees versions', :js do expect(page).to have_content 'version 1' end - expect(page).to have_content '0 Files' + expect(page).to have_content '0 files' end end diff --git a/spec/features/projects/snippets/user_views_snippets_spec.rb b/spec/features/projects/snippets/user_views_snippets_spec.rb index a02f7f8a8d2..22910029ee5 100644 --- a/spec/features/projects/snippets/user_views_snippets_spec.rb +++ b/spec/features/projects/snippets/user_views_snippets_spec.rb @@ -31,6 +31,16 @@ describe 'Projects > Snippets > User views snippets' do it_behaves_like 'paginated snippets' end + context 'filtering by visibility' do + before do + visit_project_snippets + end + + it_behaves_like 'tabs with counts' do + let_it_be(:counts) { { all: '1', public: '0', private: '1', internal: '0' } } + end + end + it 'shows snippets' do visit_project_snippets diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb index 5a8db3c070d..0bef61a4854 100644 --- a/spec/features/users/login_spec.rb +++ b/spec/features/users/login_spec.rb @@ -797,7 +797,6 @@ describe 'Login' do before do stub_application_setting(send_user_confirmation_email: true) - stub_feature_flags(soft_email_confirmation: true) allow(User).to receive(:allow_unconfirmed_access_for).and_return grace_period end diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb index daa987ea389..8d5c0657fa5 100644 --- a/spec/features/users/signup_spec.rb +++ b/spec/features/users/signup_spec.rb @@ -129,63 +129,29 @@ shared_examples 'Signup' do stub_application_setting(send_user_confirmation_email: true) end - context 'when soft email confirmation is not enabled' do - before do - stub_feature_flags(soft_email_confirmation: false) - end - - it 'creates the user account and sends a confirmation email' do - visit new_user_registration_path - - fill_in 'new_user_username', with: new_user.username - fill_in 'new_user_email', with: new_user.email - - if Gitlab::Experimentation.enabled?(:signup_flow) - fill_in 'new_user_first_name', with: new_user.first_name - fill_in 'new_user_last_name', with: new_user.last_name - else - fill_in 'new_user_name', with: new_user.name - fill_in 'new_user_email_confirmation', with: new_user.email - end - - fill_in 'new_user_password', with: new_user.password - - expect { click_button 'Register' }.to change { User.count }.by(1) + it 'creates the user account and sends a confirmation email' do + visit new_user_registration_path - expect(current_path).to eq users_almost_there_path - expect(page).to have_content('Please check your email to confirm your account') - end - end + fill_in 'new_user_username', with: new_user.username + fill_in 'new_user_email', with: new_user.email - context 'when soft email confirmation is enabled' do - before do - stub_feature_flags(soft_email_confirmation: true) + if Gitlab::Experimentation.enabled?(:signup_flow) + fill_in 'new_user_first_name', with: new_user.first_name + fill_in 'new_user_last_name', with: new_user.last_name + else + fill_in 'new_user_name', with: new_user.name + fill_in 'new_user_email_confirmation', with: new_user.email end - it 'creates the user account and sends a confirmation email' do - visit new_user_registration_path - - fill_in 'new_user_username', with: new_user.username - fill_in 'new_user_email', with: new_user.email - - if Gitlab::Experimentation.enabled?(:signup_flow) - fill_in 'new_user_first_name', with: new_user.first_name - fill_in 'new_user_last_name', with: new_user.last_name - else - fill_in 'new_user_name', with: new_user.name - fill_in 'new_user_email_confirmation', with: new_user.email - end - - fill_in 'new_user_password', with: new_user.password + fill_in 'new_user_password', with: new_user.password - expect { click_button 'Register' }.to change { User.count }.by(1) + expect { click_button 'Register' }.to change { User.count }.by(1) - if Gitlab::Experimentation.enabled?(:signup_flow) - expect(current_path).to eq users_sign_up_welcome_path - else - expect(current_path).to eq dashboard_projects_path - expect(page).to have_content("Please check your email (#{new_user.email}) to verify that you own this address and unlock the power of CI/CD.") - end + if Gitlab::Experimentation.enabled?(:signup_flow) + expect(current_path).to eq users_sign_up_welcome_path + else + expect(current_path).to eq dashboard_projects_path + expect(page).to have_content("Please check your email (#{new_user.email}) to verify that you own this address and unlock the power of CI/CD.") end end end diff --git a/spec/frontend/diffs/components/compare_versions_spec.js b/spec/frontend/diffs/components/compare_versions_spec.js index 5f919408459..ff92a12eaf6 100644 --- a/spec/frontend/diffs/components/compare_versions_spec.js +++ b/spec/frontend/diffs/components/compare_versions_spec.js @@ -49,8 +49,7 @@ describe('CompareVersions', () => { expect(treeListBtn.exists()).toBe(true); expect(treeListBtn.attributes('title')).toBe('Hide file browser'); - expect(treeListBtn.findAll(Icon).length).not.toBe(0); - expect(treeListBtn.find(Icon).props('name')).toBe('collapse-left'); + expect(treeListBtn.find(Icon).props('name')).toBe('file-tree'); }); it('should render comparison dropdowns with correct values', () => { diff --git a/spec/frontend/diffs/components/diff_stats_spec.js b/spec/frontend/diffs/components/diff_stats_spec.js index 0dffc74f608..5956b478019 100644 --- a/spec/frontend/diffs/components/diff_stats_spec.js +++ b/spec/frontend/diffs/components/diff_stats_spec.js @@ -1,6 +1,6 @@ import { shallowMount } from '@vue/test-utils'; -import Icon from '~/vue_shared/components/icon.vue'; import DiffStats from '~/diffs/components/diff_stats.vue'; +import Icon from '~/vue_shared/components/icon.vue'; describe('diff_stats', () => { it('does not render a group if diffFileLengths is empty', () => { @@ -37,18 +37,18 @@ describe('diff_stats', () => { }, }); + const findFileLine = name => wrapper.find(name); const findIcon = name => wrapper .findAll(Icon) .filter(c => c.attributes('name') === name) .at(0).element.parentNode; - - const additions = findIcon('file-addition'); - const deletions = findIcon('file-deletion'); + const additions = findFileLine('.js-file-addition-line'); + const deletions = findFileLine('.js-file-deletion-line'); const filesChanged = findIcon('doc-code'); - expect(additions.textContent).toContain('100'); - expect(deletions.textContent).toContain('200'); + expect(additions.text()).toBe('100'); + expect(deletions.text()).toBe('200'); expect(filesChanged.textContent).toContain('300'); }); }); diff --git a/spec/frontend/error_tracking/components/error_details_spec.js b/spec/frontend/error_tracking/components/error_details_spec.js index cfb9ce51979..94bf0189c91 100644 --- a/spec/frontend/error_tracking/components/error_details_spec.js +++ b/spec/frontend/error_tracking/components/error_details_spec.js @@ -37,14 +37,13 @@ describe('ErrorDetails', () => { projectPath: '/root/gitlab-test', listPath: '/error_tracking', issueUpdatePath: '/123', - issueDetailsPath: '/123/details', issueStackTracePath: '/stacktrace', projectIssuesPath: '/test-project/issues/', csrfToken: 'fakeToken', }, }); wrapper.setData({ - GQLerror: { + error: { id: 'gid://gitlab/Gitlab::ErrorTracking::DetailedError/129381', sentryId: 129381, title: 'Issue title', @@ -59,7 +58,6 @@ describe('ErrorDetails', () => { beforeEach(() => { actions = { - startPollingDetails: () => {}, startPollingStacktrace: () => {}, updateIgnoreStatus: jest.fn(), updateResolveStatus: jest.fn().mockResolvedValue({ closed_issue_iid: 1 }), @@ -71,8 +69,6 @@ describe('ErrorDetails', () => { }; const state = { - error: {}, - loading: true, stacktraceData: {}, loadingStacktrace: true, }; @@ -93,7 +89,7 @@ describe('ErrorDetails', () => { $apollo: { query, queries: { - GQLerror: { + error: { loading: true, stopPolling: jest.fn(), }, @@ -122,9 +118,7 @@ describe('ErrorDetails', () => { describe('Error details', () => { beforeEach(() => { - store.state.details.loading = false; - store.state.details.error.id = 1; - mocks.$apollo.queries.GQLerror.loading = false; + mocks.$apollo.queries.error.loading = false; mountComponent(); }); @@ -138,16 +132,22 @@ describe('ErrorDetails', () => { describe('Badges', () => { it('should show language and error level badges', () => { - store.state.details.error.tags = { level: 'error', logger: 'ruby' }; - mountComponent(); + wrapper.setData({ + error: { + tags: { level: 'error', logger: 'ruby' }, + }, + }); return wrapper.vm.$nextTick().then(() => { expect(wrapper.findAll(GlBadge).length).toBe(2); }); }); it('should NOT show the badge if the tag is not present', () => { - store.state.details.error.tags = { level: 'error' }; - mountComponent(); + wrapper.setData({ + error: { + tags: { level: 'error' }, + }, + }); return wrapper.vm.$nextTick().then(() => { expect(wrapper.findAll(GlBadge).length).toBe(1); }); @@ -156,8 +156,11 @@ describe('ErrorDetails', () => { it.each(Object.keys(severityLevel))( 'should set correct severity level variant for %s badge', level => { - store.state.details.error.tags = { level: severityLevel[level] }; - mountComponent(); + wrapper.setData({ + error: { + tags: { level: severityLevel[level] }, + }, + }); return wrapper.vm.$nextTick().then(() => { expect(wrapper.find(GlBadge).attributes('variant')).toEqual( severityLevelVariant[severityLevel[level]], @@ -167,8 +170,11 @@ describe('ErrorDetails', () => { ); it('should fallback for ERROR severityLevelVariant when severityLevel is unknown', () => { - store.state.details.error.tags = { level: 'someNewErrorLevel' }; - mountComponent(); + wrapper.setData({ + error: { + tags: { level: 'someNewErrorLevel' }, + }, + }); return wrapper.vm.$nextTick().then(() => { expect(wrapper.find(GlBadge).attributes('variant')).toEqual( severityLevelVariant[severityLevel.ERROR], @@ -180,7 +186,6 @@ describe('ErrorDetails', () => { describe('Stacktrace', () => { it('should show stacktrace', () => { store.state.details.loadingStacktrace = false; - mountComponent(); return wrapper.vm.$nextTick().then(() => { expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); expect(wrapper.find(Stacktrace).exists()).toBe(true); @@ -190,9 +195,10 @@ describe('ErrorDetails', () => { it('should NOT show stacktrace if no entries', () => { store.state.details.loadingStacktrace = false; store.getters = { 'details/sentryUrl': () => 'sentry.io', 'details/stacktrace': () => [] }; - mountComponent(); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); - expect(wrapper.find(Stacktrace).exists()).toBe(false); + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); + expect(wrapper.find(Stacktrace).exists()).toBe(false); + }); }); }); @@ -331,19 +337,18 @@ describe('ErrorDetails', () => { }); describe('GitLab issue link', () => { - const gitlabIssue = 'https://gitlab.example.com/issues/1'; - const findGitLabLink = () => wrapper.find(`[href="${gitlabIssue}"]`); + const gitlabIssuePath = 'https://gitlab.example.com/issues/1'; + const findGitLabLink = () => wrapper.find(`[href="${gitlabIssuePath}"]`); const findCreateIssueButton = () => wrapper.find('[data-qa-selector="create_issue_button"]'); const findViewIssueButton = () => wrapper.find('[data-qa-selector="view_issue_button"]'); describe('is present', () => { beforeEach(() => { - store.state.details.loading = false; - store.state.details.error = { - id: 1, - gitlab_issue: gitlabIssue, - }; - mountComponent(); + wrapper.setData({ + error: { + gitlabIssuePath, + }, + }); }); it('should display the View issue button', () => { @@ -361,12 +366,11 @@ describe('ErrorDetails', () => { describe('is not present', () => { beforeEach(() => { - store.state.details.loading = false; - store.state.details.error = { - id: 1, - gitlab_issue: null, - }; - mountComponent(); + wrapper.setData({ + error: { + gitlabIssuePath: null, + }, + }); }); it('should not display the View issue button', () => { @@ -390,9 +394,9 @@ describe('ErrorDetails', () => { const findGitLabCommitLink = () => wrapper.find(`[href$="${gitlabCommitPath}"]`); it('should display a link', () => { - mocks.$apollo.queries.GQLerror.loading = false; + mocks.$apollo.queries.error.loading = false; wrapper.setData({ - GQLerror: { + error: { gitlabCommit, gitlabCommitPath, }, @@ -403,9 +407,9 @@ describe('ErrorDetails', () => { }); it('should not display a link', () => { - mocks.$apollo.queries.GQLerror.loading = false; + mocks.$apollo.queries.error.loading = false; wrapper.setData({ - GQLerror: { + error: { gitlabCommit: null, }, }); diff --git a/spec/frontend/error_tracking/components/error_tracking_list_spec.js b/spec/frontend/error_tracking/components/error_tracking_list_spec.js index 310cd676ca1..9cf73d54d9b 100644 --- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js +++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js @@ -239,7 +239,7 @@ describe('ErrorTrackingList', () => { expect(actions.updateStatus).toHaveBeenCalledWith( expect.anything(), { - endpoint: '/project/test/-/error_tracking/3.json', + endpoint: `/project/test/-/error_tracking/${errorsList[0].id}.json`, redirectUrl: '/error_tracking', status: 'ignored', }, @@ -267,7 +267,7 @@ describe('ErrorTrackingList', () => { expect(actions.updateStatus).toHaveBeenCalledWith( expect.anything(), { - endpoint: '/project/test/-/error_tracking/3.json', + endpoint: `/project/test/-/error_tracking/${errorsList[0].id}.json`, redirectUrl: '/error_tracking', status: 'resolved', }, diff --git a/spec/frontend/error_tracking/store/details/actions_spec.js b/spec/frontend/error_tracking/store/details/actions_spec.js index fa37886f176..6802300b0f5 100644 --- a/spec/frontend/error_tracking/store/details/actions_spec.js +++ b/spec/frontend/error_tracking/store/details/actions_spec.js @@ -26,53 +26,6 @@ describe('Sentry error details store actions', () => { } }); - describe('startPollingDetails', () => { - const endpoint = '123/details'; - it('should commit SET_ERROR with received response', done => { - const payload = { error: { id: 1 } }; - mockedAdapter.onGet().reply(200, payload); - testAction( - actions.startPollingDetails, - { endpoint }, - {}, - [ - { type: types.SET_ERROR, payload: payload.error }, - { type: types.SET_LOADING, payload: false }, - ], - [], - () => { - done(); - }, - ); - }); - - it('should show flash on API error', done => { - mockedAdapter.onGet().reply(400); - - testAction( - actions.startPollingDetails, - { endpoint }, - {}, - [{ type: types.SET_LOADING, payload: false }], - [], - () => { - expect(createFlash).toHaveBeenCalledTimes(1); - done(); - }, - ); - }); - - it('should not restart polling when receiving an empty 204 response', done => { - mockedRestart = jest.spyOn(Poll.prototype, 'restart'); - mockedAdapter.onGet().reply(204); - - testAction(actions.startPollingDetails, { endpoint }, {}, [], [], () => { - expect(mockedRestart).toHaveBeenCalledTimes(0); - done(); - }); - }); - }); - describe('startPollingStacktrace', () => { const endpoint = '123/stacktrace'; it('should commit SET_ERROR with received response', done => { diff --git a/spec/frontend/jobs/components/log/mock_data.js b/spec/frontend/jobs/components/log/mock_data.js index 587818045eb..cdf5a3e10b1 100644 --- a/spec/frontend/jobs/components/log/mock_data.js +++ b/spec/frontend/jobs/components/log/mock_data.js @@ -34,7 +34,7 @@ export const utilsMockData = [ content: [ { text: - 'Using Docker executor with image dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.5-golang-1.12-git-2.24-lfs-2.9-chrome-73.0-node-12.x-yarn-1.16-postgresql-9.6-graphicsmagick-1.3.33', + 'Using Docker executor with image dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.5-golang-1.12-git-2.24-lfs-2.9-chrome-73.0-node-12.x-yarn-1.16-postgresql-9.6-graphicsmagick-1.3.34', }, ], section: 'prepare-executor', diff --git a/spec/helpers/projects/error_tracking_helper_spec.rb b/spec/helpers/projects/error_tracking_helper_spec.rb index d4dcd7eb54e..38a6ef6826b 100644 --- a/spec/helpers/projects/error_tracking_helper_spec.rb +++ b/spec/helpers/projects/error_tracking_helper_spec.rb @@ -83,7 +83,6 @@ describe Projects::ErrorTrackingHelper do describe '#error_details_data' do let(:issue_id) { 1234 } let(:route_params) { [project.owner, project, issue_id, { format: :json }] } - let(:details_path) { details_namespace_project_error_tracking_index_path(*route_params) } let(:project_path) { project.full_path } let(:stack_trace_path) { stack_trace_namespace_project_error_tracking_index_path(*route_params) } let(:issues_path) { project_issues_path(project) } @@ -98,10 +97,6 @@ describe Projects::ErrorTrackingHelper do expect(result['project-path']).to eq project_path end - it 'returns the correct details path' do - expect(result['issue-details-path']).to eq details_path - end - it 'returns the correct stack trace path' do expect(result['issue-stack-trace-path']).to eq stack_trace_path end diff --git a/spec/lib/gitlab/kubernetes/generic_secret_spec.rb b/spec/lib/gitlab/kubernetes/generic_secret_spec.rb new file mode 100644 index 00000000000..fe1d4cc11e6 --- /dev/null +++ b/spec/lib/gitlab/kubernetes/generic_secret_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Kubernetes::GenericSecret do + let(:secret) { described_class.new(name, data, namespace) } + let(:name) { 'example-name' } + let(:data) { 'example-data' } + let(:namespace) { 'example-namespace' } + + describe '#generate' do + subject { secret.generate } + + let(:resource) do + ::Kubeclient::Resource.new( + type: 'Opaque', + metadata: { name: name, namespace: namespace }, + data: data + ) + end + + it { is_expected.to eq(resource) } + end +end diff --git a/spec/lib/gitlab/kubernetes/tls_secret_spec.rb b/spec/lib/gitlab/kubernetes/tls_secret_spec.rb new file mode 100644 index 00000000000..438a0dc79fc --- /dev/null +++ b/spec/lib/gitlab/kubernetes/tls_secret_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Kubernetes::TlsSecret do + let(:secret) { described_class.new(name, cert, key, namespace) } + let(:name) { 'example-name' } + let(:cert) { 'example-cert' } + let(:key) { 'example-key' } + let(:namespace) { 'example-namespace' } + + let(:data) do + { + 'tls.crt': Base64.strict_encode64(cert), + 'tls.key': Base64.strict_encode64(key) + } + end + + describe '#generate' do + subject { secret.generate } + + let(:resource) do + ::Kubeclient::Resource.new( + type: 'kubernetes.io/tls', + metadata: { name: name, namespace: namespace }, + data: data + ) + end + + it { is_expected.to eq(resource) } + end +end diff --git a/spec/lib/gitlab/utils/deep_size_spec.rb b/spec/lib/gitlab/utils/deep_size_spec.rb index ccd202b33f7..5a155fb6c80 100644 --- a/spec/lib/gitlab/utils/deep_size_spec.rb +++ b/spec/lib/gitlab/utils/deep_size_spec.rb @@ -17,29 +17,45 @@ describe Gitlab::Utils::DeepSize do let(:max_size) { 1.kilobyte } let(:max_depth) { 10 } - let(:deep_size) { described_class.new(data, max_size: max_size, max_depth: max_depth) } - describe '#evaluate' do - context 'when data within size and depth limits' do - it 'returns true' do - expect(deep_size).to be_valid + subject(:deep_size) { described_class.new(data, max_size: max_size, max_depth: max_depth) } + + it { expect(described_class::DEFAULT_MAX_SIZE).to eq(1.megabyte) } + it { expect(described_class::DEFAULT_MAX_DEPTH).to eq(100) } + + describe '#initialize' do + context 'when max_size is nil' do + let(:max_size) { nil } + + it 'sets max_size to DEFAULT_MAX_SIZE' do + expect(subject.instance_variable_get(:@max_size)).to eq(described_class::DEFAULT_MAX_SIZE) + end + end + + context 'when max_depth is nil' do + let(:max_depth) { nil } + + it 'sets max_depth to DEFAULT_MAX_DEPTH' do + expect(subject.instance_variable_get(:@max_depth)).to eq(described_class::DEFAULT_MAX_DEPTH) end end + end + + describe '#valid?' do + context 'when data within size and depth limits' do + it { is_expected.to be_valid } + end context 'when data not within size limit' do let(:max_size) { 200.bytes } - it 'returns false' do - expect(deep_size).not_to be_valid - end + it { is_expected.not_to be_valid } end context 'when data not within depth limit' do let(:max_depth) { 2 } - it 'returns false' do - expect(deep_size).not_to be_valid - end + it { is_expected.not_to be_valid } end end diff --git a/spec/models/serverless/domain_cluster_spec.rb b/spec/models/serverless/domain_cluster_spec.rb index 9bc5c04678b..bd645b7d0aa 100644 --- a/spec/models/serverless/domain_cluster_spec.rb +++ b/spec/models/serverless/domain_cluster_spec.rb @@ -50,4 +50,12 @@ describe ::Serverless::DomainCluster do describe 'domain' do it { is_expected.to respond_to(:domain) } end + + describe 'certificate' do + it { is_expected.to respond_to(:certificate) } + end + + describe 'key' do + it { is_expected.to respond_to(:key) } + end end diff --git a/spec/services/clusters/kubernetes/configure_istio_ingress_service_spec.rb b/spec/services/clusters/kubernetes/configure_istio_ingress_service_spec.rb new file mode 100644 index 00000000000..572e2b91187 --- /dev/null +++ b/spec/services/clusters/kubernetes/configure_istio_ingress_service_spec.rb @@ -0,0 +1,197 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Clusters::Kubernetes::ConfigureIstioIngressService, '#execute' do + include KubernetesHelpers + + let(:cluster) { create(:cluster, :project, :provided_by_gcp) } + let(:api_url) { 'https://kubernetes.example.com' } + let(:project) { cluster.project } + let(:environment) { create(:environment, project: project) } + let(:cluster_project) { cluster.cluster_project } + let(:namespace) { "#{project.name}-#{project.id}-#{environment.slug}" } + let(:kubeclient) { cluster.kubeclient } + + subject do + described_class.new( + cluster: cluster + ).execute + end + + before do + stub_kubeclient_discover_istio(api_url) + stub_kubeclient_create_secret(api_url, namespace: namespace) + stub_kubeclient_put_secret(api_url, "#{namespace}-token", namespace: namespace) + + stub_kubeclient_get_secret( + api_url, + { + metadata_name: "#{namespace}-token", + token: Base64.encode64('sample-token'), + namespace: namespace + } + ) + + stub_kubeclient_get_secret( + api_url, + { + metadata_name: 'istio-ingressgateway-ca-certs', + namespace: 'istio-system' + } + ) + + stub_kubeclient_get_secret( + api_url, + { + metadata_name: 'istio-ingressgateway-certs', + namespace: 'istio-system' + } + ) + + stub_kubeclient_put_secret(api_url, 'istio-ingressgateway-ca-certs', namespace: 'istio-system') + stub_kubeclient_put_secret(api_url, 'istio-ingressgateway-certs', namespace: 'istio-system') + stub_kubeclient_get_gateway(api_url, 'knative-ingress-gateway', namespace: 'knative-serving') + stub_kubeclient_put_gateway(api_url, 'knative-ingress-gateway', namespace: 'knative-serving') + end + + context 'without a serverless_domain_cluster' do + it 'configures gateway to use PASSTHROUGH' do + subject + + expect(WebMock).to have_requested(:put, api_url + '/apis/networking.istio.io/v1alpha3/namespaces/knative-serving/gateways/knative-ingress-gateway').with( + body: hash_including( + apiVersion: "networking.istio.io/v1alpha3", + kind: "Gateway", + metadata: { + generation: 1, + labels: { + "networking.knative.dev/ingress-provider" => "istio", + "serving.knative.dev/release" => "v0.7.0" + }, + name: "knative-ingress-gateway", + namespace: "knative-serving", + selfLink: "/apis/networking.istio.io/v1alpha3/namespaces/knative-serving/gateways/knative-ingress-gateway" + }, + spec: { + selector: { + istio: "ingressgateway" + }, + servers: [ + { + hosts: ["*"], + port: { + name: "http", + number: 80, + protocol: "HTTP" + } + }, + { + hosts: ["*"], + port: { + name: "https", + number: 443, + protocol: "HTTPS" + }, + tls: { + mode: "PASSTHROUGH" + } + } + ] + } + ) + ) + end + end + + context 'with a serverless_domain_cluster' do + let(:serverless_domain_cluster) { create(:serverless_domain_cluster) } + let(:certificate) { OpenSSL::X509::Certificate.new(serverless_domain_cluster.certificate) } + + before do + cluster.application_knative = serverless_domain_cluster.knative + end + + it 'configures certificates' do + subject + + expect(serverless_domain_cluster.reload.key).not_to be_blank + expect(serverless_domain_cluster.reload.certificate).not_to be_blank + + expect(certificate.subject.to_s).to include(serverless_domain_cluster.knative.hostname) + + expect(certificate.not_before).to be_within(1.minute).of(Time.now) + expect(certificate.not_after).to be_within(1.minute).of(Time.now + 1000.years) + + expect(WebMock).to have_requested(:put, api_url + '/api/v1/namespaces/istio-system/secrets/istio-ingressgateway-ca-certs').with( + body: hash_including( + metadata: { + name: 'istio-ingressgateway-ca-certs', + namespace: 'istio-system' + }, + type: 'Opaque' + ) + ) + + expect(WebMock).to have_requested(:put, api_url + '/api/v1/namespaces/istio-system/secrets/istio-ingressgateway-certs').with( + body: hash_including( + metadata: { + name: 'istio-ingressgateway-certs', + namespace: 'istio-system' + }, + type: 'kubernetes.io/tls' + ) + ) + end + + it 'configures gateway to use MUTUAL' do + subject + + expect(WebMock).to have_requested(:put, api_url + '/apis/networking.istio.io/v1alpha3/namespaces/knative-serving/gateways/knative-ingress-gateway').with( + body: { + apiVersion: "networking.istio.io/v1alpha3", + kind: "Gateway", + metadata: { + generation: 1, + labels: { + "networking.knative.dev/ingress-provider" => "istio", + "serving.knative.dev/release" => "v0.7.0" + }, + name: "knative-ingress-gateway", + namespace: "knative-serving", + selfLink: "/apis/networking.istio.io/v1alpha3/namespaces/knative-serving/gateways/knative-ingress-gateway" + }, + spec: { + selector: { + istio: "ingressgateway" + }, + servers: [ + { + hosts: ["*"], + port: { + name: "http", + number: 80, + protocol: "HTTP" + } + }, + { + hosts: ["*"], + port: { + name: "https", + number: 443, + protocol: "HTTPS" + }, + tls: { + mode: "MUTUAL", + privateKey: "/etc/istio/ingressgateway-certs/tls.key", + serverCertificate: "/etc/istio/ingressgateway-certs/tls.crt", + caCertificates: "/etc/istio/ingressgateway-ca-certs/cert.pem" + } + } + ] + } + } + ) + end + end +end diff --git a/spec/services/snippets/count_service_spec.rb b/spec/services/snippets/count_service_spec.rb new file mode 100644 index 00000000000..4137e65dcca --- /dev/null +++ b/spec/services/snippets/count_service_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Snippets::CountService do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :public) } + + describe '#new' do + it 'raises an error if no author or project' do + expect { described_class.new(user) }.to raise_error(ArgumentError) + end + + it 'uses the SnippetsFinder to scope snippets by user' do + expect(SnippetsFinder) + .to receive(:new) + .with(user, author: user, project: nil) + + described_class.new(user, author: user) + end + + it 'allows scoping to project' do + expect(SnippetsFinder) + .to receive(:new) + .with(user, author: nil, project: project) + + described_class.new(user, project: project) + end + end + + describe '#execute' do + subject { described_class.new(user, author: user).execute } + + it 'returns a hash of counts' do + expect(subject).to match({ + are_public: 0, + are_internal: 0, + are_private: 0, + are_public_or_internal: 0, + total: 0 + }) + end + + it 'only counts snippets the user has access to' do + create(:personal_snippet, :private, author: user) + create(:project_snippet, :private, author: user) + create(:project_snippet, :private, author: create(:user)) + + expect(subject).to match({ + are_public: 0, + are_internal: 0, + are_private: 1, + are_public_or_internal: 0, + total: 1 + }) + end + + it 'returns an empty hash if select returns nil' do + allow_next_instance_of(described_class) do |instance| + allow(instance).to receive(:snippet_counts).and_return(nil) + end + + expect(subject).to match({}) + end + end +end diff --git a/spec/support/shared_examples/features/snippets_shared_examples.rb b/spec/support/shared_examples/features/snippets_shared_examples.rb index 5c35617bd36..1c8a9714bdf 100644 --- a/spec/support/shared_examples/features/snippets_shared_examples.rb +++ b/spec/support/shared_examples/features/snippets_shared_examples.rb @@ -18,3 +18,35 @@ RSpec.shared_examples 'paginated snippets' do |remote: false| end end end + +RSpec.shared_examples 'tabs with counts' do + let(:tabs) { page.all('.snippet-scope-menu li') } + + it 'shows a tab for All snippets and count' do + tab = tabs[0] + + expect(tab.text).to include('All') + expect(tab.find('.badge').text).to eq(counts[:all]) + end + + it 'shows a tab for Private snippets and count' do + tab = tabs[1] + + expect(tab.text).to include('Private') + expect(tab.find('.badge').text).to eq(counts[:private]) + end + + it 'shows a tab for Internal snippets and count' do + tab = tabs[2] + + expect(tab.text).to include('Internal') + expect(tab.find('.badge').text).to eq(counts[:internal]) + end + + it 'shows a tab for Public snippets and count' do + tab = tabs[3] + + expect(tab.text).to include('Public') + expect(tab.find('.badge').text).to eq(counts[:public]) + end +end diff --git a/spec/workers/cluster_configure_istio_worker_spec.rb b/spec/workers/cluster_configure_istio_worker_spec.rb new file mode 100644 index 00000000000..0f02d428ced --- /dev/null +++ b/spec/workers/cluster_configure_istio_worker_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ClusterConfigureIstioWorker do + describe '#perform' do + shared_examples 'configure istio service' do + it 'configures istio' do + expect_any_instance_of(Clusters::Kubernetes::ConfigureIstioIngressService).to receive(:execute) + + described_class.new.perform(cluster.id) + end + end + + context 'when provider type is gcp' do + let(:cluster) { create(:cluster, :project, :provided_by_gcp) } + + it_behaves_like 'configure istio service' + end + + context 'when provider type is aws' do + let(:cluster) { create(:cluster, :project, :provided_by_aws) } + + it_behaves_like 'configure istio service' + end + + context 'when provider type is user' do + let(:cluster) { create(:cluster, :project, :provided_by_user) } + + it_behaves_like 'configure istio service' + end + + context 'when cluster does not exist' do + it 'does not provision a cluster' do + expect_any_instance_of(Clusters::Kubernetes::ConfigureIstioIngressService).not_to receive(:execute) + + described_class.new.perform(123) + end + end + end +end |