Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-12-13 09:12:51 +00:00
parent e7b6c527c4
commit c283bdb7bc
39 changed files with 562 additions and 99 deletions

View File

@ -1 +1 @@
14.31.0
14.32.0

View File

@ -504,7 +504,7 @@ group :test do
gem 'webmock', '~> 3.19.1' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'rails-controller-testing' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'concurrent-ruby', '~> 1.1' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'test-prof', '~> 1.3.0' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'test-prof', '~> 1.3.1' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'rspec_junit_formatter' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'guard-rspec' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'axe-core-rspec' # rubocop:todo Gemfile/MissingFeatureCategory

View File

@ -643,7 +643,7 @@
{"name":"term-ansicolor","version":"1.7.1","platform":"ruby","checksum":"92339ffec77c4bddc786a29385c91601dd52fc68feda23609bba0491229b05f7"},
{"name":"terminal-table","version":"3.0.2","platform":"ruby","checksum":"f951b6af5f3e00203fb290a669e0a85c5dd5b051b3b023392ccfd67ba5abae91"},
{"name":"terser","version":"1.0.2","platform":"ruby","checksum":"80c2e0bc7e2db4e12e8529658f9e0820e13d685ae67d745bf981f269743bb28e"},
{"name":"test-prof","version":"1.3.0","platform":"ruby","checksum":"6092032f15810063a7b83595ce102398fbf3d684b9d3c71fb7b9c1d6464b2c17"},
{"name":"test-prof","version":"1.3.1","platform":"ruby","checksum":"aa35cd06eae4e6545d7b1310181875e61ad0e9e08c31223fb1aafd50da187523"},
{"name":"test_file_finder","version":"0.2.1","platform":"ruby","checksum":"a5e9b369d80c76aefbb609acf5e11d89a048f35e565de3cc261c20112f0fcdb3"},
{"name":"text","version":"1.3.1","platform":"ruby","checksum":"2fbbbc82c1ce79c4195b13018a87cbb00d762bda39241bb3cdc32792759dd3f4"},
{"name":"thor","version":"1.3.0","platform":"ruby","checksum":"1adc7f9e5b3655a68c71393fee8bd0ad088d14ee8e83a0b73726f23cbb3ca7c3"},

View File

@ -1637,7 +1637,7 @@ GEM
unicode-display_width (>= 1.1.1, < 3)
terser (1.0.2)
execjs (>= 0.3.0, < 3)
test-prof (1.3.0)
test-prof (1.3.1)
test_file_finder (0.2.1)
faraday (>= 1.0, < 3.0, != 2.0.0)
text (1.3.1)
@ -2084,7 +2084,7 @@ DEPENDENCIES
tanuki_emoji (~> 0.9)
telesignenterprise (~> 2.2)
terser (= 1.0.2)
test-prof (~> 1.3.0)
test-prof (~> 1.3.1)
test_file_finder (~> 0.2.1)
thrift (>= 0.16.0)
timfel-krb5-auth (~> 0.8)

View File

