diff options
94 files changed, 1803 insertions, 390 deletions
diff --git a/.rubocop.yml b/.rubocop.yml index dca7ff24008..128b89773a0 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -837,7 +837,9 @@ Rails/TimeZone: Rails/SaveBang: Enabled: true AllowImplicitReturn: false - AllowedReceivers: ['ActionDispatch::TestRequest'] + AllowedReceivers: + - ActionDispatch::TestRequest + - Tempfile Include: - 'spec/**/*.rb' - 'ee/spec/**/*.rb' diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml index 471aed8912f..c568dce5138 100644 --- a/.rubocop_todo/layout/line_length.yml +++ b/.rubocop_todo/layout/line_length.yml @@ -3201,7 +3201,7 @@ Layout/LineLength: - 'spec/features/milestones/user_views_milestone_spec.rb' - 'spec/features/milestones/user_views_milestones_spec.rb' - 'spec/features/oauth_login_spec.rb' - - 'spec/features/profiles/active_sessions_spec.rb' + - 'spec/features/user_settings/active_sessions_spec.rb' - 'spec/features/profiles/password_spec.rb' - 'spec/features/profiles/personal_access_tokens_spec.rb' - 'spec/features/profiles/two_factor_auths_spec.rb' diff --git a/.rubocop_todo/rspec/feature_category.yml b/.rubocop_todo/rspec/feature_category.yml index c956d05f6f0..4ea3134accf 100644 --- a/.rubocop_todo/rspec/feature_category.yml +++ b/.rubocop_todo/rspec/feature_category.yml @@ -1605,7 +1605,6 @@ RSpec/FeatureCategory: - 'spec/controllers/oauth/tokens_controller_spec.rb' - 'spec/controllers/passwords_controller_spec.rb' - 'spec/controllers/profiles/accounts_controller_spec.rb' - - 'spec/controllers/profiles/active_sessions_controller_spec.rb' - 'spec/controllers/profiles/avatars_controller_spec.rb' - 'spec/controllers/profiles/emails_controller_spec.rb' - 'spec/controllers/profiles/gpg_keys_controller_spec.rb' diff --git a/.rubocop_todo/style/class_and_module_children.yml b/.rubocop_todo/style/class_and_module_children.yml index 89b20f231f9..fb5031f513c 100644 --- a/.rubocop_todo/style/class_and_module_children.yml +++ b/.rubocop_todo/style/class_and_module_children.yml @@ -102,7 +102,7 @@ Style/ClassAndModuleChildren: - 'app/controllers/oauth/token_info_controller.rb' - 'app/controllers/oauth/tokens_controller.rb' - 'app/controllers/profiles/accounts_controller.rb' - - 'app/controllers/profiles/active_sessions_controller.rb' + - 'app/controllers/user_settings/active_sessions_controller.rb' - 'app/controllers/profiles/application_controller.rb' - 'app/controllers/profiles/avatars_controller.rb' - 'app/controllers/profiles/chat_names_controller.rb' diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 15058411922..e6c4740ae00 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -9171724863b73d3e98c1b2021a6095a58ac128e1 +b8fda37aff7f658c1b0195fd36cefc93fa3bb718 @@ -390,7 +390,7 @@ group :development do gem 'solargraph', '~> 0.47.2', require: false # rubocop:todo Gemfile/MissingFeatureCategory gem 'letter_opener_web', '~> 2.0.0' # rubocop:todo Gemfile/MissingFeatureCategory - gem 'lookbook', '~> 2.0', '>= 2.0.1' # rubocop:todo Gemfile/MissingFeatureCategory + gem 'lookbook', '~> 2.2' # rubocop:todo Gemfile/MissingFeatureCategory # Better errors handler gem 'better_errors', '~> 2.10.1' # rubocop:todo Gemfile/MissingFeatureCategory diff --git a/Gemfile.checksum b/Gemfile.checksum index bb6e4f41724..0e6b7025851 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -347,7 +347,7 @@ {"name":"lockbox","version":"1.3.0","platform":"ruby","checksum":"ca8e5806e4e0c56d1d762ac5cf401940ff53fc37554ef623d3313c7a6331a3ea"}, {"name":"lograge","version":"0.11.2","platform":"ruby","checksum":"4cbd1554b86f545d795eff15a0c24fd25057d2ac4e1caa5fc186168b3da932ef"}, {"name":"loofah","version":"2.22.0","platform":"ruby","checksum":"10d76e070c86b12fec74b6a9515fd1940f4459198b991342d0a7897d86c372fe"}, -{"name":"lookbook","version":"2.0.1","platform":"ruby","checksum":"0f14729c8c992810de0792a0be865a5792e5765fbaea5950cce74c6e5c73fc4a"}, +{"name":"lookbook","version":"2.2.0","platform":"ruby","checksum":"27c869364cf8bb8e9c61e43f909cbf5aec91dc35f4db791c3d0b7287a637a2d9"}, {"name":"lru_redux","version":"1.1.0","platform":"ruby","checksum":"ee71d0ccab164c51de146c27b480a68b3631d5b4297b8ffe8eda1c72de87affb"}, {"name":"lumberjack","version":"1.2.7","platform":"ruby","checksum":"a5c6aae6b4234f1420dbcd80b23e3bca0817bd239440dde097ebe3fa63c63b1f"}, {"name":"mail","version":"2.8.1","platform":"ruby","checksum":"ec3b9fadcf2b3755c78785cb17bc9a0ca9ee9857108a64b6f5cfc9c0b5bfc9ad"}, diff --git a/Gemfile.lock b/Gemfile.lock index bce114b23e8..5be2590f8fa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1019,7 +1019,7 @@ GEM loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) - lookbook (2.0.1) + lookbook (2.2.0) activemodel css_parser htmlbeautifier (~> 1.3) @@ -1950,7 +1950,7 @@ DEPENDENCIES lockbox (~> 1.3.0) lograge (~> 0.5) loofah (~> 2.22.0) - lookbook (~> 2.0, >= 2.0.1) + lookbook (~> 2.2) lru_redux mail (= 2.8.1) mail-smtp_pool (~> 0.1.0)! diff --git a/app/assets/javascripts/environments/components/environment_form.vue b/app/assets/javascripts/environments/components/environment_form.vue index c6cf6b7e24b..a4c2d4fcc51 100644 --- a/app/assets/javascripts/environments/components/environment_form.vue +++ b/app/assets/javascripts/environments/components/environment_form.vue @@ -7,7 +7,6 @@ import { GlCollapsibleListbox, GlLink, GlSprintf, - GlAlert, } from '@gitlab/ui'; import { helpPagePath } from '~/helpers/help_page_helper'; import { isAbsolute } from '~/lib/utils/url_utility'; @@ -19,9 +18,9 @@ import { import csrf from '~/lib/utils/csrf'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import getNamespacesQuery from '../graphql/queries/k8s_namespaces.query.graphql'; import getUserAuthorizedAgents from '../graphql/queries/user_authorized_agents.query.graphql'; import EnvironmentFluxResourceSelector from './environment_flux_resource_selector.vue'; +import EnvironmentNamespaceSelector from './environment_namespace_selector.vue'; export default { components: { @@ -32,8 +31,8 @@ export default { GlCollapsibleListbox, GlLink, GlSprintf, - GlAlert, EnvironmentFluxResourceSelector, + EnvironmentNamespaceSelector, }, mixins: [glFeatureFlagsMixin()], inject: { @@ -72,8 +71,6 @@ export default { urlFeedback: __('The URL should start with http:// or https://'), agentLabel: s__('Environments|GitLab agent'), agentHelpText: s__('Environments|Select agent'), - namespaceLabel: s__('Environments|Kubernetes namespace (optional)'), - namespaceHelpText: s__('Environments|Select namespace'), save: __('Save'), cancel: __('Cancel'), reset: __('Reset'), @@ -93,35 +90,9 @@ export default { selectedAgentId: this.environment.clusterAgentId, agentSearchTerm: '', selectedNamespace: this.environment.kubernetesNamespace, - k8sNamespaces: [], - namespaceSearchTerm: '', kubernetesError: '', }; }, - apollo: { - k8sNamespaces: { - query: getNamespacesQuery, - skip() { - return !this.showNamespaceSelector; - }, - variables() { - return { - configuration: this.k8sAccessConfiguration, - }; - }, - update(data) { - return data?.k8sNamespaces || []; - }, - error(error) { - this.kubernetesError = error.message; - }, - result(result) { - if (!result?.error && !result.errors?.length) { - this.kubernetesError = null; - } - }, - }, - }, computed: { loadingNamespacesList() { return this.$apollo.queries.k8sNamespaces.loading; @@ -161,26 +132,9 @@ export default { item.text.toLowerCase().includes(lowerCasedSearchTerm), ); }, - namespacesList() { - return this.k8sNamespaces.map((item) => { - return { - value: item.metadata.name, - text: item.metadata.name, - }; - }); - }, - filteredNamespacesList() { - const lowerCasedSearchTerm = this.namespaceSearchTerm.toLowerCase(); - return this.namespacesList.filter((item) => - item.text.toLowerCase().includes(lowerCasedSearchTerm), - ); - }, showNamespaceSelector() { return Boolean(this.selectedAgentId); }, - namespaceDropdownToggleText() { - return this.selectedNamespace || this.$options.i18n.namespaceHelpText; - }, showFluxResourceSelector() { return Boolean(this.selectedNamespace && this.selectedAgentId); }, @@ -239,9 +193,6 @@ export default { fluxResourcePath: null, }); }, - onNamespaceSearch(search) { - this.namespaceSearchTerm = search; - }, }, }; </script> @@ -334,34 +285,14 @@ export default { /> </gl-form-group> - <gl-form-group + <environment-namespace-selector v-if="showNamespaceSelector" - :label="$options.i18n.namespaceLabel" - label-for="environment_namespace" - > - <gl-alert v-if="kubernetesError" variant="warning" :dismissible="false" class="gl-mb-5"> - {{ kubernetesError }} - </gl-alert> - <gl-collapsible-listbox - v-else - id="environment_namespace" - v-model="selectedNamespace" - class="gl-w-full" - data-testid="namespace-selector" - block - :items="filteredNamespacesList" - :loading="loadingNamespacesList" - :toggle-text="namespaceDropdownToggleText" - :header-text="$options.i18n.namespaceHelpText" - :reset-button-label="$options.i18n.reset" - :searchable="true" - @search="onNamespaceSearch" - @select=" - onChange({ ...environment, kubernetesNamespace: $event, fluxResourcePath: null }) - " - @reset="onChange({ ...environment, kubernetesNamespace: null })" - /> - </gl-form-group> + :namespace="selectedNamespace" + :configuration="k8sAccessConfiguration" + @change=" + onChange({ ...environment, kubernetesNamespace: $event, fluxResourcePath: null }) + " + /> <environment-flux-resource-selector v-if="showFluxResourceSelector" diff --git a/app/assets/javascripts/environments/components/environment_namespace_selector.vue b/app/assets/javascripts/environments/components/environment_namespace_selector.vue new file mode 100644 index 00000000000..101d70d36f3 --- /dev/null +++ b/app/assets/javascripts/environments/components/environment_namespace_selector.vue @@ -0,0 +1,136 @@ +<script> +import { GlFormGroup, GlCollapsibleListbox, GlAlert, GlButton, GlSprintf } from '@gitlab/ui'; +import { __, s__ } from '~/locale'; +import getNamespacesQuery from '../graphql/queries/k8s_namespaces.query.graphql'; + +export default { + components: { + GlFormGroup, + GlCollapsibleListbox, + GlAlert, + GlButton, + GlSprintf, + }, + props: { + configuration: { + required: true, + type: Object, + }, + namespace: { + required: false, + type: String, + default: '', + }, + }, + i18n: { + namespaceLabel: s__('Environments|Kubernetes namespace (optional)'), + namespaceHelpText: s__('Environments|Select namespace'), + selectButton: s__('Environments|Or select namespace: %{searchTerm}'), + reset: __('Reset'), + }, + data() { + return { + k8sNamespaces: [], + searchTerm: '', + kubernetesError: '', + }; + }, + apollo: { + k8sNamespaces: { + query: getNamespacesQuery, + + variables() { + return { + configuration: this.configuration, + }; + }, + update(data) { + return ( + data?.k8sNamespaces?.map((item) => { + return { + value: item.metadata.name, + text: item.metadata.name, + }; + }) || [] + ); + }, + error(error) { + this.kubernetesError = error.message; + }, + result(result) { + if (!result?.error && !result.errors?.length) { + this.kubernetesError = null; + } + }, + }, + }, + computed: { + loadingNamespacesList() { + return this.$apollo.queries.k8sNamespaces.loading; + }, + filteredNamespacesList() { + const lowerCasedSearchTerm = this.searchTerm.toLowerCase(); + return this.k8sNamespaces.filter((item) => + item.text.toLowerCase().includes(lowerCasedSearchTerm), + ); + }, + namespaceDropdownToggleText() { + return this.namespace || this.$options.i18n.namespaceHelpText; + }, + shouldRenderSelectButton() { + const hasSearchedItem = this.k8sNamespaces.some( + (item) => item.text === this.searchTerm.toLowerCase(), + ); + return this.searchTerm && !hasSearchedItem; + }, + }, + methods: { + onChange(namespace) { + this.$emit('change', namespace); + }, + onNamespaceSearch(search) { + this.searchTerm = search; + }, + onSelect(namespace) { + this.onChange(namespace); + this.$refs.namespaceSelector.close(); + }, + }, +}; +</script> +<template> + <gl-form-group :label="$options.i18n.namespaceLabel" label-for="environment_namespace"> + <gl-alert v-if="kubernetesError" variant="warning" :dismissible="false" class="gl-mb-5"> + {{ kubernetesError }} + </gl-alert> + <gl-collapsible-listbox + id="environment_namespace" + ref="namespaceSelector" + :selected="namespace" + class="gl-w-full" + block + :items="filteredNamespacesList" + :loading="loadingNamespacesList" + :toggle-text="namespaceDropdownToggleText" + :header-text="$options.i18n.namespaceHelpText" + :reset-button-label="$options.i18n.reset" + :searchable="true" + @search="onNamespaceSearch" + @select="onChange" + @reset="onChange" + > + <template v-if="shouldRenderSelectButton" #footer> + <gl-button + category="tertiary" + class="gl-justify-content-start! gl-border-t-1! gl-border-t-solid gl-border-t-gray-200 gl-pl-7! gl-rounded-top-left-none! gl-rounded-top-right-none!" + :class="{ 'gl-mt-3': !filteredNamespacesList.length }" + @click="onSelect(searchTerm)" + > + <gl-sprintf :message="$options.i18n.selectButton"> + <template #searchTerm>{{ searchTerm }}</template> + </gl-sprintf> + </gl-button> + </template> + </gl-collapsible-listbox> + </gl-form-group> +</template> diff --git a/app/assets/javascripts/environments/constants.js b/app/assets/javascripts/environments/constants.js index 4f0eabb26a4..2fe9008c042 100644 --- a/app/assets/javascripts/environments/constants.js +++ b/app/assets/javascripts/environments/constants.js @@ -162,7 +162,7 @@ const ERROR_OTHER = 'other'; export const CLUSTER_AGENT_ERROR_MESSAGES = { [ERROR_UNAUTHORIZED]: s__( - 'Environment|Unauthorized to access the cluster agent from this environment. Check your authentication and try again.', + "Environment|You don't have permission to view all the namespaces in the cluster. If a namespace is not shown, you can still enter its name to select it.", ), [ERROR_FORBIDDEN]: s__( 'Environment|Forbidden to access the cluster agent from this environment.', diff --git a/app/assets/javascripts/environments/folder/environments_folder_app.vue b/app/assets/javascripts/environments/folder/environments_folder_app.vue index f2c1b2f5cdf..6804e5cc0c7 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_app.vue +++ b/app/assets/javascripts/environments/folder/environments_folder_app.vue @@ -1,13 +1,29 @@ <script> -import { GlSkeletonLoader } from '@gitlab/ui'; -import { s__ } from '~/locale'; +import { GlSkeletonLoader, GlTabs, GlTab, GlBadge } from '@gitlab/ui'; +import { __, s__ } from '~/locale'; import folderQuery from '../graphql/queries/folder.query.graphql'; +import environmentToDeleteQuery from '../graphql/queries/environment_to_delete.query.graphql'; +import environmentToRollbackQuery from '../graphql/queries/environment_to_rollback.query.graphql'; +import environmentToStopQuery from '../graphql/queries/environment_to_stop.query.graphql'; +import environmentToChangeCanaryQuery from '../graphql/queries/environment_to_change_canary.query.graphql'; import EnvironmentItem from '../components/new_environment_item.vue'; +import StopEnvironmentModal from '../components/stop_environment_modal.vue'; +import ConfirmRollbackModal from '../components/confirm_rollback_modal.vue'; +import DeleteEnvironmentModal from '../components/delete_environment_modal.vue'; +import CanaryUpdateModal from '../components/canary_update_modal.vue'; +import { ENVIRONMENTS_SCOPE } from '../constants'; export default { components: { + GlBadge, + GlTabs, + GlTab, GlSkeletonLoader, EnvironmentItem, + StopEnvironmentModal, + ConfirmRollbackModal, + DeleteEnvironmentModal, + CanaryUpdateModal, }, props: { folderName: { @@ -18,6 +34,20 @@ export default { type: String, required: true, }, + scope: { + type: String, + required: true, + default: ENVIRONMENTS_SCOPE.ACTIVE, + }, + }, + data() { + return { + environmentToDelete: {}, + environmentToRollback: {}, + environmentToStop: {}, + environmentToChangeCanary: {}, + weight: 0, + }; }, apollo: { folder: { @@ -25,11 +55,27 @@ export default { variables() { return { environment: this.environmentQueryData, - scope: '', + scope: this.scope, search: '', perPage: 10, }; }, + pollInterval: 3000, + }, + environmentToDelete: { + query: environmentToDeleteQuery, + }, + environmentToRollback: { + query: environmentToRollbackQuery, + }, + environmentToStop: { + query: environmentToStopQuery, + }, + environmentToChangeCanary: { + query: environmentToChangeCanaryQuery, + }, + weight: { + query: environmentToChangeCanaryQuery, }, }, computed: { @@ -42,18 +88,65 @@ export default { isLoading() { return this.$apollo.queries.folder.loading; }, + activeCount() { + return this.folder?.activeCount ?? '-'; + }, + stoppedCount() { + return this.folder?.stoppedCount ?? '-'; + }, + activeTab() { + return this.scope === ENVIRONMENTS_SCOPE.ACTIVE ? 0 : 1; + }, + }, + methods: { + setScope(scope) { + if (scope !== this.scope) { + this.$router.push({ query: { scope } }); + } + }, }, i18n: { pageTitle: s__('Environments|Environments'), + active: __('Active'), + stopped: __('Stopped'), }, + ENVIRONMENTS_SCOPE, }; </script> <template> <div> + <delete-environment-modal :environment="environmentToDelete" graphql /> + <stop-environment-modal :environment="environmentToStop" graphql /> + <confirm-rollback-modal :environment="environmentToRollback" graphql /> + <canary-update-modal :environment="environmentToChangeCanary" :weight="weight" /> <h4 class="gl-font-weight-normal" data-testid="folder-name"> {{ $options.i18n.pageTitle }} / <b>{{ folderName }}</b> </h4> + <gl-tabs :value="activeTab" query-param-name="scope"> + <gl-tab + :query-param-value="$options.ENVIRONMENTS_SCOPE.ACTIVE" + @click="setScope($options.ENVIRONMENTS_SCOPE.ACTIVE)" + > + <template #title> + <span>{{ $options.i18n.active }}</span> + <gl-badge size="sm" class="gl-tab-counter-badge"> + {{ activeCount }} + </gl-badge> + </template> + </gl-tab> + <gl-tab + :query-param-value="$options.ENVIRONMENTS_SCOPE.STOPPED" + @click="setScope($options.ENVIRONMENTS_SCOPE.STOPPED)" + > + <template #title> + <span>{{ $options.i18n.stopped }}</span> + <gl-badge size="sm" class="gl-tab-counter-badge"> + {{ stoppedCount }} + </gl-badge> + </template> + </gl-tab> + </gl-tabs> <div v-if="isLoading"> <div v-for="n in 3" @@ -63,12 +156,15 @@ export default { <gl-skeleton-loader :lines="2" /> </div> </div> - <environment-item - v-for="environment in environments" - :key="environment.name" - :environment="environment" - class="gl-border-gray-100 gl-border-t-solid gl-border-1 gl-pt-3" - in-folder - /> + <div v-else> + <environment-item + v-for="environment in environments" + :id="environment.name" + :key="environment.name" + :environment="environment" + class="gl-border-gray-100 gl-border-t-solid gl-border-1 gl-pt-3" + in-folder + /> + </div> </div> </template> diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js index 2f06f003bb2..44a5d1e3662 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js +++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; +import VueRouter from 'vue-router'; import createDefaultClient from '~/lib/graphql'; import Translate from '~/vue_shared/translate'; import { apolloProvider } from '../graphql/client'; @@ -17,28 +18,46 @@ export default () => { const el = document.getElementById('environments-folder-list-view'); const environmentsData = el.dataset; if (gon.features.environmentsFolderNewLook) { + Vue.use(VueRouter); + const folderName = environmentsData.environmentsDataFolderName; const folderPath = environmentsData.environmentsDataEndpoint.replace('.json', ''); const projectPath = environmentsData.environmentsDataProjectPath; const helpPagePath = environmentsData.environmentsDataHelpPagePath; + const router = new VueRouter({ + mode: 'history', + base: window.location.pathname, + routes: [ + { + path: '/', + name: 'environments_folder', + component: EnvironmentsFolderApp, + props: (route) => ({ + scope: route.query.scope, + folderName, + folderPath, + }), + }, + ], + scrollBehavior(to, from, savedPosition) { + if (savedPosition) { + return savedPosition; + } + return { top: 0 }; + }, + }); + return new Vue({ el, - components: { - EnvironmentsFolderApp, - }, provide: { projectPath, helpPagePath, }, apolloProvider, + router, render(createElement) { - return createElement('environments-folder-app', { - props: { - folderName, - folderPath, - }, - }); + return createElement('router-view'); }, }); } diff --git a/app/assets/javascripts/environments/graphql/resolvers/base.js b/app/assets/javascripts/environments/graphql/resolvers/base.js index 404b7024cde..8b0ac039290 100644 --- a/app/assets/javascripts/environments/graphql/resolvers/base.js +++ b/app/assets/javascripts/environments/graphql/resolvers/base.js @@ -75,7 +75,7 @@ export const baseQueries = (endpoint) => ({ }); export const baseMutations = { - stopEnvironmentREST(_, { environment }, { client }) { + stopEnvironmentREST(_, { environment }, { client, cache }) { client.writeQuery({ query: isEnvironmentStoppingQuery, variables: { environment }, @@ -84,6 +84,9 @@ export const baseMutations = { return axios .post(environment.stopPath) .then(() => buildErrors()) + .then(() => { + cache.evict({ fieldName: 'folder' }); + }) .catch(() => { client.writeQuery({ query: isEnvironmentStoppingQuery, @@ -95,10 +98,11 @@ export const baseMutations = { ]); }); }, - deleteEnvironment(_, { environment: { deletePath } }) { + deleteEnvironment(_, { environment: { deletePath } }, { cache }) { return axios .delete(deletePath) .then(() => buildErrors()) + .then(() => cache.evict({ fieldName: 'folder' })) .catch(() => buildErrors([ s__( @@ -107,10 +111,13 @@ export const baseMutations = { ]), ); }, - rollbackEnvironment(_, { environment, isLastDeployment }) { + rollbackEnvironment(_, { environment, isLastDeployment }, { cache }) { return axios .post(environment?.retryUrl) .then(() => buildErrors()) + .then(() => { + cache.evict({ fieldName: 'folder' }); + }) .catch(() => { buildErrors([ isLastDeployment diff --git a/app/assets/javascripts/ide/init_gitlab_web_ide.js b/app/assets/javascripts/ide/init_gitlab_web_ide.js index 868830c953a..f5fb4c8be2f 100644 --- a/app/assets/javascripts/ide/init_gitlab_web_ide.js +++ b/app/assets/javascripts/ide/init_gitlab_web_ide.js @@ -6,10 +6,13 @@ import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_action'; import { createAndSubmitForm } from '~/lib/utils/create_and_submit_form'; import csrf from '~/lib/utils/csrf'; import Tracking from '~/tracking'; -import { getBaseConfig } from './lib/gitlab_web_ide/get_base_config'; -import { setupRootElement } from './lib/gitlab_web_ide/setup_root_element'; +import { + getBaseConfig, + getOAuthConfig, + setupRootElement, + handleTracking, +} from './lib/gitlab_web_ide'; import { GITLAB_WEB_IDE_FEEDBACK_ISSUE } from './constants'; -import { handleTracking } from './lib/gitlab_web_ide/handle_tracking_event'; const buildRemoteIdeURL = (ideRemotePath, remoteHost, remotePathArg) => { const remotePath = cleanLeadingSeparator(remotePathArg); @@ -51,15 +54,21 @@ export const initGitlabWebIDE = async (el) => { : null; const forkInfo = forkInfoJSON ? JSON.parse(forkInfoJSON) : null; + const oauthConfig = getOAuthConfig(el.dataset); + const httpHeaders = oauthConfig + ? undefined + : // Use same headers as defined in axios_utils (not needed in oauth) + { + [csrf.headerKey]: csrf.token, + 'X-Requested-With': 'XMLHttpRequest', + }; + // See ClientOnlyConfig https://gitlab.com/gitlab-org/gitlab-web-ide/-/blob/main/packages/web-ide-types/src/config.ts#L17 start(rootEl, { ...getBaseConfig(), nonce, - // Use same headers as defined in axios_utils - httpHeaders: { - [csrf.headerKey]: csrf.token, - 'X-Requested-With': 'XMLHttpRequest', - }, + httpHeaders, + auth: oauthConfig, projectPath, ref, filePath, diff --git a/app/assets/javascripts/ide/lib/gitlab_web_ide/get_oauth_config.js b/app/assets/javascripts/ide/lib/gitlab_web_ide/get_oauth_config.js new file mode 100644 index 00000000000..5493a9ba7c7 --- /dev/null +++ b/app/assets/javascripts/ide/lib/gitlab_web_ide/get_oauth_config.js @@ -0,0 +1,12 @@ +export const getOAuthConfig = ({ clientId, callbackUrl }) => { + if (!clientId) { + return undefined; + } + + return { + type: 'oauth', + clientId, + callbackUrl, + protectRefreshToken: true, + }; +}; diff --git a/app/assets/javascripts/ide/lib/gitlab_web_ide/index.js b/app/assets/javascripts/ide/lib/gitlab_web_ide/index.js index 8311e11672e..87e0002c8c8 100644 --- a/app/assets/javascripts/ide/lib/gitlab_web_ide/index.js +++ b/app/assets/javascripts/ide/lib/gitlab_web_ide/index.js @@ -1,2 +1,4 @@ export * from './get_base_config'; +export * from './get_oauth_config'; +export * from './handle_tracking_event'; export * from './setup_root_element'; diff --git a/app/assets/javascripts/ide/mount_oauth_callback.js b/app/assets/javascripts/ide/mount_oauth_callback.js new file mode 100644 index 00000000000..79fffb24f8e --- /dev/null +++ b/app/assets/javascripts/ide/mount_oauth_callback.js @@ -0,0 +1,12 @@ +import { oauthCallback } from '@gitlab/web-ide'; +import { getBaseConfig, getOAuthConfig } from './lib/gitlab_web_ide'; + +export const mountOAuthCallback = () => { + const el = document.getElementById('ide'); + + return oauthCallback({ + ...getBaseConfig(), + username: gon.current_username, + auth: getOAuthConfig(el.dataset), + }); +}; diff --git a/app/assets/javascripts/pages/ide/index.js b/app/assets/javascripts/pages/ide/index/index.js index 15933256e75..15933256e75 100644 --- a/app/assets/javascripts/pages/ide/index.js +++ b/app/assets/javascripts/pages/ide/index/index.js diff --git a/app/assets/javascripts/pages/ide/oauth_redirect/index.js b/app/assets/javascripts/pages/ide/oauth_redirect/index.js new file mode 100644 index 00000000000..ee9233fab38 --- /dev/null +++ b/app/assets/javascripts/pages/ide/oauth_redirect/index.js @@ -0,0 +1,3 @@ +import { mountOAuthCallback } from '~/ide/mount_oauth_callback'; + +mountOAuthCallback(); diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_title.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_title.vue index 50fcd3c9350..478d261d06c 100644 --- a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_title.vue +++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_title.vue @@ -37,7 +37,6 @@ export default { category="tertiary" size="small" class="float-right js-sidebar-dropdown-toggle gl-mr-n2" - data-qa-selector="labels_edit_button" @click="toggleDropdownContents" > {{ __('Edit') }} diff --git a/app/assets/javascripts/super_sidebar/components/menu_section.vue b/app/assets/javascripts/super_sidebar/components/menu_section.vue index a672e254004..f93a0256bd4 100644 --- a/app/assets/javascripts/super_sidebar/components/menu_section.vue +++ b/app/assets/javascripts/super_sidebar/components/menu_section.vue @@ -111,7 +111,7 @@ export default { :id="`menu-section-button-${itemId}`" class="gl-rounded-base gl-relative gl-display-flex gl-align-items-center gl-min-h-7 gl-gap-3 gl-mb-2 gl-py-2 gl-px-3 gl-text-black-normal! gl-hover-bg-t-gray-a-08 gl-focus-bg-t-gray-a-08 gl-text-decoration-none! gl-appearance-none gl-border-0 gl-bg-transparent gl-text-left gl-w-full gl-focus--focus" :class="computedLinkClasses" - data-qa-selector="menu_section_button" + data-testid="menu-section-button" :data-qa-section-name="item.title" v-bind="buttonProps" @click="isExpanded = !isExpanded" @@ -153,7 +153,7 @@ export default { :id="itemId" v-model="isExpanded" class="gl-list-style-none gl-p-0 gl-m-0 gl-transition-duration-medium gl-transition-timing-function-ease" - data-qa-selector="menu_section" + data-testid="menu-section" :data-qa-section-name="item.title" > <slot> diff --git a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue index 52015484cb5..3beec4ccc14 100644 --- a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue +++ b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue @@ -188,7 +188,6 @@ export default { class="super-sidebar" :class="peekClasses" data-testid="super-sidebar" - data-qa-selector="navbar" :inert="sidebarState.isCollapsed" @mouseenter="isMouseover = true" @mouseleave="isMouseover = false" diff --git a/app/assets/javascripts/super_sidebar/components/user_menu.vue b/app/assets/javascripts/super_sidebar/components/user_menu.vue index 5dab74374df..1e8f0a81088 100644 --- a/app/assets/javascripts/super_sidebar/components/user_menu.vue +++ b/app/assets/javascripts/super_sidebar/components/user_menu.vue @@ -150,7 +150,7 @@ export default { href: this.data.sign_out_link, extraAttrs: { 'data-method': 'post', - 'data-testid': 'sign_out_link', + 'data-testid': 'sign-out-link', class: 'sign-out-link', }, }, diff --git a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue index 2bdc8a174d0..e12e06a2454 100644 --- a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue +++ b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue @@ -36,11 +36,6 @@ export default { required: false, default: 'confirm-danger-button', }, - buttonQaSelector: { - type: String, - required: false, - default: null, - }, buttonVariant: { type: String, required: false, @@ -58,7 +53,6 @@ export default { :variant="buttonVariant" :disabled="disabled" :data-testid="buttonTestid" - :data-qa-selector="buttonQaSelector" >{{ buttonText }}</gl-button > <confirm-danger-modal diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue index 67ad7769c7c..f3b483c5f53 100644 --- a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue +++ b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue @@ -100,7 +100,7 @@ export default { type="search" class="mb-3" autofocus - data-qa-selector="project_search_field" + data-testid="project-search-field" @input="onInput" /> <div class="d-flex flex-column"> @@ -120,7 +120,7 @@ export default { :project="project" :matcher="searchQuery" class="js-project-list-item" - data-qa-selector="project_list_item" + data-testid="project-list-item" @click="projectClicked(project)" /> </div> diff --git a/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue b/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue index c1ec39e1545..b81d288d932 100644 --- a/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue +++ b/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue @@ -110,7 +110,6 @@ export default { :loading="isLoading" :variant="variant" :category="category" - :data-qa-selector="`${feature.type}_mr_button`" @click="mutate" >{{ $options.i18n.buttonLabel }}</gl-button > diff --git a/app/controllers/ide_controller.rb b/app/controllers/ide_controller.rb index d9566121dcd..4a4d41f3e6f 100644 --- a/app/controllers/ide_controller.rb +++ b/app/controllers/ide_controller.rb @@ -5,7 +5,8 @@ class IdeController < ApplicationController include StaticObjectExternalStorageCSP include Gitlab::Utils::StrongMemoize - before_action :authorize_read_project! + before_action :authorize_read_project!, only: [:index] + before_action :ensure_web_ide_oauth_application!, only: [:index] before_action do push_frontend_feature_flag(:build_service_proxy) @@ -27,12 +28,28 @@ class IdeController < ApplicationController render layout: helpers.use_new_web_ide? ? 'fullscreen' : 'application' end + def oauth_redirect + return render_404 unless ::Gitlab::WebIde::DefaultOauthApplication.feature_enabled?(current_user) + # TODO - It's **possible** we end up here and no oauth application has been set up. + # We need to have better handling of these edge cases. Here's a follow-up issue: + # https://gitlab.com/gitlab-org/gitlab/-/issues/433322 + return render_404 unless ::Gitlab::WebIde::DefaultOauthApplication.oauth_application + + render layout: 'fullscreen', locals: { minimal: true } + end + private def authorize_read_project! render_404 unless can?(current_user, :read_project, project) end + def ensure_web_ide_oauth_application! + return unless ::Gitlab::WebIde::DefaultOauthApplication.feature_enabled?(current_user) + + ::Gitlab::WebIde::DefaultOauthApplication.ensure_oauth_application! + end + def fork_info(project, branch) return if can?(current_user, :push_code, project) diff --git a/app/controllers/profiles/active_sessions_controller.rb b/app/controllers/user_settings/active_sessions_controller.rb index 5a86179b89f..da5664a8c1b 100644 --- a/app/controllers/profiles/active_sessions_controller.rb +++ b/app/controllers/user_settings/active_sessions_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class Profiles::ActiveSessionsController < Profiles::ApplicationController +class UserSettings::ActiveSessionsController < Profiles::ApplicationController feature_category :system_access def index @@ -13,7 +13,7 @@ class Profiles::ActiveSessionsController < Profiles::ApplicationController current_user.forget_me! respond_to do |format| - format.html { redirect_to profile_active_sessions_url, status: :found } + format.html { redirect_to user_settings_active_sessions_url, status: :found } format.js { head :ok } end end diff --git a/app/graphql/mutations/branch_rules/update.rb b/app/graphql/mutations/branch_rules/update.rb new file mode 100644 index 00000000000..c10e11970eb --- /dev/null +++ b/app/graphql/mutations/branch_rules/update.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Mutations + module BranchRules + class Update < BaseMutation + graphql_name 'BranchRuleUpdate' + + include FindsProject + + authorize :admin_project + + argument :id, ::Types::GlobalIDType[::ProtectedBranch], + required: true, + description: 'Global ID of the protected branch.' + + argument :name, GraphQL::Types::String, + required: true, + description: 'Branch name, with wildcards, for the branch rules.' + + argument :project_path, GraphQL::Types::ID, + required: true, + description: 'Full path to the project that the branch is associated with.' + + field :branch_rule, + Types::Projects::BranchRuleType, + null: true, + description: 'Branch rule after mutation.' + + def resolve(id:, project_path:, name:) + protected_branch = ::Gitlab::Graphql::Lazy.force(GitlabSchema.object_from_id(id, + expected_type: ::ProtectedBranch)) + raise_resource_not_available_error! unless protected_branch + + project = authorized_find!(project_path) + + protected_branch = ::ProtectedBranches::UpdateService.new(project, current_user, + { name: name }).execute(protected_branch) + + if protected_branch.errors.empty? + { + branch_rule: ::Projects::BranchRule.new(project, protected_branch), + errors: [] + } + else + { errors: errors_on_object(protected_branch) } + end + end + end + end +end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 9b3c443200d..5593911d1fb 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -110,6 +110,7 @@ module Types mount_mutation Mutations::Organizations::Update, alpha: { milestone: '16.7' } mount_mutation Mutations::Projects::SyncFork, calls_gitaly: true, alpha: { milestone: '15.9' } mount_mutation Mutations::Projects::Star, alpha: { milestone: '16.7' } + mount_mutation Mutations::BranchRules::Update, alpha: { milestone: '16.7' } mount_mutation Mutations::Releases::Create mount_mutation Mutations::Releases::Update mount_mutation Mutations::Releases::Delete diff --git a/app/helpers/active_sessions_helper.rb b/app/helpers/active_sessions_helper.rb index cfe0b747e78..48639526c31 100644 --- a/app/helpers/active_sessions_helper.rb +++ b/app/helpers/active_sessions_helper.rb @@ -24,6 +24,6 @@ module ActiveSessionsHelper end def revoke_session_path(active_session) - profile_active_session_path(active_session.session_private_id) + user_settings_active_session_path(active_session.session_private_id) end end diff --git a/app/helpers/ide_helper.rb b/app/helpers/ide_helper.rb index f2d393f1f77..2ec11b8a9ed 100644 --- a/app/helpers/ide_helper.rb +++ b/app/helpers/ide_helper.rb @@ -52,6 +52,19 @@ module IdeHelper {} end + def new_ide_oauth_data + return {} unless ::Gitlab::WebIde::DefaultOauthApplication.feature_enabled?(current_user) + return {} unless ::Gitlab::WebIde::DefaultOauthApplication.oauth_application + + client_id = ::Gitlab::WebIde::DefaultOauthApplication.oauth_application.uid + callback_url = ::Gitlab::WebIde::DefaultOauthApplication.oauth_callback_url + + { + 'client-id' => client_id, + 'callback-url' => callback_url + } + end + def new_ide_data(project:) { 'project-path' => project&.path_with_namespace, @@ -59,7 +72,7 @@ module IdeHelper # We will replace these placeholders in the FE 'ide-remote-path' => ide_remote_path(remote_host: ':remote_host', remote_path: ':remote_path'), 'editor-font' => new_ide_fonts.to_json - }.merge(new_ide_code_suggestions_data) + }.merge(new_ide_code_suggestions_data).merge(new_ide_oauth_data) end def legacy_ide_data(project:) diff --git a/app/helpers/nav/new_dropdown_helper.rb b/app/helpers/nav/new_dropdown_helper.rb index 88e834b537a..af81e7832c8 100644 --- a/app/helpers/nav/new_dropdown_helper.rb +++ b/app/helpers/nav/new_dropdown_helper.rb @@ -116,7 +116,7 @@ module Nav id: 'general_new_project', title: _('New project/repository'), href: new_project_path, - data: { track_action: 'click_link_new_project', track_label: 'plus_menu_dropdown', track_property: 'navigation_top', testid: 'global_new_project_link' } + data: { track_action: 'click_link_new_project', track_label: 'plus_menu_dropdown', track_property: 'navigation_top', testid: 'global-new-project-link' } ) ) end @@ -127,7 +127,7 @@ module Nav id: 'general_new_group', title: _('New group'), href: new_group_path, - data: { track_action: 'click_link_new_group', track_label: 'plus_menu_dropdown', track_property: 'navigation_top', testid: 'global_new_group_link' } + data: { track_action: 'click_link_new_group', track_label: 'plus_menu_dropdown', track_property: 'navigation_top', testid: 'global-new-group-link' } ) ) end @@ -149,7 +149,7 @@ module Nav id: 'general_new_snippet', title: _('New snippet'), href: new_snippet_path, - data: { track_action: 'click_link_new_snippet_parent', track_label: 'plus_menu_dropdown', track_property: 'navigation_top', testid: 'global_new_snippet_link' } + data: { track_action: 'click_link_new_snippet_parent', track_label: 'plus_menu_dropdown', track_property: 'navigation_top', testid: 'global-new-snippet-link' } ) ) end diff --git a/app/helpers/ssh_keys_helper.rb b/app/helpers/ssh_keys_helper.rb index 974a6869528..d640e7ffba9 100644 --- a/app/helpers/ssh_keys_helper.rb +++ b/app/helpers/ssh_keys_helper.rb @@ -33,7 +33,6 @@ module SshKeysHelper title: title, aria_label: title, modal_attributes: { - 'data-qa-selector': 'ssh_key_revoke_modal', title: _('Are you sure you want to revoke this SSH key?'), message: _('This action cannot be undone, and will permanently delete the %{key} SSH key. All commits signed using this SSH key will be marked as unverified.') % { key: key.title }, okVariant: 'danger', diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 7942b2b9236..51cdd993c97 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -42,6 +42,7 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord add_authentication_token_field :error_tracking_access_token, encrypted: :required belongs_to :push_rule + belongs_to :web_ide_oauth_application, class_name: 'Doorkeeper::Application' alias_attribute :housekeeping_optimize_repository_period, :housekeeping_incremental_repack_period diff --git a/app/models/ci/instance_variable.rb b/app/models/ci/instance_variable.rb index 3e572dbe18f..179befb8469 100644 --- a/app/models/ci/instance_variable.rb +++ b/app/models/ci/instance_variable.rb @@ -47,5 +47,9 @@ module Ci end end end + + def audit_details + key + end end end diff --git a/app/views/ide/oauth_redirect.html.haml b/app/views/ide/oauth_redirect.html.haml new file mode 100644 index 00000000000..e0ce9f9768c --- /dev/null +++ b/app/views/ide/oauth_redirect.html.haml @@ -0,0 +1,3 @@ +- page_title _("IDE") + += render partial: 'shared/ide_root', locals: { data: new_ide_oauth_data, loading_text: _('Authenticating...') } diff --git a/app/views/shared/_ide_root.html.haml b/app/views/shared/_ide_root.html.haml index db3e76e188c..173b081d693 100644 --- a/app/views/shared/_ide_root.html.haml +++ b/app/views/shared/_ide_root.html.haml @@ -5,6 +5,6 @@ -# 100vh because of the presence of the bottom bar #ide.gl-h-full{ data: data } - .web-ide-loader.gl-display-flex.gl-justify-content-center.gl-align-items-center.gl-flex-direction-column.gl-h-full.gl-mr-auto.gl-ml-auto + .web-ide-loader.gl-display-flex.gl-justify-content-center.gl-align-items-center.gl-flex-direction-column.gl-h-full.gl-mx-auto = brand_header_logo %h3.clblack.gl-mt-6= loading_text diff --git a/app/views/profiles/active_sessions/_active_session.html.haml b/app/views/user_settings/active_sessions/_active_session.html.haml index e91c28e6e84..e91c28e6e84 100644 --- a/app/views/profiles/active_sessions/_active_session.html.haml +++ b/app/views/user_settings/active_sessions/_active_session.html.haml diff --git a/app/views/profiles/active_sessions/index.html.haml b/app/views/user_settings/active_sessions/index.html.haml index baca9559e08..fb1ca1a431c 100644 --- a/app/views/profiles/active_sessions/index.html.haml +++ b/app/views/user_settings/active_sessions/index.html.haml @@ -12,4 +12,4 @@ = render Pajamas::CardComponent.new(card_options: { class: 'gl-border-0' }, body_options: { class: 'gl-p-0' }) do |c| - c.with_body do %ul.list-group.list-group-flush - = render partial: 'profiles/active_sessions/active_session', collection: @sessions + = render partial: 'user_settings/active_sessions/active_session', collection: @sessions diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 89047bda445..3d8c349910e 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -246,6 +246,15 @@ :weight: 1 :idempotent: false :tags: [] +- :name: cronjob:ci_catalog_resources_process_sync_events + :worker_name: Ci::Catalog::Resources::ProcessSyncEventsWorker + :feature_category: :pipeline_composition + :has_external_dependencies: false + :urgency: :high + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] - :name: cronjob:ci_delete_unit_tests :worker_name: Ci::DeleteUnitTestsWorker :feature_category: :code_testing @@ -2694,15 +2703,6 @@ :weight: 1 :idempotent: true :tags: [] -- :name: ci_catalog_resources_process_sync_events - :worker_name: Ci::Catalog::Resources::ProcessSyncEventsWorker - :feature_category: :pipeline_composition - :has_external_dependencies: false - :urgency: :high - :resource_boundary: :unknown - :weight: 1 - :idempotent: true - :tags: [] - :name: ci_delete_objects :worker_name: Ci::DeleteObjectsWorker :feature_category: :continuous_integration diff --git a/app/workers/ci/catalog/resources/process_sync_events_worker.rb b/app/workers/ci/catalog/resources/process_sync_events_worker.rb index a577f36858d..10a3e4f0a35 100644 --- a/app/workers/ci/catalog/resources/process_sync_events_worker.rb +++ b/app/workers/ci/catalog/resources/process_sync_events_worker.rb @@ -5,8 +5,18 @@ module Ci module Resources # This worker can be called multiple times simultaneously but only one can process events # at a time. This is ensured by `try_obtain_lease` in `Ci::ProcessSyncEventsService`. + # + # This worker is enqueued in 3 ways: + # 1. By Project model callback after updating one of the columns referenced in + # `Ci::Catalog::Resource#sync_with_project`. + # 2. Every minute by cron job. This ensures we process SyncEvents from direct/bulk + # database updates that do not use the Project AR model. + # 3. By `Ci::ProcessSyncEventsService` if there are any remaining pending + # SyncEvents after processing. + # class ProcessSyncEventsWorker include ApplicationWorker + include CronjobQueue # rubocop: disable Scalability/CronWorkerContext -- Periodic processing is required feature_category :pipeline_composition @@ -17,6 +27,8 @@ module Ci deduplicate :until_executed, if_deduplicated: :reschedule_once, ttl: 1.minute def perform + return if Feature.disabled?(:ci_process_catalog_resource_sync_events) + results = ::Ci::ProcessSyncEventsService.new( ::Ci::Catalog::Resources::SyncEvent, ::Ci::Catalog::Resource ).execute diff --git a/config/feature_flags/development/cache_autocomplete_sources_members.yml b/config/feature_flags/development/cache_autocomplete_sources_members.yml index c80a7490031..f55c604153a 100644 --- a/config/feature_flags/development/cache_autocomplete_sources_members.yml +++ b/config/feature_flags/development/cache_autocomplete_sources_members.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/427452 milestone: '16.5' type: development group: group::global search -default_enabled: false +default_enabled: true diff --git a/config/feature_flags/development/web_ide_oauth.yml b/config/feature_flags/development/web_ide_oauth.yml new file mode 100644 index 00000000000..fc3132d01b4 --- /dev/null +++ b/config/feature_flags/development/web_ide_oauth.yml @@ -0,0 +1,8 @@ +--- +name: web_ide_oauth +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/138015 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/433324 +milestone: '16.7' +type: development +group: group::ide +default_enabled: false diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index ef1199fefef..24182e6b7db 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -700,6 +700,9 @@ Settings.cron_jobs['deactivated_pages_deployments_delete_cron_worker']['job_clas Settings.cron_jobs['ci_schedule_unlock_pipelines_in_queue_worker'] ||= {} Settings.cron_jobs['ci_schedule_unlock_pipelines_in_queue_worker']['cron'] ||= '*/1 * * * *' Settings.cron_jobs['ci_schedule_unlock_pipelines_in_queue_worker']['job_class'] = 'Ci::ScheduleUnlockPipelinesInQueueCronWorker' +Settings.cron_jobs['ci_catalog_resources_process_sync_events_worker'] ||= {} +Settings.cron_jobs['ci_catalog_resources_process_sync_events_worker']['cron'] ||= '*/1 * * * *' +Settings.cron_jobs['ci_catalog_resources_process_sync_events_worker']['job_class'] = 'Ci::Catalog::Resources::ProcessSyncEventsWorker' Gitlab.ee do Settings.cron_jobs['analytics_devops_adoption_create_all_snapshots_worker'] ||= {} diff --git a/config/routes.rb b/config/routes.rb index 18c3e257f8e..b433a88f1c5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -131,6 +131,7 @@ InitializerConnections.raise_if_new_database_connection do scope :ide, as: :ide, format: false do get '/', to: 'ide#index' get '/project', to: 'ide#index' + get '/oauth_redirect', to: 'ide#oauth_redirect' scope path: 'project/:project_id', as: :project, constraints: { project_id: Gitlab::PathRegex.full_namespace_route_regex } do %w[edit tree blob].each do |action| diff --git a/config/routes/profile.rb b/config/routes/profile.rb index b4f00fa4ad8..36feb075f4b 100644 --- a/config/routes/profile.rb +++ b/config/routes/profile.rb @@ -61,7 +61,7 @@ resource :profile, only: [:show, :update] do put :revoke end end - resources :active_sessions, only: [:index, :destroy] + resources :emails, only: [:index, :create, :destroy] do member do put :resend_confirmation_instructions diff --git a/config/routes/user_settings.rb b/config/routes/user_settings.rb index 478d807c8b5..f7891118af3 100644 --- a/config/routes/user_settings.rb +++ b/config/routes/user_settings.rb @@ -1,7 +1,17 @@ # frozen_string_literal: true -scope module: 'user_settings' do - namespace :user_settings do +namespace :user_settings do + scope module: 'user_settings' do get :authentication_log end + resources :active_sessions, only: [:index, :destroy] +end + +# Redirect routes till GitLab 17.0 release + +resource :profile, only: [] do + resources :active_sessions, only: [:destroy], controller: 'user_settings/active_sessions' + member do + get :active_sessions, to: redirect('-/user_settings/active_sessions') + end end diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 25a4299ab70..77503814158 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -157,8 +157,6 @@ - 1 - - ci_cancel_redundant_pipelines - 1 -- - ci_catalog_resources_process_sync_events - - 1 - - ci_delete_objects - 1 - - ci_initialize_pipelines_iid_sequence diff --git a/data/deprecations/16-7-dependency-proxy-group-deploy-token.yml b/data/deprecations/16-7-dependency-proxy-group-deploy-token.yml new file mode 100644 index 00000000000..d19159f36b8 --- /dev/null +++ b/data/deprecations/16-7-dependency-proxy-group-deploy-token.yml @@ -0,0 +1,13 @@ +- title: "Dependency Proxy: group access tokens to have additional scope checks for service accounts" + announcement_milestone: "16.7" + removal_milestone: "17.0" + breaking_change: true + reporter: trizzi + stage: Package + issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/431386 + body: | + When using the Dependency Proxy for containers with a group access token, `docker login` and `docker pull` requests with insufficient scopes for Dependency Proxy are not rejected. + + GitLab 16.7 adds checks for group access tokens authenticating for the dependency proxy for containers. This is a breaking change, because tokens without the required scopes will fail. + + To help avoid being impacted by this breaking change, create new group access tokens with the [required scopes](https://docs.gitlab.com/ee/user/packages/dependency_proxy/#authenticate-with-the-dependency-proxy), and update your workflow variables and scripts with those new tokens. diff --git a/doc/administration/audit_event_streaming/audit_event_types.md b/doc/administration/audit_event_streaming/audit_event_types.md index b1ff9640d77..28cab04553a 100644 --- a/doc/administration/audit_event_streaming/audit_event_types.md +++ b/doc/administration/audit_event_streaming/audit_event_types.md @@ -180,6 +180,9 @@ Audit event types belong to the following product categories. | [`ci_group_variable_created`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91983) | Triggered when a CI variable is created at a group level| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.2](https://gitlab.com/gitlab-org/gitlab/-/issues/363090) | | [`ci_group_variable_deleted`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91983) | Triggered when a group's CI variable is deleted| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.2](https://gitlab.com/gitlab-org/gitlab/-/issues/363090) | | [`ci_group_variable_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91983) | Triggered when a group's CI variable is updated| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.2](https://gitlab.com/gitlab-org/gitlab/-/issues/363090) | +| [`ci_instance_variable_created`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131882) | When an instance level CI variable is created| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.5](https://gitlab.com/gitlab-org/gitlab/-/issues/8070) | +| [`ci_instance_variable_deleted`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131882) | When an instance level CI varialbe is deleted| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.5](https://gitlab.com/gitlab-org/gitlab/-/issues/8070) | +| [`ci_instance_variable_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131882) | When an instance level CI variable is changed| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.5](https://gitlab.com/gitlab-org/gitlab/-/issues/8070) | | [`ci_variable_created`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91983) | Triggered when a CI variable is created at a project level| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.2](https://gitlab.com/gitlab-org/gitlab/-/issues/363090) | | [`ci_variable_deleted`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91983) | Triggered when a project's CI variable is deleted| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.2](https://gitlab.com/gitlab-org/gitlab/-/issues/363090) | | [`ci_variable_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91983) | Triggered when a project's CI variable is updated| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.2](https://gitlab.com/gitlab-org/gitlab/-/issues/363090) | diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md index d450ee1ec6d..e88d60aebe7 100644 --- a/doc/administration/pages/index.md +++ b/doc/administration/pages/index.md @@ -93,6 +93,42 @@ Where `example.io` is the domain GitLab Pages is served from, `192.0.2.1` is the IPv4 address of your GitLab instance, and `2001:db8::1` is the IPv6 address. If you don't have IPv6, you can omit the `AAAA` record. +#### For namespace in URL path, without wildcard DNS **(EXPERIMENT)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/17584) in GitLab 16.7. This feature is an [Experiment](../../policy/experiment-beta-support.md). + +FLAG: +On self-managed GitLab, by default this feature is available. +On GitLab.com, this feature is not available. +This feature is not ready for production use. + +Prerequisites: + +- Your instance must use the Linux package installation method. + +If you need support for namespace in the URL path to remove the requirement for wildcard DNS: + +1. Enable the GitLab Pages flag for this feature by adding + `gitlab_pages["namespace_in_path"] = true` to `gitlab.rb`. For more information, + see [Use environment variables](#use-environment-variables). +1. In your DNS provider, add entries for `example.com` and `projects.example.com`. + In both lines, replace `example.com` with your domain name, and `192.0.0.0` with + the IPv4 version of your IP address. The entries look like this: + + ```plaintext + example.com 1800 IN A 192.0.0.0 + projects.example.com 1800 IN A 192.0.0.0 + ``` + +1. Optional. If your GitLab instance has an IPv6 address, add entries for it. + In both lines, replace `example.com` with your domain name, and `2001:db8::1` with + the IPv6 version of your IP address. The entries look like this: + + ```plaintext + example.com 1800 IN AAAA 2001:db8::1 + projects.example.com 1800 IN AAAA 2001:db8::1 + ``` + #### DNS configuration for custom domains If support for custom domains is needed, all subdomains of the Pages root domain should point to the @@ -149,6 +185,47 @@ The Pages daemon doesn't listen to the outside world. Watch the [video tutorial](https://youtu.be/dD8c7WNcc6s) for this configuration. +### Pages domain without wildcard DNS **(EXPERIMENT)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/17584) in GitLab 16.7. This feature is an [Experiment](../../policy/experiment-beta-support.md). + +FLAG: +On self-managed GitLab, by default this feature is available. +On GitLab.com, this feature is not available. +This feature is not ready for production use. + +This configuration is the minimum setup for GitLab Pages. It is the base for all +other configurations. In this configuration, NGINX proxies all requests to the daemon, +because the GitLab Pages daemon doesn't listen to the outside world. + +Prerequisites: + +- Your instance must use the Linux package installation method. +- You have configured DNS setup + [without a wildcard](#for-namespace-in-url-path-without-wildcard-dns). + +1. In `/etc/gitlab/gitlab.rb`, set the external URL for GitLab Pages, and enable + the feature flag: + + ```ruby + # External_url here is only for reference + external_url "http://example.com" + pages_external_url 'http://example.io' + + pages_nginx['enable'] = true + + # Set this feature flag to enable this feature + # For more information, see https://docs.gitlab.com/ee/administration/pages/index.html#use-environment-variables + gitlab_pages["namespace_in_path"] = true + ``` + +1. [Reconfigure GitLab](../restart_gitlab.md#reconfigure-a-linux-package-installation). + +NGINX uses the custom proxy header `X-Gitlab-Namespace-In-Path` +to send the namespace to the GitLab Pages daemon. + +The resulting URL scheme is `http://example.io/<namespace>/<project_slug>`. + ### Wildcard domains with TLS support **Requirements:** @@ -163,7 +240,7 @@ URL scheme: `https://<namespace>.example.io/<project_slug>` NGINX proxies all requests to the daemon. Pages daemon doesn't listen to the outside world. -1. Place the wildcard LTS certificate for `*.example.io` and the key inside `/etc/gitlab/ssl`. +1. Place the wildcard TLS certificate for `*.example.io` and the key inside `/etc/gitlab/ssl`. 1. In `/etc/gitlab/gitlab.rb` specify the following configuration: ```ruby @@ -195,6 +272,69 @@ Before you reconfigure, remove the `gitlab_pages` section from `/etc/gitlab/gitl then run `gitlab-ctl reconfigure`. For more information, read [GitLab Pages does not regenerate OAuth](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/3947). +### Pages domain with TLS support, without wildcard DNS **(EXPERIMENT)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/17584) in GitLab 16.7. This feature is an [Experiment](../../policy/experiment-beta-support.md). + +FLAG: +On self-managed GitLab, by default this feature is available. +On GitLab.com, this feature is not available. +This feature is not ready for production use. + +Prerequisites: + +- Your instance must use the Linux package installation method. +- You have configured DNS setup + [without a wildcard](#for-namespace-in-url-path-without-wildcard-dns). +- You have a single TLS certificate that covers your domain (like `example.com`) + and the `projects.*` version of your domain, like `projects.example.com`. + +In this configuration, NGINX proxies all requests to the daemon. The GitLab Pages +daemon doesn't listen to the outside world: + +1. Add your TLS certificate and key as mentioned in the prerequisites into `/etc/gitlab/ssl`. +1. In `/etc/gitlab/gitlab.rb`, set the external URL for GitLab Pages, and enable + the feature flag: + + ```ruby + # The external_url field is here only for reference. + external_url "https://example.com" + pages_external_url 'https://example.io' + + pages_nginx['enable'] = true + pages_nginx['redirect_http_to_https'] = true + + # Set this feature flag to enable this feature + # For more information, see https://docs.gitlab.com/ee/administration/pages/index.html#use-environment-variables + gitlab_pages["namespace_in_path"] = true + ``` + +1. If your TLS certificate and key don't match the name of your domain, like + `example.io.crt` and `example.io.key`, + add the full paths for the certificate and key files to `/etc/gitlab/gitlab.rb`: + + ```ruby + pages_nginx['ssl_certificate'] = "/etc/gitlab/ssl/pages-nginx.crt" + pages_nginx['ssl_certificate_key'] = "/etc/gitlab/ssl/pages-nginx.key" + ``` + +1. [Reconfigure GitLab](../restart_gitlab.md#reconfigure-a-linux-package-installation). + + WARNING: + GitLab Pages does not update the OAuth application if changes are made to the redirect URI. + Before you reconfigure, remove the `gitlab_pages` section from `/etc/gitlab/gitlab-secrets.json`, + then run `gitlab-ctl reconfigure`. For more information, see + [GitLab Pages does not regenerate OAuth](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/3947). + +1. If you're using [Pages Access Control](#access-control), update the redirect URI in the GitLab Pages + [System OAuth application](../../integration/oauth_provider.md#create-an-instance-wide-application) + to use the HTTPS protocol. + +NGINX uses the custom proxy header `X-Gitlab-Namespace-In-Path` +to send the namespace to the GitLab Pages daemon. + +The resulting URL scheme is `https://example.io/<namespace>/<project_slug>`. + ### Wildcard domains with TLS-terminating Load Balancer **Requirements:** @@ -269,6 +409,7 @@ control over how the Pages daemon runs and serves content in your environment. | `log_directory` | Absolute path to a log directory. | | `log_format` | The log output format: `text` or `json`. | | `log_verbose` | Verbose logging, true/false. | +| `namespace_in_path` | (Experimental) Enable or disable namespace in the URL path. This requires `pages_nginx[enable] = true`. Sets `rewrite` configuration in NGINX to support [without wildcard DNS setup](#for-namespace-in-url-path-without-wildcard-dns). Default: `false` | | `propagate_correlation_id` | Set to true (false by default) to re-use existing Correlation ID from the incoming request header `X-Request-ID` if present. If a reverse proxy sets this header, the value is propagated in the request chain. | | `max_connections` | Limit on the number of concurrent connections to the HTTP, HTTPS or proxy listeners. | | `max_uri_length` | The maximum length of URIs accepted by GitLab Pages. Set to 0 for unlimited length. [Introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/5729) in GitLab 14.5. diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index b0a6e52e838..dfa933fb006 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -1945,6 +1945,31 @@ Input type: `BoardListUpdateLimitMetricsInput` | <a id="mutationboardlistupdatelimitmetricserrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | <a id="mutationboardlistupdatelimitmetricslist"></a>`list` | [`BoardList`](#boardlist) | Updated list. | +### `Mutation.branchRuleUpdate` + +WARNING: +**Introduced** in 16.7. +This feature is an Experiment. It can be changed or removed at any time. + +Input type: `BranchRuleUpdateInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="mutationbranchruleupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationbranchruleupdateid"></a>`id` | [`ProtectedBranchID!`](#protectedbranchid) | Global ID of the protected branch. | +| <a id="mutationbranchruleupdatename"></a>`name` | [`String!`](#string) | Branch name, with wildcards, for the branch rules. | +| <a id="mutationbranchruleupdateprojectpath"></a>`projectPath` | [`ID!`](#id) | Full path to the project that the branch is associated with. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="mutationbranchruleupdatebranchrule"></a>`branchRule` | [`BranchRule`](#branchrule) | Branch rule after mutation. | +| <a id="mutationbranchruleupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationbranchruleupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | + ### `Mutation.buildForecast` WARNING: @@ -32484,6 +32509,12 @@ A `ProjectImportStateID` is a global ID. It is encoded as a string. An example `ProjectImportStateID` is: `"gid://gitlab/ProjectImportState/1"`. +### `ProtectedBranchID` + +A `ProtectedBranchID` is a global ID. It is encoded as a string. + +An example `ProtectedBranchID` is: `"gid://gitlab/ProtectedBranch/1"`. + ### `ReleaseID` A `ReleaseID` is a global ID. It is encoded as a string. diff --git a/doc/ci/jobs/ci_job_token.md b/doc/ci/jobs/ci_job_token.md index 77bf10c5dc2..ba2d63c12e4 100644 --- a/doc/ci/jobs/ci_job_token.md +++ b/doc/ci/jobs/ci_job_token.md @@ -124,8 +124,8 @@ Prerequisites: - You must have the Maintainer role for the project. -1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project. -1. On the left sidebar, select **Settings > General**. +1. On the left sidebar, select **Search or go to** and find your project. +1. Select **Settings > General**. 1. Expand **Visibility, project features, permissions**. 1. Set the visibility to **Only project members** for the features you want to restrict access to. - The ability to fetch artifacts is controlled by the CI/CD visibility setting. diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md index aa6c89da5b9..66b5d5be59a 100644 --- a/doc/update/deprecations.md +++ b/doc/update/deprecations.md @@ -315,6 +315,24 @@ In 16.3, the names of these settings were changed to clarify their meanings: the </div> +<div class="deprecation breaking-change" data-milestone="17.0"> + +### Dependency Proxy: group access tokens to have additional scope checks for service accounts + +<div class="deprecation-notes"> +- Announced in GitLab <span class="milestone">16.7</span> +- Removal in GitLab <span class="milestone">17.0</span> ([breaking change](https://docs.gitlab.com/ee/update/terminology.html#breaking-change)) +- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/431386). +</div> + +When using the Dependency Proxy for containers with a group access token, `docker login` and `docker pull` requests with insufficient scopes for Dependency Proxy are not rejected. + +GitLab 16.7 adds checks for group access tokens authenticating for the dependency proxy for containers. This is a breaking change, because tokens without the required scopes will fail. + +To help avoid being impacted by this breaking change, create new group access tokens with the [required scopes](https://docs.gitlab.com/ee/user/packages/dependency_proxy/#authenticate-with-the-dependency-proxy), and update your workflow variables and scripts with those new tokens. + +</div> + <div class="deprecation " data-milestone="17.0"> ### Deprecate GraphQL fields related to the temporary storage increase diff --git a/doc/user/application_security/dependency_list/index.md b/doc/user/application_security/dependency_list/index.md index 34f7bd9ca63..d263fcde77f 100644 --- a/doc/user/application_security/dependency_list/index.md +++ b/doc/user/application_security/dependency_list/index.md @@ -37,7 +37,7 @@ To view your project's dependencies, ensure you meet the following requirements: To view the dependencies of a project or all projects in a group: -1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project or group. +1. On the left sidebar, select **Search or go to** and find your project or group. 1. Select **Secure > Dependency list**. Details of each dependency are listed, sorted by decreasing severity of vulnerabilities (if any). You can sort the list instead by component name or packager. @@ -98,6 +98,6 @@ list shows only the results of the last successful pipeline that ran on the defa To download the dependency list: -1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project or group. +1. On the left sidebar, select **Search or go to** and find your project or group. 1. Select **Secure > Dependency list**. 1. Select **Export**. diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md index e2d2f090524..896a00af3b4 100644 --- a/doc/user/application_security/dependency_scanning/index.md +++ b/doc/user/application_security/dependency_scanning/index.md @@ -710,7 +710,7 @@ your GitLab CI/CD configuration file is complex. To enable dependency scanning: -1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project. +1. On the left sidebar, select **Search or go to** and find your project. 1. Select **Build > Pipeline editor**. 1. Copy and paste the following to the bottom of the `.gitlab-ci.yml` file: diff --git a/doc/user/compliance/compliance_center/index.md b/doc/user/compliance/compliance_center/index.md index 42e9296b11a..f745259c763 100644 --- a/doc/user/compliance/compliance_center/index.md +++ b/doc/user/compliance/compliance_center/index.md @@ -31,7 +31,7 @@ Prerequisites: To view the standards adherence dashboard for a group: 1. On the left sidebar, select **Search or go to** and find your group. -1. On the left sidebar, select **Secure > Compliance center**. +1. Select **Secure > Compliance center**. ### GitLab standard @@ -99,7 +99,7 @@ Prerequisites: To view the compliance violations report: 1. On the left sidebar, select **Search or go to** and find your group. -1. On the left sidebar, select **Secure > Compliance center**. +1. Select **Secure > Compliance center**. You can sort the compliance report on: @@ -186,7 +186,7 @@ Prerequisites: To export a report of merge request compliance violations for projects in a group: 1. On the left sidebar, select **Search or go to** and find your group. -1. On the left sidebar, select **Secure > Compliance center**. +1. Select **Secure > Compliance center**. 1. In the top-right corner, select **Export**. 1. Select **Export violations report**. @@ -233,7 +233,7 @@ If the commit has a related merge commit, then the following are also included: To generate the Chain of Custody report: 1. On the left sidebar, select **Search or go to** and find your group. -1. On the left sidebar, select **Secure > Compliance center**. +1. Select **Secure > Compliance center**. 1. In the top-right corner, select **Export**. 1. Select **Export chain of custody report**. @@ -250,7 +250,7 @@ details for the provided commit SHA. To generate a commit-specific Chain of Custody report: 1. On the left sidebar, select **Search or go to** and find your group. -1. On the left sidebar, select **Secure > Compliance center**. +1. Select **Secure > Compliance center**. 1. In the top-right corner, select **Export**. 1. Select **Export custody report of a specific commit**. 1. Enter the commit SHA, and then select **Export custody report**. @@ -282,7 +282,7 @@ Prerequisites: To view the compliance projects report: 1. On the left sidebar, select **Search or go to** and find your group. -1. On the left sidebar, select **Secure > Compliance center**. +1. Select **Secure > Compliance center**. 1. On the page, select the **Projects** tab. ### Apply a compliance framework to projects in a group @@ -299,7 +299,7 @@ Prerequisites: To apply a compliance framework to one project in a group: 1. On the left sidebar, select **Search or go to** and find your group. -1. On the left sidebar, select **Secure > Compliance center**. +1. Select **Secure > Compliance center**. 1. On the page, select the **Projects** tab. 1. Next to the project you want to add the compliance framework to, select **{plus}** **Add framework**. 1. Select an existing compliance framework or create a new one. @@ -307,7 +307,7 @@ To apply a compliance framework to one project in a group: To apply a compliance framework to multiple projects in a group: 1. On the left sidebar, select **Search or go to** and find your group. -1. On the left sidebar, select **Secure > Compliance center**. +1. Select **Secure > Compliance center**. 1. On the page, select the **Projects** tab. 1. Select multiple projects. 1. From the **Choose one bulk action** dropdown list, select **Apply framework to selected projects**. @@ -328,14 +328,14 @@ Prerequisites: To remove a compliance framework from one project in a group: 1. On the left sidebar, select **Search or go to** and find your group. -1. On the left sidebar, select **Secure > Compliance center**. +1. Select **Secure > Compliance center**. 1. On the page, select the **Projects** tab. 1. Next to the compliance framework to remove from the project, select **{close}** on the framework label. To remove a compliance framework from multiple projects in a group: 1. On the left sidebar, select **Search or go to** and find your group. -1. On the left sidebar, select **Secure > Compliance center**. +1. Select **Secure > Compliance center**. 1. On the page, select the **Projects** tab. 1. Select multiple projects. 1. From the **Choose one bulk action** dropdown list, select **Remove framework from selected projects**. @@ -357,7 +357,7 @@ Prerequisites: To export a report of compliance frameworks on projects in a group: 1. On the left sidebar, select **Search or go to** and find your group. -1. On the left sidebar, select **Secure > Compliance center**. +1. Select **Secure > Compliance center**. 1. In the top-right corner, select **Export**. 1. Select **Export list of project frameworks**. @@ -370,13 +370,13 @@ A report is compiled and delivered to your email inbox as an attachment. To filter the list of compliance frameworks: 1. On the left sidebar, select **Search or go to** and find your group. -1. On the left sidebar, select **Secure > Compliance center**. +1. Select **Secure > Compliance center**. 1. On the page, select the **Projects** tab. 1. In the search field: 1. Select the attribute you want to filter by. 1. Select an operator. 1. Select from the list of options or enter text for the search. -1. Select **Search** (**{search}**). +1. Select **Search**. Repeat this process to filter by multiple attributes. @@ -405,5 +405,5 @@ Prerequisites: To view the compliance projects report: 1. On the left sidebar, select **Search or go to** and find your group. -1. On the left sidebar, select **Secure > Compliance center**. +1. Select **Secure > Compliance center**. 1. On the page, select the **Frameworks** tab. diff --git a/doc/user/project/repository/signed_commits/index.md b/doc/user/project/repository/signed_commits/index.md index c4abcf34490..297ae31048e 100644 --- a/doc/user/project/repository/signed_commits/index.md +++ b/doc/user/project/repository/signed_commits/index.md @@ -22,15 +22,14 @@ Sign commits with your: ## Verify commits You can review commits for a merge request, or for an entire project, to confirm -they are signed: - -1. To review commits for a project: - 1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project. - 1. Select **Code > Commits**. -1. To review commits for a merge request: - 1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project. - 1. On the left sidebar, select **Merge requests**, then select your merge request. - 1. Select **Commits**. +they are signed. + +1. On the left sidebar, select **Search or go to** and find your project. +1. To review commits: + - For a project, select **Code > Commits**. + - For a merge request: + 1. Select **Merge requests**, then select your merge request. + 1. Select **Commits**. 1. Identify the commit you want to review. Signed commits show either a **Verified** or **Unverified** badge, depending on the verification status of the signature. Unsigned commits do not display a badge: diff --git a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml index 3f8bcbbc635..111df0af67a 100644 --- a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml @@ -1,5 +1,5 @@ variables: - AUTO_BUILD_IMAGE_VERSION: 'v1.50.0' + AUTO_BUILD_IMAGE_VERSION: 'v1.51.0' build: stage: build diff --git a/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml index 3f8bcbbc635..111df0af67a 100644 --- a/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml @@ -1,5 +1,5 @@ variables: - AUTO_BUILD_IMAGE_VERSION: 'v1.50.0' + AUTO_BUILD_IMAGE_VERSION: 'v1.51.0' build: stage: build diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb index 3744c81f51d..aa59caa4268 100644 --- a/lib/gitlab/git/blob.rb +++ b/lib/gitlab/git/blob.rb @@ -149,7 +149,7 @@ module Gitlab return if @data == '' # don't mess with submodule blobs # Even if we return early, recalculate whether this blob is binary in - # case a blob was initialized as text but the full data isn't + # case a blob was initialized as text but the full data isn'tspec/requests/api/graphql/mutations/branch_rules/update_spec.rb: @binary = nil return if @loaded_all_data diff --git a/lib/gitlab/nav/top_nav_menu_item.rb b/lib/gitlab/nav/top_nav_menu_item.rb index e7790fd77d0..f6fea97dae9 100644 --- a/lib/gitlab/nav/top_nav_menu_item.rb +++ b/lib/gitlab/nav/top_nav_menu_item.rb @@ -21,7 +21,7 @@ module Gitlab href: href, view: view.to_s, css_class: css_class, - data: data || { testid: 'menu_item_link', qa_title: title }, + data: data || { testid: 'menu-item-link', qa_title: title }, partial: partial, component: component } diff --git a/lib/gitlab/web_ide/default_oauth_application.rb b/lib/gitlab/web_ide/default_oauth_application.rb new file mode 100644 index 00000000000..01b7637c1c0 --- /dev/null +++ b/lib/gitlab/web_ide/default_oauth_application.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module Gitlab + module WebIde + module DefaultOauthApplication + class << self + def feature_enabled?(current_user) + Feature.enabled?(:vscode_web_ide, current_user) && Feature.enabled?(:web_ide_oauth, current_user) + end + + def oauth_application + application_settings.web_ide_oauth_application + end + + def oauth_callback_url + Gitlab::Routing.url_helpers.ide_oauth_redirect_url + end + + def ensure_oauth_application! + return if oauth_application + + should_expire_cache = false + + application_settings.transaction do + # note: This should run very rarely and should be safe for us to do a lock + # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132496#note_1587293087 + application_settings.lock! + + # note: `lock!`` breaks applicaiton_settings cache and will trigger another query. + # We need to double check here so that requests previously waiting on the lock can + # now just skip. + next if oauth_application + + application = Doorkeeper::Application.new( + name: 'GitLab Web IDE', + redirect_uri: oauth_callback_url, + scopes: ['api'], + trusted: true, + confidential: false) + application.save! + application_settings.update!(web_ide_oauth_application: application) + should_expire_cache = true + end + + # note: This needs to happen outside the transaction, but only if we actually changed something + ::Gitlab::CurrentSettings.expire_current_application_settings if should_expire_cache + end + + private + + def application_settings + ::Gitlab::CurrentSettings.current_application_settings + end + end + end + end +end diff --git a/lib/sidebars/user_settings/menus/active_sessions_menu.rb b/lib/sidebars/user_settings/menus/active_sessions_menu.rb index f806c04e77c..3ddf8700556 100644 --- a/lib/sidebars/user_settings/menus/active_sessions_menu.rb +++ b/lib/sidebars/user_settings/menus/active_sessions_menu.rb @@ -8,7 +8,7 @@ module Sidebars override :link def link - profile_active_sessions_path + user_settings_active_sessions_path end override :title diff --git a/locale/gitlab.pot b/locale/gitlab.pot index c16aaa3e9b8..172b3642dd7 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -4427,6 +4427,9 @@ msgstr "" msgid "After the export is complete, download the data file from a notification email or from this page. You can then import the data file from the %{strong_text_start}Create new group%{strong_text_end} page of another GitLab instance." msgstr "" +msgid "After the report is generated, an email will be sent with the report attached." +msgstr "" + msgid "After you enable the integration, the following protected variable is created for CI/CD use:" msgstr "" @@ -5030,9 +5033,6 @@ msgstr "" msgid "An email notification was recently sent from the admin panel. Please wait %{wait_time_in_words} before attempting to send another message." msgstr "" -msgid "An email will be sent with the report attached after it is generated." -msgstr "" - msgid "An empty GitLab User field will add the FogBugz user's full name (e.g. \"By John Smith\") in the description of all issues and comments. It will also associate and/or assign these issues and comments with the project creator." msgstr "" @@ -7130,6 +7130,9 @@ msgstr "" msgid "Authenticated web requests" msgstr "" +msgid "Authenticating..." +msgstr "" + msgid "Authentication" msgstr "" @@ -11990,6 +11993,9 @@ msgstr "" msgid "CodeSuggestions|Subject to the %{terms_link_start}Testing Terms of Use%{link_end}. Code Suggestions uses third-party AI services." msgstr "" +msgid "CodeSuggestions|Subject to the %{terms_link_start}Testing Terms of Use%{link_end}. Code Suggestions uses third-party AI services.|To start receiving Code Suggestions after enabling this feature, you must install and configure a %{ide_extension_link_start}supported IDE editor extension%{link_end}." +msgstr "" + msgid "CodeownersValidation|An error occurred while loading the validation errors. Please try again later." msgstr "" @@ -12438,16 +12444,16 @@ msgstr "" msgid "Completed in %{duration_seconds} seconds (%{relative_time})" msgstr "" -msgid "Compliance Center Export|(limited to 15 MB)" +msgid "Compliance Center Export|Example: 2dc6aa3" msgstr "" -msgid "Compliance Center Export|Example: 2dc6aa3" +msgid "Compliance Center Export|Export chain of custody report" msgstr "" -msgid "Compliance Center Export|Export as CSV" +msgid "Compliance Center Export|Export chain of custody report as a CSV file (limited to 15MB)." msgstr "" -msgid "Compliance Center Export|Export chain of custody report" +msgid "Compliance Center Export|Export chain of custody report of a specific commit as a CSV file (limited to 15MB)." msgstr "" msgid "Compliance Center Export|Export custody report" @@ -12459,6 +12465,12 @@ msgstr "" msgid "Compliance Center Export|Export list of project frameworks" msgstr "" +msgid "Compliance Center Export|Export list of project frameworks as a CSV file." +msgstr "" + +msgid "Compliance Center Export|Export merge request violations as a CSV file." +msgstr "" + msgid "Compliance Center Export|Export violations report" msgstr "" @@ -12468,6 +12480,9 @@ msgstr "" msgid "Compliance Center Export|Send email of the chosen report as CSV" msgstr "" +msgid "Compliance Center Export|You will be emailed after the export is processed." +msgstr "" + msgid "Compliance Center|Frameworks" msgstr "" @@ -12483,6 +12498,9 @@ msgstr "" msgid "Compliance framework" msgstr "" +msgid "ComplianceChainOfCustody| Chain of custody export" +msgstr "" + msgid "ComplianceFrameworksReport|Associated Projects" msgstr "" @@ -12495,6 +12513,9 @@ msgstr "" msgid "ComplianceFrameworksReport|Edit framework" msgstr "" +msgid "ComplianceFrameworks| Frameworks export" +msgstr "" + msgid "ComplianceFrameworks|Active compliance frameworks" msgstr "" @@ -12507,9 +12528,6 @@ msgstr "" msgid "ComplianceFrameworks|Cancel" msgstr "" -msgid "ComplianceFrameworks|Compliance Frameworks Export" -msgstr "" - msgid "ComplianceFrameworks|Compliance framework created" msgstr "" @@ -12798,7 +12816,7 @@ msgstr "" msgid "ComplianceStandardsAdherence|View details (fix available)" msgstr "" -msgid "ComplianceViolations|Compliance Violations Export" +msgid "ComplianceViolations| Violations export" msgstr "" msgid "ComplianceViolations|Your Compliance Violations CSV export for the group \"%{group_name}\" has been attached to this email." @@ -18942,6 +18960,9 @@ msgstr "" msgid "Environments|Open live environment" msgstr "" +msgid "Environments|Or select namespace: %{searchTerm}" +msgstr "" + msgid "Environments|Re-deploy environment" msgstr "" @@ -19101,10 +19122,10 @@ msgstr "" msgid "Environment|Unauthorized to access %{resourceType} from this environment." msgstr "" -msgid "Environment|Unauthorized to access the cluster agent from this environment. Check your authentication and try again." +msgid "Environment|Unhealthy" msgstr "" -msgid "Environment|Unhealthy" +msgid "Environment|You don't have permission to view all the namespaces in the cluster. If a namespace is not shown, you can still enter its name to select it." msgstr "" msgid "Epic" diff --git a/qa/qa/page/component/project_selector.rb b/qa/qa/page/component/project_selector.rb index 54bd95c5422..6a6c42a1afa 100644 --- a/qa/qa/page/component/project_selector.rb +++ b/qa/qa/page/component/project_selector.rb @@ -10,20 +10,20 @@ module QA super base.view 'app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue' do - element :project_search_field - element :project_list_item + element 'project-search-field' + element 'project-list-item' end end def fill_project_search_input(project_name) - fill_element :project_search_field, project_name + fill_element 'project-search-field', project_name end def select_project wait_until(sleep_interval: 2, reload: false) do - has_element? :project_list_item + has_element? 'project-list-item' end - click_element :project_list_item + click_element 'project-list-item' end end end diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb index 6890f490617..a1bb1475a0c 100644 --- a/qa/qa/page/main/menu.rb +++ b/qa/qa/page/main/menu.rb @@ -10,7 +10,7 @@ module QA include SubMenus::SuperSidebar::GlobalSearchModal view 'app/assets/javascripts/super_sidebar/components/super_sidebar.vue' do - element :navbar, required: true # TODO: rename to sidebar once it's default implementation + element 'super-sidebar', required: true end view 'app/assets/javascripts/super_sidebar/components/user_bar.vue' do @@ -20,7 +20,7 @@ module QA view 'app/assets/javascripts/super_sidebar/components/user_menu.vue' do element 'user-dropdown', required: !Runtime::Env.phone_layout? element 'user-avatar-content', required: !Runtime::Env.phone_layout? - element :sign_out_link + element 'sign-out-link' element 'edit-profile-link' end @@ -35,12 +35,8 @@ module QA element 'todos-shortcut-button', required: !Runtime::Env.phone_layout? end - view 'app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue' do - element 'global-search-input' - end - view 'lib/gitlab/nav/top_nav_menu_item.rb' do - element :menu_item_link + element 'menu-item-link' end view 'app/views/layouts/header/_new_dropdown.html.haml' do @@ -48,9 +44,9 @@ module QA end view 'app/helpers/nav/new_dropdown_helper.rb' do - element :global_new_group_link - element :global_new_project_link - element :global_new_snippet_link + element 'global-new-group-link' + element 'global-new-project-link' + element 'global-new-snippet-link' end def go_to_projects @@ -112,7 +108,7 @@ module QA break true unless signed_in? within_user_menu do - click_element :sign_out_link + click_element 'sign-out-link' end not_signed_in? @@ -164,7 +160,7 @@ module QA private def within_user_menu(&block) - within_element(:navbar) do + within_element('super-sidebar') do click_element 'user-avatar-content' unless has_element?('user-profile-link', wait: 1) within_element('user-dropdown', &block) diff --git a/qa/qa/page/sub_menus/common.rb b/qa/qa/page/sub_menus/common.rb index bfe6b30eff6..5196957c0b3 100644 --- a/qa/qa/page/sub_menus/common.rb +++ b/qa/qa/page/sub_menus/common.rb @@ -9,7 +9,20 @@ module QA base.class_eval do view 'app/assets/javascripts/super_sidebar/components/super_sidebar.vue' do - element :navbar + element 'super-sidebar' + end + + view 'app/assets/javascripts/super_sidebar/components/menu_section.vue' do + element 'menu-section-button' + element 'menu-section' + end + + view 'app/assets/javascripts/super_sidebar/components/nav_item.vue' do + element 'nav-item-link' + end + + view 'app/views/layouts/header/_new_dropdown.html.haml' do + element 'new-menu-toggle' end end end @@ -28,16 +41,15 @@ module QA # Open sidebar navigation submenu # # @param [String] parent_menu_name - # @param [String] parent_section_id # @param [String] sub_menu # @return [void] def open_submenu(parent_menu_name, sub_menu) # prevent closing sub-menu if it was already open - unless has_element?(:menu_section, section_name: parent_menu_name, wait: 0) - click_element(:menu_section_button, section_name: parent_menu_name) + unless has_element?('menu-section', section_name: parent_menu_name, wait: 0) + click_element('menu-section-button', section_name: parent_menu_name) end - within_element(:menu_section, section_name: parent_menu_name) do + within_element('menu-section', section_name: parent_menu_name) do click_element('nav-item-link', submenu_item: sub_menu) end end diff --git a/spec/controllers/profiles/active_sessions_controller_spec.rb b/spec/controllers/user_settings/active_sessions_controller_spec.rb index 12cf4f982e9..01c1095fef5 100644 --- a/spec/controllers/profiles/active_sessions_controller_spec.rb +++ b/spec/controllers/user_settings/active_sessions_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Profiles::ActiveSessionsController do +RSpec.describe UserSettings::ActiveSessionsController, feature_category: :system_access do describe 'DELETE destroy' do let_it_be(:user) { create(:user) } diff --git a/spec/features/environments/environments_folder_spec.rb b/spec/features/environments/environments_folder_spec.rb index 6b0306a70a8..9e2932e315f 100644 --- a/spec/features/environments/environments_folder_spec.rb +++ b/spec/features/environments/environments_folder_spec.rb @@ -8,11 +8,43 @@ RSpec.describe 'Environments Folder page', :js, feature_category: :environment_m let_it_be(:project) { create(:project) } let_it_be(:user) { create(:user) } let!(:envs) { create_list(:environment, 4, :with_folders, project: project, folder: folder_name) } + let!(:stopped_env) { create(:environment, :stopped, :with_folders, project: project, folder: folder_name) } def get_env_name(environment) environment.name.split('/').last end + def find_env_element(environment) + find_by_id(environment.name) + end + + def stop_environment(environment) + environment_item = find_env_element(environment) + within(environment_item) do + click_button 'Stop' + end + + within('.modal') do + click_button 'Stop environment' + end + + wait_for_requests + end + + def redeploy_environment(environment) + environment_item = find_env_element(environment) + within(environment_item) do + click_button 'More actions' + click_button 'Delete environment' + end + + within('.modal') do + click_button 'Delete environment' + end + + wait_for_requests + end + before_all do project.add_role(user, :developer) end @@ -36,6 +68,37 @@ RSpec.describe 'Environments Folder page', :js, feature_category: :environment_m expect(page).not_to have_content('production') envs.each { |env| expect(page).to have_content(get_env_name(env)) } end + + it 'shows scope tabs' do + expect(page).to have_content("Active") + expect(page).to have_content("Stopped") + end + + it 'can stop the environment' do + environment_to_stop = envs.first + + stop_environment(environment_to_stop) + + expect(page).not_to have_content(get_env_name(environment_to_stop)) + end + + describe 'stopped environments tab' do + before do + element = find('a', text: 'Stopped') + element.click + wait_for_requests + end + + it 'shows stopped environments on stopped tab' do + expect(page).to have_content(get_env_name(stopped_env)) + end + + it 'can re-start the environment' do + redeploy_environment(stopped_env) + + expect(page).not_to have_content(get_env_name(stopped_env)) + end + end end describe 'legacy folders page' do diff --git a/spec/features/profiles/active_sessions_spec.rb b/spec/features/user_settings/active_sessions_spec.rb index 2e800ae88b6..5d1d4bc6490 100644 --- a/spec/features/profiles/active_sessions_spec.rb +++ b/spec/features/user_settings/active_sessions_spec.rb @@ -55,7 +55,7 @@ RSpec.describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state, fe end using_session :session1 do - visit profile_active_sessions_path + visit user_settings_active_sessions_path expect(page).to(have_selector('ul.list-group li.list-group-item', text: 'Signed in on', count: 2)) @@ -93,7 +93,7 @@ RSpec.describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state, fe using_session :session1 do gitlab_sign_in(user) - visit profile_active_sessions_path + visit user_settings_active_sessions_path expect(page).to have_link('Revoke', count: 1) @@ -105,7 +105,7 @@ RSpec.describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state, fe end using_session :session2 do - visit profile_active_sessions_path + visit user_settings_active_sessions_path expect(page).to have_content('You need to sign in or sign up before continuing.') end diff --git a/spec/frontend/environments/environment_flux_resource_selector_spec.js b/spec/frontend/environments/environment_flux_resource_selector_spec.js index ba3375c731f..8dab8fdd96a 100644 --- a/spec/frontend/environments/environment_flux_resource_selector_spec.js +++ b/spec/frontend/environments/environment_flux_resource_selector_spec.js @@ -25,7 +25,7 @@ const DEFAULT_PROPS = { fluxResourcePath: '', }; -describe('~/environments/components/form.vue', () => { +describe('~/environments/components/flux_resource_selector.vue', () => { let wrapper; const kustomizationItem = { diff --git a/spec/frontend/environments/environment_form_spec.js b/spec/frontend/environments/environment_form_spec.js index 478ac8d6e0e..f3dfc7a72f2 100644 --- a/spec/frontend/environments/environment_form_spec.js +++ b/spec/frontend/environments/environment_form_spec.js @@ -1,11 +1,12 @@ -import { GlLoadingIcon, GlAlert } from '@gitlab/ui'; -import Vue from 'vue'; +import { GlLoadingIcon } from '@gitlab/ui'; +import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import waitForPromises from 'helpers/wait_for_promises'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import EnvironmentForm from '~/environments/components/environment_form.vue'; import getUserAuthorizedAgents from '~/environments/graphql/queries/user_authorized_agents.query.graphql'; import EnvironmentFluxResourceSelector from '~/environments/components/environment_flux_resource_selector.vue'; +import EnvironmentNamespaceSelector from '~/environments/components/environment_namespace_selector.vue'; import createMockApollo from '../__helpers__/mock_apollo_helper'; import { mockKasTunnelUrl } from './mock_data'; @@ -36,13 +37,16 @@ const configuration = { credentials: 'include', }; +const environmentWithAgentAndNamespace = { + ...DEFAULT_PROPS.environment, + clusterAgent: { id: '12', name: 'agent-2' }, + clusterAgentId: '2', + kubernetesNamespace: 'agent', +}; + describe('~/environments/components/form.vue', () => { let wrapper; - const getNamespacesQueryResult = jest - .fn() - .mockReturnValue([{ metadata: { name: 'default' } }, { metadata: { name: 'agent' } }]); - const createWrapper = (propsData = {}, options = {}) => mountExtended(EnvironmentForm, { provide: PROVIDE, @@ -53,7 +57,7 @@ describe('~/environments/components/form.vue', () => { }, }); - const createWrapperWithApollo = ({ propsData = {}, queryResult = null } = {}) => { + const createWrapperWithApollo = (propsData = {}) => { Vue.use(VueApollo); const requestHandlers = [ @@ -70,12 +74,6 @@ describe('~/environments/components/form.vue', () => { ], ]; - const mockResolvers = { - Query: { - k8sNamespaces: queryResult || getNamespacesQueryResult, - }, - }; - return mountExtended(EnvironmentForm, { provide: { ...PROVIDE, @@ -84,13 +82,12 @@ describe('~/environments/components/form.vue', () => { ...DEFAULT_PROPS, ...propsData, }, - apolloProvider: createMockApollo(requestHandlers, mockResolvers), + apolloProvider: createMockApollo(requestHandlers, []), }); }; const findAgentSelector = () => wrapper.findByTestId('agent-selector'); - const findNamespaceSelector = () => wrapper.findByTestId('namespace-selector'); - const findAlert = () => wrapper.findComponent(GlAlert); + const findNamespaceSelector = () => wrapper.findComponent(EnvironmentNamespaceSelector); const findFluxResourceSelector = () => wrapper.findComponent(EnvironmentFluxResourceSelector); const selectAgent = async () => { @@ -326,91 +323,15 @@ describe('~/environments/components/form.vue', () => { expect(findNamespaceSelector().exists()).toBe(true); }); - it('requests the kubernetes namespaces with the correct configuration', async () => { - await waitForPromises(); - - expect(getNamespacesQueryResult).toHaveBeenCalledWith( - {}, - { configuration }, - expect.anything(), - expect.anything(), - ); - }); - - it('sets the loading prop while fetching the list', async () => { - expect(findNamespaceSelector().props('loading')).toBe(true); - - await waitForPromises(); - - expect(findNamespaceSelector().props('loading')).toBe(false); - }); - - it('renders a list of available namespaces', async () => { - await waitForPromises(); - - expect(findNamespaceSelector().props('items')).toEqual([ - { text: 'default', value: 'default' }, - { text: 'agent', value: 'agent' }, - ]); - }); - - it('filters the namespaces list on user search', async () => { - await waitForPromises(); - await findNamespaceSelector().vm.$emit('search', 'default'); - - expect(findNamespaceSelector().props('items')).toEqual([ - { value: 'default', text: 'default' }, - ]); - }); - - it('updates namespace selector field with the name of selected namespace', async () => { - await waitForPromises(); - await findNamespaceSelector().vm.$emit('select', 'agent'); - - expect(findNamespaceSelector().props('toggleText')).toBe('agent'); - }); - it('emits changes to the kubernetesNamespace', async () => { await waitForPromises(); - await findNamespaceSelector().vm.$emit('select', 'agent'); + findNamespaceSelector().vm.$emit('change', 'agent'); + await nextTick(); expect(wrapper.emitted('change')[1]).toEqual([ { name: '', externalUrl: '', kubernetesNamespace: 'agent', fluxResourcePath: null }, ]); }); - - it('clears namespace selector when another agent was selected', async () => { - await waitForPromises(); - await findNamespaceSelector().vm.$emit('select', 'agent'); - - expect(findNamespaceSelector().props('toggleText')).toBe('agent'); - - await findAgentSelector().vm.$emit('select', '1'); - expect(findNamespaceSelector().props('toggleText')).toBe( - EnvironmentForm.i18n.namespaceHelpText, - ); - }); - }); - - describe('when cannot connect to the cluster', () => { - const error = new Error('Error from the cluster_client API'); - - beforeEach(async () => { - wrapper = createWrapperWithApollo({ - queryResult: jest.fn().mockRejectedValueOnce(error), - }); - - await selectAgent(); - await waitForPromises(); - }); - - it("doesn't render the namespace selector", () => { - expect(findNamespaceSelector().exists()).toBe(false); - }); - - it('renders an alert', () => { - expect(findAlert().text()).toBe('Error from the cluster_client API'); - }); }); }); @@ -431,16 +352,6 @@ describe('~/environments/components/form.vue', () => { it("doesn't render flux resource selector", () => { expect(findFluxResourceSelector().exists()).toBe(false); }); - - it('renders the flux resource selector when the namespace is selected', async () => { - await findNamespaceSelector().vm.$emit('select', 'agent'); - - expect(findFluxResourceSelector().props()).toEqual({ - namespace: 'agent', - fluxResourcePath: '', - configuration, - }); - }); }); }); @@ -451,9 +362,7 @@ describe('~/environments/components/form.vue', () => { clusterAgentId: '1', }; beforeEach(() => { - wrapper = createWrapperWithApollo({ - propsData: { environment: environmentWithAgent }, - }); + wrapper = createWrapperWithApollo({ environment: environmentWithAgent }); }); it('updates agent selector field with the name of the associated agent', () => { @@ -468,45 +377,46 @@ describe('~/environments/components/form.vue', () => { it('renders a list of available namespaces', async () => { await waitForPromises(); - expect(findNamespaceSelector().props('items')).toEqual([ - { text: 'default', value: 'default' }, - { text: 'agent', value: 'agent' }, - ]); + expect(findNamespaceSelector().exists()).toBe(true); }); }); describe('when environment has an associated kubernetes namespace', () => { - const environmentWithAgentAndNamespace = { - ...DEFAULT_PROPS.environment, - clusterAgent: { id: '1', name: 'agent-1' }, - clusterAgentId: '1', - kubernetesNamespace: 'default', - }; beforeEach(() => { - wrapper = createWrapperWithApollo({ - propsData: { environment: environmentWithAgentAndNamespace }, - }); + wrapper = createWrapperWithApollo({ environment: environmentWithAgentAndNamespace }); }); it('updates namespace selector with the name of the associated namespace', async () => { await waitForPromises(); - expect(findNamespaceSelector().props('toggleText')).toBe('default'); + expect(findNamespaceSelector().props('namespace')).toBe('agent'); + }); + + it('clears namespace selector when another agent was selected', async () => { + expect(findNamespaceSelector().props('namespace')).toBe('agent'); + + findAgentSelector().vm.$emit('select', '1'); + await nextTick(); + + expect(findNamespaceSelector().props('namespace')).toBe(null); + }); + + it('renders the flux resource selector when the namespace is selected', () => { + expect(findFluxResourceSelector().props()).toEqual({ + namespace: 'agent', + fluxResourcePath: '', + configuration, + }); }); }); describe('when environment has an associated flux resource', () => { const fluxResourcePath = 'path/to/flux/resource'; - const environmentWithAgentAndNamespace = { - ...DEFAULT_PROPS.environment, - clusterAgent: { id: '1', name: 'agent-1' }, - clusterAgentId: '1', - kubernetesNamespace: 'default', + const environmentWithFluxResource = { + ...environmentWithAgentAndNamespace, fluxResourcePath, }; beforeEach(() => { - wrapper = createWrapperWithApollo({ - propsData: { environment: environmentWithAgentAndNamespace }, - }); + wrapper = createWrapperWithApollo({ environment: environmentWithFluxResource }); }); it('provides flux resource path to the flux resource selector component', () => { diff --git a/spec/frontend/environments/environment_namespace_selector_spec.js b/spec/frontend/environments/environment_namespace_selector_spec.js new file mode 100644 index 00000000000..53e4f807751 --- /dev/null +++ b/spec/frontend/environments/environment_namespace_selector_spec.js @@ -0,0 +1,217 @@ +import { GlAlert, GlCollapsibleListbox, GlButton } from '@gitlab/ui'; +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import { shallowMount } from '@vue/test-utils'; +import waitForPromises from 'helpers/wait_for_promises'; +import EnvironmentNamespaceSelector from '~/environments/components/environment_namespace_selector.vue'; +import { stubComponent } from 'helpers/stub_component'; +import createMockApollo from '../__helpers__/mock_apollo_helper'; +import { mockKasTunnelUrl } from './mock_data'; + +const configuration = { + basePath: mockKasTunnelUrl.replace(/\/$/, ''), + headers: { + 'GitLab-Agent-Id': 2, + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + credentials: 'include', +}; + +const DEFAULT_PROPS = { + namespace: '', + configuration, +}; + +describe('~/environments/components/namespace_selector.vue', () => { + let wrapper; + + const getNamespacesQueryResult = jest + .fn() + .mockReturnValue([ + { metadata: { name: 'default' } }, + { metadata: { name: 'agent' } }, + { metadata: { name: 'test-agent' } }, + ]); + + const closeMock = jest.fn(); + + const createWrapper = ({ propsData = {}, queryResult = null } = {}) => { + Vue.use(VueApollo); + + const mockResolvers = { + Query: { + k8sNamespaces: queryResult || getNamespacesQueryResult, + }, + }; + + return shallowMount(EnvironmentNamespaceSelector, { + propsData: { + ...DEFAULT_PROPS, + ...propsData, + }, + stubs: { + GlCollapsibleListbox: stubComponent(GlCollapsibleListbox, { + template: `<div><slot name="footer"></slot></div>`, + methods: { + close: closeMock, + }, + }), + }, + apolloProvider: createMockApollo([], mockResolvers), + }); + }; + + const findNamespaceSelector = () => wrapper.findComponent(GlCollapsibleListbox); + const findAlert = () => wrapper.findComponent(GlAlert); + const findSelectButton = () => wrapper.findComponent(GlButton); + + const searchNamespace = async (searchTerm = 'test') => { + findNamespaceSelector().vm.$emit('search', searchTerm); + await nextTick(); + }; + + describe('default', () => { + beforeEach(() => { + wrapper = createWrapper(); + }); + + it('renders namespace selector', () => { + expect(findNamespaceSelector().exists()).toBe(true); + }); + + it('requests the namespaces', async () => { + await waitForPromises(); + + expect(getNamespacesQueryResult).toHaveBeenCalled(); + }); + + it('sets the loading prop while fetching the list', async () => { + expect(findNamespaceSelector().props('loading')).toBe(true); + + await waitForPromises(); + + expect(findNamespaceSelector().props('loading')).toBe(false); + }); + + it('renders a list of available namespaces', async () => { + await waitForPromises(); + + expect(findNamespaceSelector().props('items')).toMatchObject([ + { + text: 'default', + value: 'default', + }, + { + text: 'agent', + value: 'agent', + }, + { + text: 'test-agent', + value: 'test-agent', + }, + ]); + }); + + it('filters the namespaces list on user search', async () => { + await waitForPromises(); + await searchNamespace('agent'); + + expect(findNamespaceSelector().props('items')).toMatchObject([ + { + text: 'agent', + value: 'agent', + }, + { + text: 'test-agent', + value: 'test-agent', + }, + ]); + }); + + it('emits changes to the namespace', () => { + findNamespaceSelector().vm.$emit('select', 'agent'); + + expect(wrapper.emitted('change')).toEqual([['agent']]); + }); + }); + + describe('custom select button', () => { + beforeEach(async () => { + wrapper = createWrapper(); + await waitForPromises(); + }); + + it("doesn't render custom select button before searching", () => { + expect(findSelectButton().exists()).toBe(false); + }); + + it("doesn't render custom select button when the search is found in the namespaces list", async () => { + await searchNamespace('test-agent'); + expect(findSelectButton().exists()).toBe(false); + }); + + it('renders custom select button when the namespace searched for is not found in the namespaces list', async () => { + await searchNamespace(); + expect(findSelectButton().exists()).toBe(true); + }); + + it('emits custom filled namespace name to the `change` event', async () => { + await searchNamespace(); + findSelectButton().vm.$emit('click'); + + expect(wrapper.emitted('change')).toEqual([['test']]); + }); + + it('closes the listbox after the custom value for the namespace was selected', async () => { + await searchNamespace(); + findSelectButton().vm.$emit('click'); + + expect(closeMock).toHaveBeenCalled(); + }); + }); + + describe('when environment has an associated namespace', () => { + beforeEach(() => { + wrapper = createWrapper({ + propsData: { namespace: 'existing-namespace' }, + }); + }); + + it('updates namespace selector with the name of the associated namespace', () => { + expect(findNamespaceSelector().props('toggleText')).toBe('existing-namespace'); + }); + }); + + describe('on error', () => { + const error = new Error('Error from the cluster_client API'); + + beforeEach(async () => { + wrapper = createWrapper({ + queryResult: jest.fn().mockRejectedValueOnce(error), + }); + await waitForPromises(); + }); + + it('renders an alert with the error text', () => { + expect(findAlert().text()).toContain(error.message); + }); + + it('renders an empty namespace selector', () => { + expect(findNamespaceSelector().props('items')).toMatchObject([]); + }); + + it('renders custom select button when the user performs search', async () => { + await searchNamespace(); + + expect(findSelectButton().exists()).toBe(true); + }); + + it('emits custom filled namespace name to the `change` event', async () => { + await searchNamespace(); + findSelectButton().vm.$emit('click'); + + expect(wrapper.emitted('change')).toEqual([['test']]); + }); + }); +}); diff --git a/spec/frontend/environments/folder/environments_folder_app_spec.js b/spec/frontend/environments/folder/environments_folder_app_spec.js index 262e742ba5c..fbb252fb152 100644 --- a/spec/frontend/environments/folder/environments_folder_app_spec.js +++ b/spec/frontend/environments/folder/environments_folder_app_spec.js @@ -1,12 +1,21 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; -import { GlSkeletonLoader } from '@gitlab/ui'; +import { GlSkeletonLoader, GlTab } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import EnvironmentsFolderAppComponent from '~/environments/folder/environments_folder_app.vue'; import EnvironmentItem from '~/environments/components/new_environment_item.vue'; +import StopEnvironmentModal from '~/environments/components/stop_environment_modal.vue'; +import ConfirmRollbackModal from '~/environments/components/confirm_rollback_modal.vue'; +import DeleteEnvironmentModal from '~/environments/components/delete_environment_modal.vue'; +import CanaryUpdateModal from '~/environments/components/canary_update_modal.vue'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { resolvedFolder } from '../graphql/mock_data'; +import { + resolvedFolder, + resolvedEnvironment, + resolvedEnvironmentToDelete, + resolvedEnvironmentToRollback, +} from '../graphql/mock_data'; Vue.use(VueApollo); @@ -20,6 +29,11 @@ describe('EnvironmentsFolderAppComponent', () => { const mockResolvers = { Query: { folder: environmentFolderMock, + environmentToDelete: jest.fn().mockReturnValue(resolvedEnvironmentToDelete), + environmentToRollback: jest.fn().mockReturnValue(resolvedEnvironment), + environmentToChangeCanary: jest.fn().mockReturnValue(resolvedEnvironment), + environmentToStop: jest.fn().mockReturnValue(resolvedEnvironment), + weight: jest.fn().mockReturnValue(1), }, }; @@ -47,6 +61,7 @@ describe('EnvironmentsFolderAppComponent', () => { propsData: { folderName: mockFolderName, folderPath: '/gitlab-org/test-project/-/environments/folder/dev', + scope: 'active', }, }); }; @@ -54,6 +69,7 @@ describe('EnvironmentsFolderAppComponent', () => { const findHeader = () => wrapper.findByTestId('folder-name'); const findEnvironmentItems = () => wrapper.findAllComponents(EnvironmentItem); const findSkeletonLoaders = () => wrapper.findAllComponents(GlSkeletonLoader); + const findTabs = () => wrapper.findAllComponents(GlTab); it('should render a header with the folder name', () => { createWrapper(); @@ -76,5 +92,32 @@ describe('EnvironmentsFolderAppComponent', () => { const items = findEnvironmentItems(); expect(items.length).toBe(resolvedFolder.environments.length); }); + + it('should render active and stopped tabs', () => { + const tabs = findTabs(); + expect(tabs.length).toBe(2); + }); + + [ + [StopEnvironmentModal, resolvedEnvironment], + [DeleteEnvironmentModal, resolvedEnvironmentToDelete], + [ConfirmRollbackModal, resolvedEnvironmentToRollback], + ].forEach(([Component, expectedEnvironment]) => + it(`should render ${Component.name} component`, () => { + const modal = wrapper.findComponent(Component); + + expect(modal.exists()).toBe(true); + expect(modal.props().environment).toEqual(expectedEnvironment); + expect(modal.props().graphql).toBe(true); + }), + ); + + it(`should render CanaryUpdateModal component`, () => { + const modal = wrapper.findComponent(CanaryUpdateModal); + + expect(modal.exists()).toBe(true); + expect(modal.props().environment).toEqual(resolvedEnvironment); + expect(modal.props().weight).toBe(1); + }); }); }); diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js index 7f5e46f51ad..efc63a80e89 100644 --- a/spec/frontend/environments/graphql/mock_data.js +++ b/spec/frontend/environments/graphql/mock_data.js @@ -930,3 +930,153 @@ export const fluxKustomizationsMock = [ ]; export const fluxResourcePathMock = 'path/to/flux/resource'; + +export const resolvedEnvironmentToDelete = { + __typename: 'LocalEnvironment', + id: 41, + name: 'review/hello', + deletePath: '/api/v4/projects/8/environments/41', +}; + +export const resolvedEnvironmentToRollback = { + __typename: 'LocalEnvironment', + id: 41, + name: 'review/hello', + lastDeployment: { + id: 78, + iid: 24, + sha: 'f3ba6dd84f8f891373e9b869135622b954852db1', + ref: { name: 'main', refPath: '/h5bp/html5-boilerplate/-/tree/main' }, + status: 'success', + createdAt: '2022-01-07T15:47:27.415Z', + deployedAt: '2022-01-07T15:47:32.450Z', + tierInYaml: 'staging', + tag: false, + isLast: true, + user: { + id: 1, + username: 'root', + name: 'Administrator', + state: 'active', + avatarUrl: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + webUrl: 'http://gck.test:3000/root', + showStatus: false, + path: '/root', + }, + deployable: { + id: 1014, + name: 'deploy-prod', + started: '2022-01-07T15:47:31.037Z', + complete: true, + archived: false, + buildPath: '/h5bp/html5-boilerplate/-/jobs/1014', + retryPath: '/h5bp/html5-boilerplate/-/jobs/1014/retry', + playable: false, + scheduled: false, + createdAt: '2022-01-07T15:47:27.404Z', + updatedAt: '2022-01-07T15:47:32.341Z', + status: { + action: { + buttonTitle: 'Retry this job', + icon: 'retry', + method: 'post', + path: '/h5bp/html5-boilerplate/-/jobs/1014/retry', + title: 'Retry', + }, + detailsPath: '/h5bp/html5-boilerplate/-/jobs/1014', + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + group: 'success', + hasDetails: true, + icon: 'status_success', + illustration: { + image: + '/assets/illustrations/skipped-job_empty-29a8a37d8a61d1b6f68cf3484f9024e53cd6eb95e28eae3554f8011a1146bf27.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + label: 'passed', + text: 'passed', + tooltip: 'passed', + }, + }, + commit: { + id: 'f3ba6dd84f8f891373e9b869135622b954852db1', + shortId: 'f3ba6dd8', + createdAt: '2022-01-07T15:47:26.000+00:00', + parentIds: ['3213b6ac17afab99be37d5d38f38c6c8407387cc'], + title: 'Update .gitlab-ci.yml file', + message: 'Update .gitlab-ci.yml file', + authorName: 'Administrator', + authorEmail: 'admin@example.com', + authoredDate: '2022-01-07T15:47:26.000+00:00', + committerName: 'Administrator', + committerEmail: 'admin@example.com', + committedDate: '2022-01-07T15:47:26.000+00:00', + trailers: {}, + webUrl: + 'http://gck.test:3000/h5bp/html5-boilerplate/-/commit/f3ba6dd84f8f891373e9b869135622b954852db1', + author: { + avatarUrl: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + id: 1, + name: 'Administrator', + path: '/root', + showStatus: false, + state: 'active', + username: 'root', + webUrl: 'http://gck.test:3000/root', + }, + authorGravatarUrl: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + commitUrl: + 'http://gck.test:3000/h5bp/html5-boilerplate/-/commit/f3ba6dd84f8f891373e9b869135622b954852db1', + commitPath: '/h5bp/html5-boilerplate/-/commit/f3ba6dd84f8f891373e9b869135622b954852db1', + }, + manualActions: [ + { + id: 1015, + name: 'deploy-staging', + started: null, + complete: false, + archived: false, + buildPath: '/h5bp/html5-boilerplate/-/jobs/1015', + playPath: '/h5bp/html5-boilerplate/-/jobs/1015/play', + playable: true, + scheduled: false, + createdAt: '2022-01-07T15:47:27.422Z', + updatedAt: '2022-01-07T15:47:28.557Z', + status: { + icon: 'status_manual', + text: 'manual', + label: 'manual play action', + group: 'manual', + tooltip: 'manual action', + hasDetails: true, + detailsPath: '/h5bp/html5-boilerplate/-/jobs/1015', + illustration: { + image: + '/assets/illustrations/manual_action-c55aee2c5f9ebe9f72751480af8bb307be1a6f35552f344cc6d1bf979d3422f6.svg', + size: 'svg-394', + title: 'This job requires a manual action', + content: + 'This job requires manual intervention to start. Before starting this job, you can add variables below for last-minute configuration changes.', + }, + favicon: + '/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png', + action: { + icon: 'play', + title: 'Play', + path: '/h5bp/html5-boilerplate/-/jobs/1015/play', + method: 'post', + buttonTitle: 'Run job', + }, + }, + }, + ], + scheduledActions: [], + cluster: null, + }, + retryUrl: '/h5bp/html5-boilerplate/-/jobs/1014/retry', +}; diff --git a/spec/frontend/environments/graphql/resolvers/base_spec.js b/spec/frontend/environments/graphql/resolvers/base_spec.js index e01cf18c40d..244c86fa679 100644 --- a/spec/frontend/environments/graphql/resolvers/base_spec.js +++ b/spec/frontend/environments/graphql/resolvers/base_spec.js @@ -147,10 +147,10 @@ describe('~/frontend/environments/graphql/resolvers', () => { describe('stopEnvironmentREST', () => { it('should post to the stop environment path', async () => { mock.onPost(ENDPOINT).reply(HTTP_STATUS_OK); - + const cache = { evict: jest.fn() }; const client = { writeQuery: jest.fn() }; const environment = { stopPath: ENDPOINT }; - await mockResolvers.Mutation.stopEnvironmentREST(null, { environment }, { client }); + await mockResolvers.Mutation.stopEnvironmentREST(null, { environment }, { client, cache }); expect(mock.history.post).toContainEqual( expect.objectContaining({ url: ENDPOINT, method: 'post' }), @@ -161,6 +161,7 @@ describe('~/frontend/environments/graphql/resolvers', () => { variables: { environment }, data: { isEnvironmentStopping: true }, }); + expect(cache.evict).toHaveBeenCalledWith({ fieldName: 'folder' }); }); it('should set is stopping to false if stop fails', async () => { mock.onPost(ENDPOINT).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR); @@ -183,27 +184,39 @@ describe('~/frontend/environments/graphql/resolvers', () => { describe('rollbackEnvironment', () => { it('should post to the retry environment path', async () => { mock.onPost(ENDPOINT).reply(HTTP_STATUS_OK); + const cache = { evict: jest.fn() }; - await mockResolvers.Mutation.rollbackEnvironment(null, { - environment: { retryUrl: ENDPOINT }, - }); + await mockResolvers.Mutation.rollbackEnvironment( + null, + { + environment: { retryUrl: ENDPOINT }, + }, + { cache }, + ); expect(mock.history.post).toContainEqual( expect.objectContaining({ url: ENDPOINT, method: 'post' }), ); + expect(cache.evict).toHaveBeenCalledWith({ fieldName: 'folder' }); }); }); describe('deleteEnvironment', () => { it('should DELETE to the delete environment path', async () => { mock.onDelete(ENDPOINT).reply(HTTP_STATUS_OK); + const cache = { evict: jest.fn() }; - await mockResolvers.Mutation.deleteEnvironment(null, { - environment: { deletePath: ENDPOINT }, - }); + await mockResolvers.Mutation.deleteEnvironment( + null, + { + environment: { deletePath: ENDPOINT }, + }, + { cache }, + ); expect(mock.history.delete).toContainEqual( expect.objectContaining({ url: ENDPOINT, method: 'delete' }), ); + expect(cache.evict).toHaveBeenCalledWith({ fieldName: 'folder' }); }); }); describe('cancelAutoStop', () => { diff --git a/spec/frontend/ide/init_gitlab_web_ide_spec.js b/spec/frontend/ide/init_gitlab_web_ide_spec.js index 6a5bedb0bbb..d7a16bec1c3 100644 --- a/spec/frontend/ide/init_gitlab_web_ide_spec.js +++ b/spec/frontend/ide/init_gitlab_web_ide_spec.js @@ -40,6 +40,9 @@ const TEST_EDITOR_FONT_SRC_URL = 'http://gitlab.test/assets/gitlab-mono/GitLabMo const TEST_EDITOR_FONT_FORMAT = 'woff2'; const TEST_EDITOR_FONT_FAMILY = 'GitLab Mono'; +const TEST_OAUTH_CLIENT_ID = 'oauth-client-id-123abc'; +const TEST_OAUTH_CALLBACK_URL = 'https://example.com/oauth_callback'; + describe('ide/init_gitlab_web_ide', () => { let resolveConfirm; @@ -231,4 +234,29 @@ describe('ide/init_gitlab_web_ide', () => { ); }); }); + + describe('when oauth info is in dataset', () => { + beforeEach(() => { + findRootElement().dataset.clientId = TEST_OAUTH_CLIENT_ID; + findRootElement().dataset.callbackUrl = TEST_OAUTH_CALLBACK_URL; + + createSubject(); + }); + + it('calls start with element', () => { + expect(start).toHaveBeenCalledTimes(1); + expect(start).toHaveBeenCalledWith( + findRootElement(), + expect.objectContaining({ + auth: { + type: 'oauth', + clientId: TEST_OAUTH_CLIENT_ID, + callbackUrl: TEST_OAUTH_CALLBACK_URL, + protectRefreshToken: true, + }, + httpHeaders: undefined, + }), + ); + }); + }); }); diff --git a/spec/frontend/ide/lib/gitlab_web_ide/get_oauth_config_spec.js b/spec/frontend/ide/lib/gitlab_web_ide/get_oauth_config_spec.js new file mode 100644 index 00000000000..3431068937f --- /dev/null +++ b/spec/frontend/ide/lib/gitlab_web_ide/get_oauth_config_spec.js @@ -0,0 +1,16 @@ +import { getOAuthConfig } from '~/ide/lib/gitlab_web_ide/get_oauth_config'; + +describe('~/ide/lib/gitlab_web_ide/get_oauth_config', () => { + it('returns undefined if no clientId found', () => { + expect(getOAuthConfig({})).toBeUndefined(); + }); + + it('returns auth config from dataset', () => { + expect(getOAuthConfig({ clientId: 'test-clientId', callbackUrl: 'test-callbackUrl' })).toEqual({ + type: 'oauth', + clientId: 'test-clientId', + callbackUrl: 'test-callbackUrl', + protectRefreshToken: true, + }); + }); +}); diff --git a/spec/frontend/ide/mount_oauth_callback_spec.js b/spec/frontend/ide/mount_oauth_callback_spec.js new file mode 100644 index 00000000000..6ac0b4e4615 --- /dev/null +++ b/spec/frontend/ide/mount_oauth_callback_spec.js @@ -0,0 +1,53 @@ +import { oauthCallback } from '@gitlab/web-ide'; +import { TEST_HOST } from 'helpers/test_constants'; +import { mountOAuthCallback } from '~/ide/mount_oauth_callback'; + +jest.mock('@gitlab/web-ide'); + +const TEST_USERNAME = 'gandalf.the.grey'; +const TEST_GITLAB_WEB_IDE_PUBLIC_PATH = 'test/webpack/assets/gitlab-web-ide/public/path'; + +const TEST_OAUTH_CLIENT_ID = 'oauth-client-id-123abc'; +const TEST_OAUTH_CALLBACK_URL = 'https://example.com/oauth_callback'; + +describe('~/ide/mount_oauth_callback', () => { + const createRootElement = () => { + const el = document.createElement('div'); + + el.id = 'ide'; + el.dataset.clientId = TEST_OAUTH_CLIENT_ID; + el.dataset.callbackUrl = TEST_OAUTH_CALLBACK_URL; + + document.body.append(el); + }; + + beforeEach(() => { + gon.current_username = TEST_USERNAME; + process.env.GITLAB_WEB_IDE_PUBLIC_PATH = TEST_GITLAB_WEB_IDE_PUBLIC_PATH; + + createRootElement(); + }); + + afterEach(() => { + document.body.innerHTML = ''; + }); + + it('calls oauthCallback', () => { + expect(oauthCallback).not.toHaveBeenCalled(); + + mountOAuthCallback(); + + expect(oauthCallback).toHaveBeenCalledTimes(1); + expect(oauthCallback).toHaveBeenCalledWith({ + auth: { + type: 'oauth', + callbackUrl: TEST_OAUTH_CALLBACK_URL, + clientId: TEST_OAUTH_CLIENT_ID, + protectRefreshToken: true, + }, + gitlabUrl: TEST_HOST, + baseUrl: `${TEST_HOST}/${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}`, + username: TEST_USERNAME, + }); + }); +}); diff --git a/spec/helpers/nav/new_dropdown_helper_spec.rb b/spec/helpers/nav/new_dropdown_helper_spec.rb index 4252e10c922..a69ac8b3c19 100644 --- a/spec/helpers/nav/new_dropdown_helper_spec.rb +++ b/spec/helpers/nav/new_dropdown_helper_spec.rb @@ -84,7 +84,7 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do track_action: 'click_link_new_project', track_label: 'plus_menu_dropdown', track_property: 'navigation_top', - testid: 'global_new_project_link' + testid: 'global-new-project-link' } ) ) @@ -107,7 +107,7 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do track_action: 'click_link_new_group', track_label: 'plus_menu_dropdown', track_property: 'navigation_top', - testid: 'global_new_group_link' + testid: 'global-new-group-link' } ) ) @@ -130,7 +130,7 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do track_action: 'click_link_new_snippet_parent', track_label: 'plus_menu_dropdown', track_property: 'navigation_top', - testid: 'global_new_snippet_link' + testid: 'global-new-snippet-link' } ) ) diff --git a/spec/lib/gitlab/web_ide/default_oauth_application_spec.rb b/spec/lib/gitlab/web_ide/default_oauth_application_spec.rb new file mode 100644 index 00000000000..9bfdc799aec --- /dev/null +++ b/spec/lib/gitlab/web_ide/default_oauth_application_spec.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::WebIde::DefaultOauthApplication, feature_category: :web_ide do + let_it_be(:current_user) { create(:user) } + let_it_be(:oauth_application) { create(:oauth_application, owner: nil) } + + describe '#feature_enabled?' do + where(:vscode_web_ide, :web_ide_oauth, :expectation) do + [ + [ref(:current_user), false, false], + [false, ref(:current_user), false], + [ref(:current_user), ref(:current_user), true] + ] + end + + with_them do + it 'returns the expected value' do + stub_feature_flags(vscode_web_ide: vscode_web_ide, web_ide_oauth: web_ide_oauth) + + expect(described_class.feature_enabled?(current_user)).to be(expectation) + end + end + end + + describe '#oauth_application' do + it 'returns web_ide_oauth_application from application_settings' do + expect(described_class.oauth_application).to be_nil + + stub_application_setting({ web_ide_oauth_application: oauth_application }) + + expect(described_class.oauth_application).to be(oauth_application) + end + end + + describe '#oauth_callback_url' do + it 'returns route URL for oauth callback' do + expect(described_class.oauth_callback_url).to eq(Gitlab::Routing.url_helpers.ide_oauth_redirect_url) + end + end + + describe '#ensure_oauth_application!' do + it 'if web_ide_oauth_application already exists, does nothing' do + expect(application_settings).not_to receive(:lock!) + expect(::Doorkeeper::Application).not_to receive(:new) + + stub_application_setting({ web_ide_oauth_application: oauth_application }) + + described_class.ensure_oauth_application! + end + + it 'if web_ide_oauth_application created while locked, does nothing' do + expect(application_settings).to receive(:lock!) do + stub_application_setting({ web_ide_oauth_application: oauth_application }) + end + expect(::Doorkeeper::Application).not_to receive(:new) + expect(::Gitlab::CurrentSettings).not_to receive(:expire_current_application_settings) + + described_class.ensure_oauth_application! + end + + it 'creates web_ide_oauth_application' do + expect(application_settings).to receive(:transaction).and_call_original + expect(::Doorkeeper::Application).to receive(:new).and_call_original + expect(::Gitlab::CurrentSettings).to receive(:expire_current_application_settings).and_call_original + + expect(application_settings.web_ide_oauth_application).to be_nil + + described_class.ensure_oauth_application! + + result = application_settings.web_ide_oauth_application + expect(result).not_to be_nil + expect(result).to have_attributes( + name: 'GitLab Web IDE', + redirect_uri: described_class.oauth_callback_url, + scopes: ['api'], + trusted: true, + confidential: false + ) + end + end + + def application_settings + ::Gitlab::CurrentSettings.current_application_settings + end +end diff --git a/spec/lib/sidebars/user_settings/menus/active_sessions_menu_spec.rb b/spec/lib/sidebars/user_settings/menus/active_sessions_menu_spec.rb index be5f826ee58..d4b9c359a98 100644 --- a/spec/lib/sidebars/user_settings/menus/active_sessions_menu_spec.rb +++ b/spec/lib/sidebars/user_settings/menus/active_sessions_menu_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe Sidebars::UserSettings::Menus::ActiveSessionsMenu, feature_category: :navigation do it_behaves_like 'User settings menu', - link: '/-/profile/active_sessions', + link: '/-/user_settings/active_sessions', title: _('Active Sessions'), icon: 'monitor-lines', active_routes: { controller: :active_sessions } diff --git a/spec/requests/api/graphql/mutations/branch_rules/update_spec.rb b/spec/requests/api/graphql/mutations/branch_rules/update_spec.rb new file mode 100644 index 00000000000..14874bdfaa8 --- /dev/null +++ b/spec/requests/api/graphql/mutations/branch_rules/update_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'BranchRuleUpdate', feature_category: :source_code_management do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :public) } + let_it_be(:user) { create(:user) } + let!(:branch_rule_1) { create(:protected_branch, project: project, name: name_1) } + let!(:branch_rule_2) { create(:protected_branch, project: project, name: name_2) } + let(:name_1) { "name_1" } + let(:name_2) { "name_2" } + let(:new_name) { "new name" } + let(:id) { branch_rule_1.to_global_id } + let(:project_path) { project.full_path } + let(:name) { new_name } + let(:params) do + { + id: id, + project_path: project_path, + name: name + } + end + + let(:mutation) { graphql_mutation(:branch_rule_update, params) } + + subject(:post_mutation) { post_graphql_mutation(mutation, current_user: user) } + + def mutation_response + graphql_mutation_response(:branch_rule_update) + end + + context 'when the user does not have permission' do + before_all do + project.add_developer(user) + end + + it 'does not update the branch rule' do + expect { post_mutation }.not_to change { branch_rule_1 } + end + end + + context 'when the user can update a branch rules' do + let(:current_user) { user } + + before_all do + project.add_maintainer(user) + end + + it 'updates the protected branch' do + post_mutation + + expect(branch_rule_1.reload.name).to eq(new_name) + end + + it 'returns the updated branch rule' do + post_mutation + + expect(mutation_response).to have_key('branchRule') + expect(mutation_response['branchRule']['name']).to eq(new_name) + expect(mutation_response['errors']).to be_empty + end + + context 'when name already exists for the project' do + let(:params) do + { + id: id, + project_path: project_path, + name: name_2 + } + end + + it 'returns an error' do + post_mutation + + expect(mutation_response['errors'].first).to eq('Name has already been taken') + end + end + + context 'when the protected branch cannot be found' do + let(:id) { "gid://gitlab/ProtectedBranch/#{non_existing_record_id}" } + + it_behaves_like 'a mutation that returns top-level errors', + errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] + end + + context 'when the project cannot be found' do + let(:project_path) { 'not a project path' } + + it_behaves_like 'a mutation that returns top-level errors', + errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] + end + end +end diff --git a/spec/requests/ide_controller_spec.rb b/spec/requests/ide_controller_spec.rb index fae3d9533c5..20d890fadbf 100644 --- a/spec/requests/ide_controller_spec.rb +++ b/spec/requests/ide_controller_spec.rb @@ -164,6 +164,14 @@ RSpec.describe IdeController, feature_category: :web_ide do expect(response).to render_template('layouts/application') end + + it 'does not create oauth application' do + expect(Doorkeeper::Application).not_to receive(:new) + + subject + + expect(web_ide_oauth_application).to be_nil + end end describe 'vscode IDE' do @@ -177,6 +185,40 @@ RSpec.describe IdeController, feature_category: :web_ide do expect(response).to render_template('layouts/fullscreen') end end + + describe 'with web_ide_oauth flag off' do + before do + stub_feature_flags(web_ide_oauth: false) + end + + it 'does not create oauth application' do + expect(Doorkeeper::Application).not_to receive(:new) + + subject + + expect(web_ide_oauth_application).to be_nil + end + end + + it 'ensures web_ide_oauth_application' do + expect(Doorkeeper::Application).to receive(:new).and_call_original + + subject + + expect(web_ide_oauth_application).not_to be_nil + expect(web_ide_oauth_application[:name]).to eq('GitLab Web IDE') + end + + it 'when web_ide_oauth_application already exists, does not create new one' do + existing_app = create(:oauth_application, owner_id: nil, owner_type: nil) + + stub_application_setting({ web_ide_oauth_application: existing_app }) + expect(Doorkeeper::Application).not_to receive(:new) + + subject + + expect(web_ide_oauth_application).to eq(existing_app) + end end describe 'content security policy' do @@ -199,4 +241,48 @@ RSpec.describe IdeController, feature_category: :web_ide do end end end + + describe '#oauth_redirect', :aggregate_failures do + subject(:oauth_redirect) { get '/-/ide/oauth_redirect' } + + it 'with no web_ide_oauth_application, returns not_found' do + oauth_redirect + + expect(response).to have_gitlab_http_status(:not_found) + end + + context 'with web_ide_oauth_application set' do + before do + stub_application_setting({ + web_ide_oauth_application: create(:oauth_application, owner_id: nil, owner_type: nil) + }) + end + + it 'returns ok and renders view' do + oauth_redirect + + expect(response).to have_gitlab_http_status(:ok) + end + + it 'with vscode_web_ide flag off, returns not_found' do + stub_feature_flags(vscode_web_ide: false) + + oauth_redirect + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'with web_ide_oauth flag off, returns not_found' do + stub_feature_flags(web_ide_oauth: false) + + oauth_redirect + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + def web_ide_oauth_application + ::Gitlab::CurrentSettings.current_application_settings.web_ide_oauth_application + end end diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index 1bd138ea148..9edb9c842ab 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -394,10 +394,18 @@ RSpec.describe JwksController, "routing" do end end -# user_settings_authentication_log GET /-/user_settings/authentication_log(.:format) system_access/user_settings#authentication_log +# user_settings_authentication_log GET /-/user_settings/authentication_log(.:format) user_settings/user_settings#authentication_log RSpec.describe UserSettings::UserSettingsController, 'routing', feature_category: :system_access do it 'to #authentication_log' do expect(get('/-/user_settings/authentication_log')).to route_to('user_settings/user_settings#authentication_log') end end + +# user_settings_active_sessions_log GET /-/user_settings_active_sessions_log(.:format) user_settings/active_sessions#index# + +RSpec.describe UserSettings::ActiveSessionsController, 'routing', feature_category: :system_access do + it 'to #index' do + expect(get('/-/user_settings/active_sessions')).to route_to('user_settings/active_sessions#index') + end +end diff --git a/spec/services/projects/hashed_storage/base_attachment_service_spec.rb b/spec/services/projects/hashed_storage/base_attachment_service_spec.rb index e32747ad907..4834af79225 100644 --- a/spec/services/projects/hashed_storage/base_attachment_service_spec.rb +++ b/spec/services/projects/hashed_storage/base_attachment_service_spec.rb @@ -45,7 +45,7 @@ RSpec.describe Projects::HashedStorage::BaseAttachmentService, feature_category: describe '#move_folder!' do context 'when old_path is not a directory' do it 'adds information to the logger and returns true' do - Tempfile.create do |old_path| # rubocop:disable Rails/SaveBang + Tempfile.create do |old_path| new_path = "#{old_path}-new" expect(subject.send(:move_folder!, old_path, new_path)).to be_truthy diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml index fe84a80dae6..ccb3dfc7ffc 100644 --- a/spec/support/rspec_order_todo.yml +++ b/spec/support/rspec_order_todo.yml @@ -3218,7 +3218,7 @@ - './spec/controllers/omniauth_callbacks_controller_spec.rb' - './spec/controllers/passwords_controller_spec.rb' - './spec/controllers/profiles/accounts_controller_spec.rb' -- './spec/controllers/profiles/active_sessions_controller_spec.rb' +- './spec/controllers/user_settings/active_sessions_controller_spec.rb' - './spec/controllers/profiles/avatars_controller_spec.rb' - './spec/controllers/profiles_controller_spec.rb' - './spec/controllers/profiles/emails_controller_spec.rb' @@ -3727,7 +3727,7 @@ - './spec/features/password_reset_spec.rb' - './spec/features/populate_new_pipeline_vars_with_params_spec.rb' - './spec/features/profiles/account_spec.rb' -- './spec/features/profiles/active_sessions_spec.rb' +- './spec/features/user_settings/active_sessions_spec.rb' - './spec/features/profiles/chat_names_spec.rb' - './spec/features/profiles/emails_spec.rb' - './spec/features/profiles/gpg_keys_spec.rb' diff --git a/spec/tooling/lib/tooling/parallel_rspec_runner_spec.rb b/spec/tooling/lib/tooling/parallel_rspec_runner_spec.rb index 27869974b39..ed9fdc3825b 100644 --- a/spec/tooling/lib/tooling/parallel_rspec_runner_spec.rb +++ b/spec/tooling/lib/tooling/parallel_rspec_runner_spec.rb @@ -55,7 +55,7 @@ RSpec.describe Tooling::ParallelRSpecRunner, feature_category: :tooling do # rub context 'given filter tests file' do let(:filter_tests_file) do - Tempfile.create.tap do |f| # rubocop:disable Rails/SaveBang -- Tempfile has no create! method + Tempfile.create.tap do |f| f.write(filter_tests.join(' ')) f.rewind end diff --git a/spec/workers/ci/catalog/resources/process_sync_events_worker_spec.rb b/spec/workers/ci/catalog/resources/process_sync_events_worker_spec.rb index 3c5f7bc0bf9..fba8bb50a32 100644 --- a/spec/workers/ci/catalog/resources/process_sync_events_worker_spec.rb +++ b/spec/workers/ci/catalog/resources/process_sync_events_worker_spec.rb @@ -16,14 +16,14 @@ RSpec.describe Ci::Catalog::Resources::ProcessSyncEventsWorker, feature_category end describe '#perform' do - let_it_be(:project) { create(:project) } + let_it_be(:project) { create(:project, name: 'Old Name') } let_it_be(:resource) { create(:ci_catalog_resource, project: project) } before_all do create(:ci_catalog_resource_sync_event, catalog_resource: resource, status: :processed) create_list(:ci_catalog_resource_sync_event, 2, catalog_resource: resource) # PG trigger adds an event for this update - project.update!(name: 'Name', description: 'Test', visibility_level: Gitlab::VisibilityLevel::INTERNAL) + project.update!(name: 'New Name', description: 'Test', visibility_level: Gitlab::VisibilityLevel::INTERNAL) end subject(:perform) { worker.perform } @@ -48,5 +48,21 @@ RSpec.describe Ci::Catalog::Resources::ProcessSyncEventsWorker, feature_category perform end + + context 'when FF `ci_process_catalog_resource_sync_events` is disabled' do + before do + stub_feature_flags(ci_process_catalog_resource_sync_events: false) + end + + it 'does not process the sync events', :aggregate_failures do + expect(worker).not_to receive(:log_extra_metadata_on_done) + + expect { perform }.not_to change { Ci::Catalog::Resources::SyncEvent.status_pending.count } + + expect(resource.reload.name).to eq('Old Name') + expect(resource.reload.description).to be_nil + expect(resource.reload.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) + end + end end end diff --git a/workhorse/go.mod b/workhorse/go.mod index fc29ef136ba..f14a1e86d42 100644 --- a/workhorse/go.mod +++ b/workhorse/go.mod @@ -28,9 +28,9 @@ require ( golang.org/x/image v0.7.0 golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 golang.org/x/net v0.17.0 - golang.org/x/oauth2 v0.10.0 + golang.org/x/oauth2 v0.11.0 golang.org/x/tools v0.14.0 - google.golang.org/grpc v1.58.3 + google.golang.org/grpc v1.59.0 google.golang.org/protobuf v1.31.0 honnef.co/go/tools v0.4.6 ) @@ -68,7 +68,6 @@ require ( github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect - github.com/google/go-cmp v0.5.9 // indirect github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8 // indirect github.com/google/s2a-go v0.1.4 // indirect github.com/google/uuid v1.3.1 // indirect diff --git a/workhorse/go.sum b/workhorse/go.sum index cc065837dd5..56bfe4e54ab 100644 --- a/workhorse/go.sum +++ b/workhorse/go.sum @@ -245,7 +245,6 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-replayers/grpcreplay v1.1.0 h1:S5+I3zYyZ+GQz68OfbURDdt/+cSMqCK1wrvNx7WBzTE= github.com/google/go-replayers/httpreplay v1.2.0 h1:VM1wEyyjaoU53BwrOnaf9VhAyQQEEioJvFYxYcLRKzk= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= @@ -593,8 +592,8 @@ golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8= -golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI= +golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU= +golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= golang.org/x/sync v0.0.0-20170517211232-f52d1811a629/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -894,8 +893,8 @@ google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQ google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ= -google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= |