@ -80,10 +80,10 @@ export function getStatefulSetStatuses(items) {
export function getReplicaSetStatuses(items) {
const failed = items.filter((item) => {
return item.status?.readyReplicas < item.spec?.replicas;
return calculateStatefulSetStatus(item) === STATUS_FAILED;
});
const ready = items.filter((item) => {
return item.status?.readyReplicas === item.spec?.replicas;
return calculateStatefulSetStatus(item) === STATUS_READY;
});
return {

View File

@ -15,8 +15,12 @@ export const STATUSES = {
export const PROVIDERS = {
GITHUB: 'github',
BITBUCKET_SERVER: 'bitbucket_server',
};
// Retrieved from value of `PAGE_LENGTH` in lib/bitbucket_server/paginator.rb
export const BITBUCKET_SERVER_PAGE_LENGTH = 25;
const SCHEDULED_STATUS_ICON = {
icon: 'status-scheduled',
text: __('Pending'),

View File

@ -105,10 +105,7 @@ export default {
mounted() {
this.fetchJobs();
if (!this.paginatable) {
this.fetchRepos();
}
this.fetchRepos();
},
beforeDestroy() {

View File

@ -21,7 +21,6 @@ export function initStoreFromElement(element) {
importPath,
cancelPath,
defaultTargetNamespace,
paginatable,
} = element.dataset;
return createStore({
@ -37,7 +36,6 @@ export function initStoreFromElement(element) {
importPath,
cancelPath,
},
hasPagination: parseBoolean(paginatable),
});
}

View File

@ -9,7 +9,7 @@ import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { visitUrl, objectToQuery } from '~/lib/utils/url_utility';
import { s__, sprintf } from '~/locale';
import { isProjectImportable } from '../utils';
import { PROVIDERS } from '../../constants';
import { PROVIDERS, BITBUCKET_SERVER_PAGE_LENGTH } from '../../constants';
import * as types from './mutation_types';
let eTagPoll;
@ -33,6 +33,16 @@ const commitPaginationData = ({ state, commit, data }) => {
const nextPage = state.pageInfo.page + 1;
commit(types.SET_PAGE, nextPage);
}
// Only BitBucket Server uses pagination with page length
if (state.provider === PROVIDERS.BITBUCKET_SERVER) {
const reposLength = data.providerRepos.length;
if (reposLength > 0 && reposLength % BITBUCKET_SERVER_PAGE_LENGTH === 0) {
commit(types.SET_HAS_NEXT_PAGE, true);
} else {
commit(types.SET_HAS_NEXT_PAGE, false);
}
}
};
const paginationParams = ({ state }) => {
if (state.provider === PROVIDERS.GITHUB && state.pageInfo.endCursor) {

View File

@ -8,10 +8,10 @@ import state from './state';
Vue.use(Vuex);
export default ({ initialState, endpoints, hasPagination }) =>
export default ({ initialState, endpoints }) =>
new Vuex.Store({
state: { ...state(), ...initialState },
actions: actionsFactory({ endpoints, hasPagination }),
actions: actionsFactory({ endpoints }),
mutations,
getters,
});

View File

@ -17,3 +17,5 @@ export const SET_IMPORT_TARGET = 'SET_IMPORT_TARGET';
export const SET_PAGE = 'SET_PAGE';
export const SET_PAGE_CURSORS = 'SET_PAGE_CURSORS';
export const SET_HAS_NEXT_PAGE = 'SET_HAS_NEXT_PAGE';

View File

@ -143,4 +143,8 @@ export default {
const { startCursor, endCursor, hasNextPage } = pageInfo;
state.pageInfo = { ...state.pageInfo, startCursor, endCursor, hasNextPage };
},
[types.SET_HAS_NEXT_PAGE](state, hasNextPage) {
state.pageInfo.hasNextPage = hasNextPage;
},
};

View File

@ -9,6 +9,6 @@ export default () => ({
page: 0,
startCursor: null,
endCursor: null,
hasNextPage: true,
hasNextPage: false,
},
});

View File

@ -4,6 +4,7 @@ import typeDefs from '~/environments/graphql/typedefs.graphql';
import k8sPodsQuery from './queries/k8s_dashboard_pods.query.graphql';
import k8sDeploymentsQuery from './queries/k8s_dashboard_deployments.query.graphql';
import k8sStatefulSetsQuery from './queries/k8s_dashboard_stateful_sets.query.graphql';
import k8sReplicaSetsQuery from './queries/k8s_dashboard_replica_sets.query.graphql';
import { resolvers } from './resolvers';
export const apolloProvider = () => {
@ -63,6 +64,25 @@ export const apolloProvider = () => {
},
});
cache.writeQuery({
query: k8sReplicaSetsQuery,
data: {
metadata: {
name: null,
namespace: null,
creationTimestamp: null,
labels: null,
annotations: null,
},
status: {
readyReplicas: null,
},
spec: {
replicas: null,
},
},
});
return new VueApollo({
defaultClient,
});

View File

@ -0,0 +1,17 @@
query getK8sDashboardReplicaSets($configuration: LocalConfiguration) {
k8sReplicaSets(configuration: $configuration) @client {
metadata {
name
namespace
creationTimestamp
labels
annotations
}
status {
readyReplicas
}
spec {
replicas
}
}
}

View File

@ -11,6 +11,7 @@ import {
import k8sDashboardPodsQuery from '../queries/k8s_dashboard_pods.query.graphql';
import k8sDashboardDeploymentsQuery from '../queries/k8s_dashboard_deployments.query.graphql';
import k8sDashboardStatefulSetsQuery from '../queries/k8s_dashboard_stateful_sets.query.graphql';
import k8sDashboardReplicaSetsQuery from '../queries/k8s_dashboard_replica_sets.query.graphql';
export default {
k8sPods(_, { configuration }, { client }) {
@ -91,4 +92,41 @@ export default {
}
});
},
k8sReplicaSets(_, { configuration, namespace = '' }, { client }) {
const config = new Configuration(configuration);
const appsV1api = new AppsV1Api(config);
const deploymentsApi = namespace
? appsV1api.listAppsV1NamespacedReplicaSet({ namespace })
: appsV1api.listAppsV1ReplicaSetForAllNamespaces();
return deploymentsApi
.then((res) => {
const watchPath = buildWatchPath({
resource: 'replicasets',
api: 'apis/apps/v1',
namespace,
});
watchWorkloadItems({
client,
query: k8sDashboardReplicaSetsQuery,
configuration,
namespace,
watchPath,
queryField: 'k8sReplicaSets',
mapFn: mapSetItem,
});
const data = res?.items || [];
return data.map(mapSetItem);
})
.catch(async (err) => {
try {
await handleClusterError(err);
} catch (error) {
throw new Error(error.message);
}
});
},
};

View File

@ -0,0 +1,80 @@
<script>
import { s__ } from '~/locale';
import { getAge, calculateStatefulSetStatus } from '../helpers/k8s_integration_helper';
import WorkloadLayout from '../components/workload_layout.vue';
import k8sReplicaSetsQuery from '../graphql/queries/k8s_dashboard_replica_sets.query.graphql';
import { STATUS_FAILED, STATUS_READY, STATUS_LABELS } from '../constants';
export default {
components: {
WorkloadLayout,
},
inject: ['configuration'],
apollo: {
k8sReplicaSets: {
query: k8sReplicaSetsQuery,
variables() {
return {
configuration: this.configuration,
};
},
update(data) {
return (
data?.k8sReplicaSets?.map((replicaSet) => {
return {
name: replicaSet.metadata?.name,
namespace: replicaSet.metadata?.namespace,
status: calculateStatefulSetStatus(replicaSet),
age: getAge(replicaSet.metadata?.creationTimestamp),
labels: replicaSet.metadata?.labels,
annotations: replicaSet.metadata?.annotations,
kind: s__('KubernetesDashboard|ReplicaSet'),
};
}) || []
);
},
error(err) {
this.errorMessage = err?.message;
},
},
},
data() {
return {
k8sReplicaSets: [],
errorMessage: '',
};
},
computed: {
replicaSetsStats() {
return [
{
value: this.countReplicaSetsByStatus(STATUS_READY),
title: STATUS_LABELS[STATUS_READY],
},
{
value: this.countReplicaSetsByStatus(STATUS_FAILED),
title: STATUS_LABELS[STATUS_FAILED],
},
];
},
loading() {
return this.$apollo.queries.k8sReplicaSets.loading;
},
},
methods: {
countReplicaSetsByStatus(phase) {
const filteredReplicaSets = this.k8sReplicaSets.filter((item) => item.status === phase) || [];
return filteredReplicaSets.length;
},
},
};
</script>
<template>
<workload-layout
:loading="loading"
:error-message="errorMessage"
:stats="replicaSetsStats"
:items="k8sReplicaSets"
/>
</template>

View File

@ -1,7 +1,9 @@
export const PODS_ROUTE_NAME = 'pods';
export const DEPLOYMENTS_ROUTE_NAME = 'deployments';
export const STATEFUL_SETS_ROUTE_NAME = 'statefulSets';
export const REPLICA_SETS_ROUTE_NAME = 'replicaSets';
export const PODS_ROUTE_PATH = '/pods';
export const DEPLOYMENTS_ROUTE_PATH = '/deployments';
export const STATEFUL_SETS_ROUTE_PATH = '/statefulsets';
export const REPLICA_SETS_ROUTE_PATH = '/replicasets';

View File

@ -2,6 +2,7 @@ import { s__ } from '~/locale';
import PodsPage from '../pages/pods_page.vue';
import DeploymentsPage from '../pages/deployments_page.vue';
import StatefulSetsPage from '../pages/stateful_sets_page.vue';
import ReplicaSetsPage from '../pages/replica_sets_page.vue';
import {
PODS_ROUTE_NAME,
PODS_ROUTE_PATH,
@ -9,6 +10,8 @@ import {
DEPLOYMENTS_ROUTE_PATH,
STATEFUL_SETS_ROUTE_NAME,
STATEFUL_SETS_ROUTE_PATH,
REPLICA_SETS_ROUTE_NAME,
REPLICA_SETS_ROUTE_PATH,
} from './constants';
export default [
@ -36,4 +39,12 @@ export default [
title: s__('KubernetesDashboard|StatefulSets'),
},
},
{
name: REPLICA_SETS_ROUTE_NAME,
path: REPLICA_SETS_ROUTE_PATH,
component: ReplicaSetsPage,
meta: {
title: s__('KubernetesDashboard|ReplicaSets'),
},
},
];

View File

@ -0,0 +1,31 @@
import CiIcon from './ci_icon.vue';
export default {
component: CiIcon,
title: 'vue_shared/ci_icon',
};
const Template = (args, { argTypes }) => ({
components: { CiIcon },
props: Object.keys(argTypes),
template: '<ci-icon v-bind="$props" />',
});
export const Default = Template.bind({});
Default.args = {
status: {
icon: 'status_success',
text: 'Success',
detailsPath: 'https://gitab.com/',
},
};
export const WithText = Template.bind({});
WithText.args = {
status: {
icon: 'status_success',
text: 'Success',
detailsPath: 'https://gitab.com/',
},
showStatusText: true,
};

View File

@ -41,7 +41,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:moved_mr_sidebar, project)
push_frontend_feature_flag(:sast_reports_in_inline_diff, project)
push_frontend_feature_flag(:mr_experience_survey, project)
push_force_frontend_feature_flag(:summarize_my_code_review, summarize_my_code_review_enabled?)
push_frontend_feature_flag(:ci_job_failures_in_mr, project)
push_frontend_feature_flag(:mr_pipelines_graphql, project)
push_frontend_feature_flag(:notifications_todos_buttons, current_user)
@ -49,12 +48,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:merge_blocked_component, current_user)
end
before_action only: [:edit] do
if can?(current_user, :fill_in_merge_request_template, project)
push_frontend_feature_flag(:fill_in_mr_template, project)
end
end
around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :diffs, :discussions]
after_action :log_merge_request_show, only: [:show, :diffs]
@ -638,16 +631,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
Date.strptime(date, "%Y-%m-%d")&.to_time&.to_i if date
rescue Date::Error, TypeError
end
def summarize_my_code_review_enabled?
namespace = project&.group&.root_ancestor
return false if namespace.nil?
Feature.enabled?(:summarize_my_code_review, current_user) &&
namespace.group_namespace? &&
namespace.licensed_feature_available?(:summarize_my_mr_code_review) &&
Gitlab::Llm::StageCheck.available?(namespace, :summarize_my_mr_code_review)
end
end
Projects::MergeRequestsController.prepend_mod_with('Projects::MergeRequestsController')

View File

@ -1,20 +0,0 @@
description: Install cluster application
category: cluster:applications
action: cluster application name
label_description:
property_description:
value_description:
extra_properties:
identifiers:
product_section: ops
product_stage: monitor
product_group: group::monitor
milestone: "12.7"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23000
distributions:
- ce
- ee
tiers:
- free
- premium
- ultimate

View File

@ -31553,6 +31553,7 @@ Possible identifier types for a measurement.
| Value | Description |
| ----- | ----------- |
| <a id="valuestreamdashboardmetriccontributors"></a>`CONTRIBUTORS` | Contributor count. EXPERIMENTAL: Only available on the SaaS version of GitLab when the ClickHouse database backend is enabled. |
| <a id="valuestreamdashboardmetricgroups"></a>`GROUPS` | Group count. |
| <a id="valuestreamdashboardmetricissues"></a>`ISSUES` | Issue count. |
| <a id="valuestreamdashboardmetricmerge_requests"></a>`MERGE_REQUESTS` | Merge request count. |

View File

@ -240,6 +240,7 @@ When the user is authenticated and `simple` is not set this returns something li
"ci_allow_fork_pipelines_to_run_in_parent_project": true,
"ci_job_token_scope_enabled": false,
"ci_separated_caches": true,
"ci_restrict_pipeline_cancellation_role": "developer",
"public_jobs": true,
"build_timeout": 3600,
"auto_cancel_pending_pipelines": "enabled",
@ -416,6 +417,7 @@ GET /users/:user_id/projects
"ci_forward_deployment_rollback_allowed": true,
"ci_allow_fork_pipelines_to_run_in_parent_project": true,
"ci_separated_caches": true,
"ci_restrict_pipeline_cancellation_role": "developer",
"public_jobs": true,
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
@ -535,6 +537,7 @@ GET /users/:user_id/projects
"ci_forward_deployment_rollback_allowed": true,
"ci_allow_fork_pipelines_to_run_in_parent_project": true,
"ci_separated_caches": true,
"ci_restrict_pipeline_cancellation_role": "developer",
"public_jobs": true,
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
@ -1205,6 +1208,7 @@ GET /projects/:id
"ci_forward_deployment_rollback_allowed": true,
"ci_allow_fork_pipelines_to_run_in_parent_project": true,
"ci_separated_caches": true,
"ci_restrict_pipeline_cancellation_role": "developer",
"public_jobs": true,
"shared_with_groups": [
{
@ -2321,6 +2325,7 @@ Example response:
"ci_forward_deployment_rollback_allowed": true,
"ci_allow_fork_pipelines_to_run_in_parent_project": true,
"ci_separated_caches": true,
"ci_restrict_pipeline_cancellation_role": "developer",
"public_jobs": true,
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
@ -2453,6 +2458,7 @@ Example response:
"ci_forward_deployment_rollback_allowed": true,
"ci_allow_fork_pipelines_to_run_in_parent_project": true,
"ci_separated_caches": true,
"ci_restrict_pipeline_cancellation_role": "developer",
"public_jobs": true,
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,

View File

@ -75,7 +75,7 @@ RAILS_ENV=development bundle exec rake gitlab:duo:setup['<test-group-name>']
1. You can use Rake task `rake gitlab:duo:enable_feature_flags` to enable all feature flags that are assigned to group AI Framework
1. Set the required access token. To receive an access token:
1. For Vertex, follow the [instructions below](#configure-gcp-vertex-access).
1. For all other providers, like Anthropic, create an access request where `@m_gill`, `@wayne`, and `@timzallmann` are the tech stack owners.
1. For Anthropic, create an access request
### Set up the embedding database

View File

@ -69,6 +69,10 @@ If you already have a working GDK, you should
### Using Gitpod
If you want to contribute without the overhead of setting up a local development environment,
you can use [Gitpod](../../integration/gitpod.md).
Gitpod runs a virtual instance of the GDK.
Set aside about 15 minutes to launch the GDK in Gitpod.
1. Launch the GDK in [Gitpod](https://gitpod.io/#https://gitlab.com/gitlab-community/gitlab/-/tree/master/).
@ -185,12 +189,6 @@ To confirm it was successful:
If you get errors, run `gdk doctor` to troubleshoot. For more advanced troubleshooting, see
[the troubleshooting docs](https://gitlab.com/gitlab-org/gitlab-development-kit/-/tree/main/doc/troubleshooting).
### Gitpod
If you want to contribute without the overhead of setting up a local development environment,
you can use [Gitpod](../../integration/gitpod.md) instead.
Gitpod runs a virtual instance of the GDK.
## Step 2: Change the code
[View an interactive demo of this step](https://gitlab.navattic.com/uu5a0dc5).
@ -206,6 +204,9 @@ I want to change this text:
Other settings on the page start with the word `Customize` and skip the `This setting allows you to` part.
I'll update this phrase to match the others.
NOTE:
As this text has already been [changed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/116472) when developing this tutorial, you can instead search for `Customize the appearance of the syntax` to find the files that were changed.
1. Search the `gitlab-development-kit/gitlab` directory for the string `This setting allows you to customize`.
The results show one `.haml` file, two `.md` files, one `.pot` file, and

View File

@ -52,7 +52,7 @@ To run a DAST authenticated scan:
#### Form authentication
- You are using either the [DAST proxy-based analyzer](proxy-based.md) or the [DAST browser-based analyzer](browser_based.md).
- You know the URL of the login form of your application. Alternatively, you know how to go to the login form from the authentication URL (see [clicking to go to the login form](#clicking-to-go-to-the-login-form)).
- You know the URL of the login form of your application. Alternatively, you know how to go to the login form from the authentication URL (see [clicking to go to the login form](#click-to-go-to-the-login-form)).
- You know the [selectors](#finding-an-elements-selector) of the username and password HTML fields that DAST uses to input the respective values.
- You know the element's [selector](#finding-an-elements-selector) that submits the login form when selected.
@ -80,6 +80,7 @@ To run a DAST authenticated scan:
| `DAST_USERNAME` <sup>1</sup> | string | The username to authenticate to in the website. Example: `admin` |
| `DAST_USERNAME_FIELD` <sup>1</sup> | [selector](#finding-an-elements-selector) | A selector describing the element used to enter the username on the login form. Example: `name:username` |
| `DAST_AUTH_DISABLE_CLEAR_FIELDS` | boolean | Disables clearing of username and password fields before attempting manual login. Set to `false` by default. |
| `DAST_AFTER_LOGIN_ACTIONS` | string | Comma separated list of actions to be run after login but before login verification. Currently supports "click" actions. Example: `click(on=id:change_to_bar_graph),click(on=css:input[name=username])` | |
1. Available to an on-demand proxy-based DAST scan.
1. Not available to proxy-based scans.
@ -191,7 +192,7 @@ authentication using the [single-step](#configuration-for-a-single-step-login-fo
DAST supports authentication processes where a user is redirected to an external Identity Provider's site to log in.
Check the [known limitations](#known-limitations) of DAST authentication to determine if your SSO authentication process is supported.
### Clicking to go to the login form
### Click to go to the login form
Define `DAST_BROWSER_PATH_TO_LOGIN_FORM` to provide a path of elements to click on from the `DAST_AUTH_URL` so that DAST can access the
login form. This method is suitable for applications that show the login form in a pop-up (modal) window or when the login form does not
@ -210,6 +211,25 @@ dast:
DAST_BROWSER_PATH_TO_LOGIN_FORM: "css:.navigation-menu,css:.login-menu-item"
```
### Perform additional actions after submitting the username and password
Define `DAST_AFTER_LOGIN_ACTIONS` to provide a sequence of actions required to complete the login process after the username and password forms have been submitted. For example, this can be used to dismiss a modal dialog (such as a "keep me signed in?" prompt) that appears after the submit button is pressed.
DAST verifies authentication is successful and records authentication tokens once after-login actions have been executed.
For example:
```yaml
include:
- template: DAST.gitlab-ci.yml
dast:
variables:
DAST_WEBSITE: "https://example.com"
DAST_AUTH_URL: "https://example.com/login"
DAST_AFTER_LOGIN_ACTIONS: "click(on=id:modal-yes)"
```
### Excluding logout URLs
If DAST crawls the logout URL while running an authenticated scan, the user is logged out, resulting in the remainder of the scan being unauthenticated.

View File

@ -61,8 +61,7 @@ This feature is an [Experiment](../../../policy/experiment-beta-support.md) on G
When you've completed your review of a merge request and are ready to [submit your review](reviews/index.md#submit-a-review), generate a GitLab Duo Code review summary:
1. When you are ready to submit your review, select **Finish review**.
1. Select **AI Actions** (**{tanuki}**).
1. Select **Summarize my code review**.
1. Select **Summarize my pending comments**.
The summary is displayed in the comment box. You can edit and refine the summary prior to submitting your review.

View File

@ -2,6 +2,7 @@
module BitbucketServer
class Paginator
# Should be kept in-sync with `BITBUCKET_SERVER_PAGE_LENGTH` in app/assets/javascripts/import_entities/constants.js
PAGE_LENGTH = 25
attr_reader :page_offset

View File

@ -27919,6 +27919,12 @@ msgstr ""
msgid "KubernetesDashboard|Ready"
msgstr ""
msgid "KubernetesDashboard|ReplicaSet"
msgstr ""
msgid "KubernetesDashboard|ReplicaSets"
msgstr ""
msgid "KubernetesDashboard|Running"
msgstr ""

View File

@ -331,7 +331,7 @@ module QA
end
def run_git(command_str, env: env_vars, max_attempts: 1)
run(command_str, env: env, max_attempts: max_attempts, log_prefix: 'Git: ')
run(command_str, env: env, max_attempts: max_attempts, sleep_internal: 10, log_prefix: 'Git: ')
end
end
end

View File

@ -21,11 +21,11 @@ module QA
end
end
def run(command_str, env: [], max_attempts: 1, log_prefix: '')
def run(command_str, env: [], max_attempts: 1, sleep_internal: 0, log_prefix: '')
command = [*env, command_str, '2>&1'].compact.join(' ')
result = nil
repeat_until(max_attempts: max_attempts, raise_on_failure: false) do
repeat_until(max_attempts: max_attempts, sleep_interval: sleep_internal, raise_on_failure: false) do
Runtime::Logger.debug "#{log_prefix}pwd=[#{Dir.pwd}], command=[#{command}]"
output, status = Open3.capture2e(command)
output.chomp!

View File

@ -9,7 +9,7 @@ RSpec.describe QA::Git::Repository do
let(:repo_uri_with_credentials) { 'http://root@foo/bar.git' }
let(:env_vars) { [%q(HOME="temp")] }
let(:extra_env_vars) { [] }
let(:run_params) { { env: env_vars + extra_env_vars, log_prefix: "Git: " } }
let(:run_params) { { env: env_vars + extra_env_vars, sleep_internal: 10, log_prefix: "Git: " } }
let(:repository) do
described_class.new.tap do |r|
r.uri = repo_uri

View File

@ -36,6 +36,7 @@ describe('ImportProjectsTable', () => {
.filter((w) => w.props().variant === 'confirm')
.at(0);
const findImportAllModal = () => wrapper.findComponent({ ref: 'importAllModal' });
const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
const importAllFn = jest.fn();
const importAllModalShowFn = jest.fn();
@ -203,41 +204,14 @@ describe('ImportProjectsTable', () => {
describe('when paginatable is set to true', () => {
const initState = {
namespaces: [{ fullPath: 'path' }],
pageInfo: { page: 1, hasNextPage: true },
pageInfo: { page: 1, hasNextPage: false },
repositories: [
{ importSource: { id: 1 }, importedProject: null, importStatus: STATUSES.NONE },
],
};
describe('with hasNextPage true', () => {
beforeEach(() => {
createComponent({
state: initState,
paginatable: true,
});
});
it('does not call fetchRepos on mount', () => {
expect(fetchReposFn).not.toHaveBeenCalled();
});
it('renders intersection observer component', () => {
expect(wrapper.findComponent(GlIntersectionObserver).exists()).toBe(true);
});
it('calls fetchRepos when intersection observer appears', async () => {
wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
await nextTick();
expect(fetchReposFn).toHaveBeenCalled();
});
});
describe('with hasNextPage false', () => {
beforeEach(() => {
initState.pageInfo.hasNextPage = false;
createComponent({
state: initState,
paginatable: true,
@ -245,7 +219,30 @@ describe('ImportProjectsTable', () => {
});
it('does not render intersection observer component', () => {
expect(wrapper.findComponent(GlIntersectionObserver).exists()).toBe(false);
expect(findIntersectionObserver().exists()).toBe(false);
});
});
describe('with hasNextPage true', () => {
beforeEach(() => {
initState.pageInfo.hasNextPage = true;
createComponent({
state: initState,
paginatable: true,
});
});
it('renders intersection observer component', () => {
expect(findIntersectionObserver().exists()).toBe(true);
});
it('calls fetchRepos again when intersection observer appears', async () => {
findIntersectionObserver().vm.$emit('appear');
await nextTick();
expect(fetchReposFn).toHaveBeenCalledTimes(2);
});
});
});

View File

@ -17,6 +17,7 @@ import {
SET_PAGE,
SET_FILTER,
SET_PAGE_CURSORS,
SET_HAS_NEXT_PAGE,
} from '~/import_entities/import_projects/store/mutation_types';
import state from '~/import_entities/import_projects/store/state';
import axios from '~/lib/utils/axios_utils';
@ -143,6 +144,44 @@ describe('import_projects store actions', () => {
);
});
});
describe('when provider is BITBUCKET_SERVER', () => {
beforeEach(() => {
localState.provider = PROVIDERS.BITBUCKET_SERVER;
});
describe.each`
reposLength | expectedHasNextPage
${0} | ${false}
${12} | ${false}
${20} | ${false}
${25} | ${true}
`('when reposLength is $reposLength', ({ reposLength, expectedHasNextPage }) => {
beforeEach(() => {
payload.provider_repos = Array(reposLength).fill({});
mock.onGet(MOCK_ENDPOINT).reply(HTTP_STATUS_OK, payload);
});
it('commits SET_HAS_NEXT_PAGE', () => {
return testAction(
fetchRepos,
null,
localState,
[
{ type: REQUEST_REPOS },
{ type: SET_PAGE, payload: 1 },
{ type: SET_HAS_NEXT_PAGE, payload: expectedHasNextPage },
{
type: RECEIVE_REPOS_SUCCESS,
payload: convertObjectPropsToCamelCase(payload, { deep: true }),
},
],
[],
);
});
});
});
});
it('commits REQUEST_REPOS, RECEIVE_REPOS_ERROR mutations on an unsuccessful request', () => {

View File

@ -332,6 +332,16 @@ describe('import_projects store mutations', () => {
});
});
describe(`${types.SET_HAS_NEXT_PAGE}`, () => {
it('sets hasNextPage in pageInfo', () => {
const NEW_HAS_NEXT_PAGE = true;
state = { pageInfo: { hasNextPage: false } };
mutations[types.SET_HAS_NEXT_PAGE](state, NEW_HAS_NEXT_PAGE);
expect(state.pageInfo.hasNextPage).toBe(NEW_HAS_NEXT_PAGE);
});
});
describe(`${types.CANCEL_IMPORT_SUCCESS}`, () => {
const payload = { repoId: 1 };

View File

@ -289,3 +289,9 @@ export const mockStatefulSetsTableItems = [
kind: 'StatefulSet',
},
];
export const k8sReplicaSetsMock = [readyStatefulSet, readyStatefulSet, failedStatefulSet];
export const mockReplicaSetsTableItems = mockStatefulSetsTableItems.map((item) => {
return { ...item, kind: 'ReplicaSet' };
});

View File

@ -3,7 +3,13 @@ import { resolvers } from '~/kubernetes_dashboard/graphql/resolvers';
import k8sDashboardPodsQuery from '~/kubernetes_dashboard/graphql/queries/k8s_dashboard_pods.query.graphql';
import k8sDashboardDeploymentsQuery from '~/kubernetes_dashboard/graphql/queries/k8s_dashboard_deployments.query.graphql';
import k8sDashboardStatefulSetsQuery from '~/kubernetes_dashboard/graphql/queries/k8s_dashboard_stateful_sets.query.graphql';
import { k8sPodsMock, k8sDeploymentsMock, k8sStatefulSetsMock } from '../mock_data';
import k8sDashboardReplicaSetsQuery from '~/kubernetes_dashboard/graphql/queries/k8s_dashboard_replica_sets.query.graphql';
import {
k8sPodsMock,
k8sDeploymentsMock,
k8sStatefulSetsMock,
k8sReplicaSetsMock,
} from '../mock_data';
describe('~/frontend/environments/graphql/resolvers', () => {
let mockResolvers;
@ -276,4 +282,92 @@ describe('~/frontend/environments/graphql/resolvers', () => {
).rejects.toThrow('API error');
});
});
describe('k8sReplicaSets', () => {
const client = { writeQuery: jest.fn() };
const mockWatcher = WatchApi.prototype;
const mockReplicaSetsListWatcherFn = jest.fn().mockImplementation(() => {
return Promise.resolve(mockWatcher);
});
const mockOnDataFn = jest.fn().mockImplementation((eventName, callback) => {
if (eventName === 'data') {
callback([]);
}
});
const mockReplicaSetsListFn = jest.fn().mockImplementation(() => {
return Promise.resolve({
items: k8sReplicaSetsMock,
});
});
const mockAllReplicaSetsListFn = jest.fn().mockImplementation(mockReplicaSetsListFn);
describe('when the ReplicaSets data is present', () => {
beforeEach(() => {
jest
.spyOn(AppsV1Api.prototype, 'listAppsV1ReplicaSetForAllNamespaces')
.mockImplementation(mockAllReplicaSetsListFn);
jest
.spyOn(mockWatcher, 'subscribeToStream')
.mockImplementation(mockReplicaSetsListWatcherFn);
jest.spyOn(mockWatcher, 'on').mockImplementation(mockOnDataFn);
});
it('should request all ReplicaSets from the cluster_client library and watch the events', async () => {
const ReplicaSets = await mockResolvers.Query.k8sReplicaSets(
null,
{
configuration,
},
{ client },
);
expect(mockAllReplicaSetsListFn).toHaveBeenCalled();
expect(mockReplicaSetsListWatcherFn).toHaveBeenCalled();
expect(ReplicaSets).toEqual(k8sReplicaSetsMock);
});
it('should update cache with the new data when received from the library', async () => {
await mockResolvers.Query.k8sReplicaSets(
null,
{ configuration, namespace: '' },
{ client },
);
expect(client.writeQuery).toHaveBeenCalledWith({
query: k8sDashboardReplicaSetsQuery,
variables: { configuration, namespace: '' },
data: { k8sReplicaSets: [] },
});
});
});
it('should not watch ReplicaSets from the cluster_client library when the ReplicaSets data is not present', async () => {
jest.spyOn(AppsV1Api.prototype, 'listAppsV1ReplicaSetForAllNamespaces').mockImplementation(
jest.fn().mockImplementation(() => {
return Promise.resolve({
items: [],
});
}),
);
await mockResolvers.Query.k8sReplicaSets(null, { configuration }, { client });
expect(mockReplicaSetsListWatcherFn).not.toHaveBeenCalled();
});
it('should throw an error if the API call fails', async () => {
jest
.spyOn(AppsV1Api.prototype, 'listAppsV1ReplicaSetForAllNamespaces')
.mockRejectedValue(new Error('API error'));
await expect(
mockResolvers.Query.k8sReplicaSets(null, { configuration }, { client }),
).rejects.toThrow('API error');
});
});
});

View File

@ -0,0 +1,106 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import ReplicaSetsPage from '~/kubernetes_dashboard/pages/replica_sets_page.vue';
import WorkloadLayout from '~/kubernetes_dashboard/components/workload_layout.vue';
import { useFakeDate } from 'helpers/fake_date';
import {
k8sReplicaSetsMock,
mockStatefulSetsStats,
mockReplicaSetsTableItems,
} from '../graphql/mock_data';
Vue.use(VueApollo);
describe('Kubernetes dashboard replicaSets page', () => {
let wrapper;
const configuration = {
basePath: 'kas/tunnel/url',
baseOptions: {
headers: { 'GitLab-Agent-Id': '1' },
},
};
const findWorkloadLayout = () => wrapper.findComponent(WorkloadLayout);
const createApolloProvider = () => {
const mockResolvers = {
Query: {
k8sReplicaSets: jest.fn().mockReturnValue(k8sReplicaSetsMock),
},
};
return createMockApollo([], mockResolvers);
};
const createWrapper = (apolloProvider = createApolloProvider()) => {
wrapper = shallowMount(ReplicaSetsPage, {
provide: { configuration },
apolloProvider,
});
};
describe('mounted', () => {
it('renders WorkloadLayout component', () => {
createWrapper();
expect(findWorkloadLayout().exists()).toBe(true);
});
it('sets loading prop for the WorkloadLayout', () => {
createWrapper();
expect(findWorkloadLayout().props('loading')).toBe(true);
});
it('removes loading prop from the WorkloadLayout when the list of pods loaded', async () => {
createWrapper();
await waitForPromises();
expect(findWorkloadLayout().props('loading')).toBe(false);
});
});
describe('when gets pods data', () => {
useFakeDate(2023, 10, 23, 10, 10);
it('sets correct stats object for the WorkloadLayout', async () => {
createWrapper();
await waitForPromises();
expect(findWorkloadLayout().props('stats')).toEqual(mockStatefulSetsStats);
});
it('sets correct table items object for the WorkloadLayout', async () => {
createWrapper();
await waitForPromises();
expect(findWorkloadLayout().props('items')).toMatchObject(mockReplicaSetsTableItems);
});
});
describe('when gets an error from the cluster_client API', () => {
const error = new Error('Error from the cluster_client API');
const createErroredApolloProvider = () => {
const mockResolvers = {
Query: {
k8sReplicaSets: jest.fn().mockRejectedValueOnce(error),
},
};
return createMockApollo([], mockResolvers);
};
beforeEach(async () => {
createWrapper(createErroredApolloProvider());
await waitForPromises();
});
it('sets errorMessage prop for the WorkloadLayout', () => {
expect(findWorkloadLayout().props('errorMessage')).toBe(error.message);
});
});
});