From dc6331c5ed41106f2204d20be2ef8e11d11cc3f2 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Tue, 13 Aug 2024 06:12:34 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .../shared/work_item_link_child_contents.vue | 4 +- .../work_item_children_wrapper.vue | 19 ++- .../work_item_links/work_item_links.vue | 3 + .../work_item_links/work_item_links_form.vue | 18 ++- .../work_item_links/work_item_tree.vue | 3 + app/validators/json_schema_validator.rb | 31 +++- doc/api/graphql/reference/index.md | 29 ++++ doc/api/groups.md | 18 ++- doc/update/versions/gitlab_16_changes.md | 17 ++- .../container_scanning/index.md | 1 + .../dependency_scanning/index.md | 6 +- lib/api/entities/group.rb | 9 ++ .../namespace/root_storage_statistics.rb | 27 ++++ locale/gitlab.pot | 9 +- .../work_item_children_wrapper_spec.js | 48 +++++++ .../work_item_links_form_spec.js | 58 ++++---- spec/lib/api/entities/group_spec.rb | 68 +++++++-- .../namespace/root_storage_statistics_spec.rb | 28 ++++ spec/lib/gitlab/ci/config/entry/image_spec.rb | 9 +- spec/lib/gitlab/ci/config/entry/job_spec.rb | 42 ++++-- .../gitlab/ci/config/entry/service_spec.rb | 4 +- .../ci/create_pipeline_service/run_spec.rb | 2 +- spec/validators/json_schema_validator_spec.rb | 134 +++++++++++++++++- workhorse/go.mod | 2 +- workhorse/go.sum | 4 +- 25 files changed, 513 insertions(+), 80 deletions(-) create mode 100644 lib/api/entities/namespace/root_storage_statistics.rb create mode 100644 spec/lib/api/entities/namespace/root_storage_statistics_spec.rb diff --git a/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue b/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue index 346a8836b8a..97e857df29d 100644 --- a/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue +++ b/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue @@ -158,9 +158,7 @@ export default {
-
+
findHierarchyWidgets(child.widgets) || {}) .some((hierarchy) => hierarchy.hasChildren); }, + disableList() { + return this.disableContent || this.updateInProgress; + }, }, methods: { async removeChild(child) { @@ -121,6 +130,7 @@ export default { }, async undoChildRemoval(child) { try { + this.updateInProgress = true; const { data } = await this.$apollo.mutate({ mutation: updateWorkItemMutation, variables: { input: { id: child.id, hierarchyWidget: { parentId: this.workItemId } } }, @@ -140,6 +150,8 @@ export default { } catch (error) { this.$emit('error', s__('WorkItem|Something went wrong while undoing child removal.')); Sentry.captureException(error); + } finally { + this.updateInProgress = false; } }, addWorkItemQuery({ iid }) { @@ -210,6 +222,7 @@ export default { const updatedChildren = this.children.slice(); updatedChildren.splice(oldIndex, 1); updatedChildren.splice(newIndex, 0, targetItem); + this.updateInProgress = true; this.$apollo .mutate({ @@ -255,6 +268,9 @@ export default { .catch((error) => { this.$emit('error', error.message); Sentry.captureException(error); + }) + .finally(() => { + this.updateInProgress = false; }); }, }, @@ -266,7 +282,8 @@ export default { :is="treeRootWrapper" v-bind="treeRootOptions" class="content-list" - :class="{ 'gl-cursor-grab sortable-container': canReorder }" + data-testid="child-items-container" + :class="{ 'gl-cursor-grab sortable-container': canReorder, 'disabled-content': disableList }" @end="handleDragOnEnd" > diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue index 95a6522115c..003201e4b95 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue @@ -278,8 +278,12 @@ export default { this.error = null; this.isInputValid = true; }, + markFormSubmitInProgress(value) { + this.submitInProgress = value; + this.$emit('update-in-progress', this.submitInProgress); + }, addChild() { - this.submitInProgress = true; + this.markFormSubmitInProgress(true); this.$apollo .mutate({ mutation: updateWorkItemHierarchyMutation, @@ -293,6 +297,9 @@ export default { }, }) .then(({ data }) => { + // Marking submitInProgress cannot be in finally block + // as the form may get close before the event is emitted + this.markFormSubmitInProgress(false); if (data.workItemUpdate?.errors?.length) { [this.error] = data.workItemUpdate.errors; } else { @@ -304,17 +311,17 @@ export default { .catch(() => { this.error = this.$options.i18n.addChildErrorMessage; this.isInputValid = false; + this.markFormSubmitInProgress(false); }) .finally(() => { this.search = ''; - this.submitInProgress = false; }); }, createChild() { if (!this.canSubmitForm) { return; } - this.submitInProgress = true; + this.markFormSubmitInProgress(true); this.$apollo .mutate({ mutation: createWorkItemMutation, @@ -329,6 +336,9 @@ export default { }), }) .then(({ data }) => { + // Marking submitInProgress cannot be in finally block + // as the form may get close before the event is emitted + this.markFormSubmitInProgress(false); if (data.workItemCreate?.errors?.length) { [this.error] = data.workItemCreate.errors; } else { @@ -340,11 +350,11 @@ export default { .catch(() => { this.error = this.$options.i18n.createChildErrorMessage; this.isInputValid = false; + this.markFormSubmitInProgress(false); }) .finally(() => { this.search = ''; this.childToCreateTitle = null; - this.submitInProgress = false; }); }, closeForm() { diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue index a7e11b1b13a..f83be7f4415 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue @@ -106,6 +106,7 @@ export default { showLabels: true, fetchNextPageInProgress: false, workItem: null, + disableContent: false, }; }, apollo: { @@ -324,6 +325,7 @@ export default { :parent-confidential="confidential" @cancel="hideAddForm" @addChild="$emit('addChild')" + @update-in-progress="disableContent = $event" /> diff --git a/app/validators/json_schema_validator.rb b/app/validators/json_schema_validator.rb index 7dce1bbbf16..8bfeb45058b 100644 --- a/app/validators/json_schema_validator.rb +++ b/app/validators/json_schema_validator.rb @@ -30,10 +30,7 @@ class JsonSchemaValidator < ActiveModel::EachValidator if options[:detail_errors] validator.validate(value).each do |error| - message = format( - _("'%{data_pointer}' must be a valid '%{type}'"), - data_pointer: error['data_pointer'], type: error['type'] - ) + message = format_error_message(error) record.errors.add(attribute, message) end else @@ -45,6 +42,32 @@ class JsonSchemaValidator < ActiveModel::EachValidator attr_reader :base_directory + def format_error_message(error) + case error['type'] + when 'oneOf' + format_one_of_error(error) + else + error['error'] + end + end + + def format_one_of_error(error) + schema_options = error['schema']['oneOf'] + required_props = schema_options.flat_map { |option| option['required'] }.uniq + + message = if error['root_schema']['type'] == 'array' + _("value at %{data_pointer} should use only one of: %{requirements}") + else + _("should use only one of: %{requirements}") + end + + format( + message, + requirements: required_props.join(', '), + data_pointer: error['data_pointer'] + ) + end + def valid_schema?(value) validator.valid?(value) end diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 7a197ec26e4..42cd9877e57 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -19021,6 +19021,7 @@ GitLab CI/CD configuration template. | `id` | [`ID!`](#id) | ID of the cluster agent. | | `name` | [`String`](#string) | Name of the cluster agent. | | `project` | [`Project`](#project) | Project this cluster agent is associated with. | +| `remoteDevelopmentAgentConfig` | [`RemoteDevelopmentAgentConfig`](#remotedevelopmentagentconfig) | Remote development agent config for the cluster agent. | | `tokens` | [`ClusterAgentTokenConnection`](#clusteragenttokenconnection) | Tokens associated with the cluster agent. (see [Connections](#connections)) | | `updatedAt` | [`Time`](#time) | Timestamp the cluster agent was updated. | | `userAccessAuthorizations` | [`ClusterAgentAuthorizationUserAccess`](#clusteragentauthorizationuseraccess) | User access config for the cluster agent. | @@ -31135,6 +31136,28 @@ Represents the source code attached to a release in a particular format. | `format` | [`String`](#string) | Format of the source. | | `url` | [`String`](#string) | Download URL of the source. | +### `RemoteDevelopmentAgentConfig` + +Represents a remote development agent configuration. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clusterAgent` | [`ClusterAgent!`](#clusteragent) | Cluster agent that the remote development agent config belongs to. | +| `createdAt` | [`Time!`](#time) | Timestamp of when the remote development agent config was created. | +| `defaultMaxHoursBeforeTermination` | [`Int!`](#int) | Default max hours before worksapce termination of the remote development agent config. | +| `dnsZone` | [`String!`](#string) | DNS zone where workspaces are available. | +| `enabled` | [`Boolean!`](#boolean) | Indicates whether remote development is enabled for the GitLab agent. | +| `gitlabWorkspacesProxyNamespace` | [`String!`](#string) | Namespace where gitlab-workspaces-proxy is installed. | +| `id` | [`RemoteDevelopmentRemoteDevelopmentAgentConfigID!`](#remotedevelopmentremotedevelopmentagentconfigid) | Global ID of the remote development agent config. | +| `maxHoursBeforeTerminationLimit` | [`Int!`](#int) | Max hours before worksapce termination limit of the remote development agent config. | +| `networkPolicyEnabled` | [`Boolean!`](#boolean) | Whether the network policy of the remote development agent config is enabled. | +| `projectId` | [`ID`](#id) | ID of the project that the remote development agent config belongs to. | +| `updatedAt` | [`Time!`](#time) | Timestamp of the last update to any mutable remote development agent config property. | +| `workspacesPerUserQuota` | [`Int!`](#int) | Maximum number of workspaces per user. | +| `workspacesQuota` | [`Int!`](#int) | Maximum number of workspaces for the GitLab agent. | + ### `Repository` #### Fields @@ -38683,6 +38706,12 @@ A `ReleasesLinkID` is a global ID. It is encoded as a string. An example `ReleasesLinkID` is: `"gid://gitlab/Releases::Link/1"`. +### `RemoteDevelopmentRemoteDevelopmentAgentConfigID` + +A `RemoteDevelopmentRemoteDevelopmentAgentConfigID` is a global ID. It is encoded as a string. + +An example `RemoteDevelopmentRemoteDevelopmentAgentConfigID` is: `"gid://gitlab/RemoteDevelopment::RemoteDevelopmentAgentConfig/1"`. + ### `RemoteDevelopmentWorkspaceID` A `RemoteDevelopmentWorkspaceID` is a global ID. It is encoded as a string. diff --git a/doc/api/groups.md b/doc/api/groups.md index 5fa9f1e601f..911f1e6b178 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -35,7 +35,7 @@ Parameters: | `search` | string | no | Return the list of authorized groups matching the search criteria | | `order_by` | string | no | Order groups by `name`, `path`, `id`, or `similarity`. Default is `name` | | `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` | -| `statistics` | boolean | no | Include group statistics (administrators only).
*Note:* The REST API response does not provide the full `RootStorageStatistics` data that is shown in the UI. To match the data in the UI, use GraphQL instead of REST. For more information, see the [Group GraphQL API resources](../api/graphql/reference/index.md#group).| +| `statistics` | boolean | no | Include group statistics (administrators only).
*Note:* For top-level groups, the response returns the full `root_storage_statistics` data displayed in the UI. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/469254) in GitLab 17.3. | | `visibility` | string | no | Limit to groups with `public`, `internal`, or `private` visibility. | | `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (administrators only) | | `owned` | boolean | no | Limit to groups explicitly owned by the current user | @@ -95,7 +95,7 @@ GET /groups ] ``` -When adding the parameter `statistics=true` and the authenticated user is an administrator, additional group statistics are returned. +When adding the parameter `statistics=true` and the authenticated user is an administrator, additional group statistics are returned. For top-level groups, `root_storage_statistics` are added as well. ```plaintext GET /groups?statistics=true @@ -154,6 +154,20 @@ GET /groups?statistics=true "snippets_size": 50, "uploads_size": 0 }, + "root_storage_statistics": { + "build_artifacts_size": 0, + "container_registry_size": 0, + "container_registry_size_is_estimated": false, + "dependency_proxy_size": 0, + "lfs_objects_size": 0, + "packages_size": 0, + "pipeline_artifacts_size": 0, + "repository_size": 0, + "snippets_size": 0, + "storage_size": 0, + "uploads_size": 0, + "wiki_size": 0 + }, "wiki_access_level": "private", "duo_features_enabled": true, "lock_duo_features_enabled": false, diff --git a/doc/update/versions/gitlab_16_changes.md b/doc/update/versions/gitlab_16_changes.md index 8872e79e6ac..17cd68f34c5 100644 --- a/doc/update/versions/gitlab_16_changes.md +++ b/doc/update/versions/gitlab_16_changes.md @@ -86,9 +86,24 @@ For more information about upgrading GitLab Helm Chart, see [the release notes f ## 16.11.0 +- A [`groups_direct` field was added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/146881) + to the [JSON web token (ID token)](../../ci/secrets/id_token_authentication.md). + - If you use GitLab CI/CD ID tokens to authenticate with third party services, + this change can cause the HTTP header size to increase. Proxy servers might reject + the request if the headers get too big. + - If possible, increase the header limit on the receiving system. + - See [issue 467253](https://gitlab.com/gitlab-org/gitlab/-/issues/467253) for more details. +- After upgrading to GitLab 16.11 some users with large environments and databases experience + timeouts loading source code pages in the web UI. + - These timeouts are caused by slow PostgreSQL queries for pipeline data, which then + exceed the internal 60 second timeout. + - You can still clone Git repositories, and other requests for repository data works. + - See [issue 472420](https://gitlab.com/gitlab-org/gitlab/-/issues/472420) for more details, + including steps to confirm you're affected and housekeeping to run in PostgreSQL to correct it. + ### Linux package installations -In GitLab 16.11, PostgreSQL will automatically be upgraded to 14.x except for the following cases: +In GitLab 16.11, PostgreSQL is automatically upgraded to 14.x except for the following cases: - You are running the database in high availability using Patroni. - Your database nodes are part of a GitLab Geo configuration. diff --git a/doc/user/application_security/container_scanning/index.md b/doc/user/application_security/container_scanning/index.md index c2f93cfa817..8803a73345c 100644 --- a/doc/user/application_security/container_scanning/index.md +++ b/doc/user/application_security/container_scanning/index.md @@ -716,6 +716,7 @@ Container Scanning for Registry populates the Vulnerability Report only when a n - You must have at least the Maintainer role in a project to enable Container Scanning for Registry. - The project being used must not be empty. If you are utilizing an empty project solely for storing container images, this feature won't function as intended. As a workaround, ensure the project contains an initial commit on the default branch. - By default there is a limit of `50` scans per project per day. +- You must [configure container registry notifications](../../../administration/packages/container_registry.md#configure-container-registry-notifications). ### Enabling Container Scanning for Registry diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md index 7be73e53d3f..5a3e60723e0 100644 --- a/doc/user/application_security/dependency_scanning/index.md +++ b/doc/user/application_security/dependency_scanning/index.md @@ -709,7 +709,7 @@ To enable the analyzer, either: - Create a [scan execution policy](../policies/scan_execution_policies.md) that enforces dependency scanning. - Edit the `.gitlab-ci.yml` file manually. -- [Use CI/CD components](#use-cicd-components) (Android projects only) +- [Use CI/CD components](#use-cicd-components) #### Use a preconfigured merge request @@ -771,9 +771,9 @@ Pipelines now include a Dependency Scanning job. Use [CI/CD components](../../../ci/components/index.md) to perform Dependency Scanning of your application. For instructions, see the respective component's README file. -##### Available CI/CD components per language and package manager +##### Available CI/CD components -- [Android applications](https://gitlab.com/explore/catalog/components/android-dependency-scanning) +See ### Running jobs in merge request pipelines diff --git a/lib/api/entities/group.rb b/lib/api/entities/group.rb index e65f26825b7..4ddee0dbd9e 100644 --- a/lib/api/entities/group.rb +++ b/lib/api/entities/group.rb @@ -44,6 +44,15 @@ module API expose :uploads_size end end + + expose :root_storage_statistics, using: Entities::Namespace::RootStorageStatistics, + if: ->(group, opts) { + expose_root_storage_statistics?(group, opts) + } + + def expose_root_storage_statistics?(group, opts) + opts[:statistics] && group.root? + end end end end diff --git a/lib/api/entities/namespace/root_storage_statistics.rb b/lib/api/entities/namespace/root_storage_statistics.rb new file mode 100644 index 00000000000..c269e26f594 --- /dev/null +++ b/lib/api/entities/namespace/root_storage_statistics.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module API + module Entities + class Namespace + class RootStorageStatistics < Grape::Entity + expose :build_artifacts_size, documentation: { type: 'integer', desc: 'CI artifacts size in bytes.' } + expose :container_registry_size, documentation: { type: 'integer', desc: 'Container Registry size in bytes.' } + expose :registry_size_estimated, + as: :container_registry_size_is_estimated, + documentation: { type: 'boolean', + desc: 'Indicates whether the deduplicated Container Registry size for ' \ + 'the namespace is an estimated value or not.' } + expose :dependency_proxy_size, documentation: { type: 'integer', desc: 'Dependency Proxy sizes in bytes.' } + expose :lfs_objects_size, documentation: { type: 'integer', desc: 'LFS objects size in bytes.' } + expose :packages_size, documentation: { type: 'integer', desc: 'Packages size in bytes.' } + expose :pipeline_artifacts_size, + documentation: { type: 'integer', desc: 'CI pipeline artifacts size in bytes.' } + expose :repository_size, documentation: { type: 'integer', desc: 'Git repository size in bytes.' } + expose :snippets_size, documentation: { type: 'integer', desc: 'Snippets size in bytes.' } + expose :storage_size, documentation: { type: 'integer', desc: 'Total storage in bytes.' } + expose :uploads_size, documentation: { type: 'integer', desc: 'Uploads size in bytes.' } + expose :wiki_size, documentation: { type: 'integer', desc: 'Wiki size in bytes.' } + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 373aceee5d9..557467e158e 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1533,9 +1533,6 @@ msgstr "" msgid "%{wildcards_link_start}Wildcards%{wildcards_link_end} such as %{code_tag_start}v*%{code_tag_end} or %{code_tag_start}*-release%{code_tag_end} are supported." msgstr "" -msgid "'%{data_pointer}' must be a valid '%{type}'" -msgstr "" - msgid "'%{group_name}' has been scheduled for removal on %{removal_time}." msgstr "" @@ -64528,6 +64525,9 @@ msgstr "" msgid "should have length between 16 to 24 characters." msgstr "" +msgid "should use only one of: %{requirements}" +msgstr "" + msgid "show %{count} more" msgstr "" @@ -64707,6 +64707,9 @@ msgstr "" msgid "v%{version} published %{timeAgo}" msgstr "" +msgid "value at %{data_pointer} should use only one of: %{requirements}" +msgstr "" + msgid "value for '%{storage}' must be an integer" msgstr "" diff --git a/spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js index 0147b199040..a6e5b6a2fdd 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js @@ -1,9 +1,11 @@ import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; +import Draggable from 'vuedraggable'; import createMockApollo from 'helpers/mock_apollo_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; +import { isLoggedIn } from '~/lib/utils/common_utils'; import WorkItemChildrenWrapper from '~/work_items/components/work_item_links/work_item_children_wrapper.vue'; import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; @@ -18,6 +20,8 @@ import { workItemByIidResponseFactory, } from '../../mock_data'; +jest.mock('~/lib/utils/common_utils'); + describe('WorkItemChildrenWrapper', () => { let wrapper; @@ -30,6 +34,8 @@ describe('WorkItemChildrenWrapper', () => { .mockResolvedValue(changeWorkItemParentMutationResponse); const findWorkItemLinkChildItems = () => wrapper.findAllComponents(WorkItemLinkChild); + const findDraggable = () => wrapper.findComponent(Draggable); + const findChildItemsContainer = () => wrapper.findByTestId('child-items-container'); Vue.use(VueApollo); @@ -38,6 +44,8 @@ describe('WorkItemChildrenWrapper', () => { confidential = false, children = childrenWorkItems, mutationHandler = updateWorkItemMutationHandler, + disableContent = false, + canUpdate = false, } = {}) => { const mockApollo = createMockApollo([ [workItemByIidQuery, getWorkItemQueryHandler], @@ -62,6 +70,8 @@ describe('WorkItemChildrenWrapper', () => { workItemIid: '1', confidential, children, + disableContent, + canUpdate, }, mocks: { $toast, @@ -114,6 +124,44 @@ describe('WorkItemChildrenWrapper', () => { }, ); + it('does not render draggable component when user is not logged in', () => { + createComponent({ canUpdate: true }); + + expect(findDraggable().exists()).toBe(false); + }); + + it('disables list when `disableContent` is true', () => { + createComponent({ disableContent: true }); + + expect(findChildItemsContainer().classes('disabled-content')).toBe(true); + }); + + describe('when user is logged in', () => { + beforeEach(() => { + isLoggedIn.mockReturnValue(true); + }); + + it('renders draggable component without disabling the list', () => { + createComponent({ canUpdate: true }); + + expect(findDraggable().exists()).toBe(true); + expect(findDraggable().classes('disabled-content')).toBe(false); + }); + + it('does not render draggable component when user has no permission', () => { + createComponent({ canUpdate: false }); + + expect(findDraggable().exists()).toBe(false); + }); + + it('disables the list when `disableContent` is true', () => { + createComponent({ disableContent: true, canUpdate: true }); + + expect(findDraggable().exists()).toBe(true); + expect(findDraggable().classes('disabled-content')).toBe(true); + }); + }); + describe('when removing child work item', () => { const workItem = { id: 'gid://gitlab/WorkItem/2' }; diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js index 57b7175d307..ddfb9b5b5dc 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js @@ -152,6 +152,19 @@ describe('WorkItemLinksForm', () => { ); describe('creating a new work item', () => { + const submitForm = ({ title, fullPath }) => { + findInput().vm.$emit('input', title); + + if (fullPath) { + findProjectSelector().vm.$emit('selectProject', fullPath); + } + + // Trigger form submission + findForm().vm.$emit('submit', { + preventDefault: jest.fn(), + }); + }; + describe('for project level work items', () => { beforeEach(async () => { await createComponent(); @@ -171,11 +184,10 @@ describe('WorkItemLinksForm', () => { expect(findFormGroup().props('invalidFeedback')).toBe(null); expect(findInput().props('state')).toBe(true); - findInput().vm.$emit('input', 'Create task test'); - // Trigger form submission - findForm().vm.$emit('submit', { - preventDefault: jest.fn(), - }); + submitForm({ title: 'Create task test' }); + + expect(wrapper.emitted('update-in-progress')).toEqual([[true]]); + await waitForPromises(); expect(findFormGroup().props('state')).toBe(false); @@ -183,14 +195,14 @@ describe('WorkItemLinksForm', () => { 'Something went wrong when trying to create a child. Please try again.', ); expect(findInput().props('state')).toBe(false); + expect(wrapper.emitted('update-in-progress')[1]).toEqual([false]); }); it('creates child task in non confidential parent and closes the form', async () => { - findInput().vm.$emit('input', 'Create task test'); + submitForm({ title: 'Create task test' }); + + expect(wrapper.emitted('update-in-progress')).toEqual([[true]]); - findForm().vm.$emit('submit', { - preventDefault: jest.fn(), - }); await waitForPromises(); expect(createMutationResolver).toHaveBeenCalledWith({ @@ -206,17 +218,18 @@ describe('WorkItemLinksForm', () => { }); expect(wrapper.emitted('addChild')).toEqual([[]]); expect(wrapper.emitted('cancel')).toEqual([[]]); + expect(wrapper.emitted('update-in-progress')[1]).toEqual([false]); }); it('creates child task in confidential parent', async () => { await createComponent({ parentConfidential: true }); - findInput().vm.$emit('input', 'Create confidential task'); + submitForm({ title: 'Create confidential task' }); + + expect(wrapper.emitted('update-in-progress')).toEqual([[true]]); - findForm().vm.$emit('submit', { - preventDefault: jest.fn(), - }); await waitForPromises(); + expect(wrapper.vm.childWorkItemType).toEqual(workItemTypeIdForTask); expect(createMutationResolver).toHaveBeenCalledWith({ input: { @@ -229,6 +242,7 @@ describe('WorkItemLinksForm', () => { confidential: true, }, }); + expect(wrapper.emitted('update-in-progress')[1]).toEqual([false]); }); }); @@ -250,13 +264,9 @@ describe('WorkItemLinksForm', () => { }); it('creates child issue in non confidential parent and closes the form', async () => { - findInput().vm.$emit('input', 'Create issue test'); + submitForm({ title: 'Create issue test', fullPath: projectData[0].fullPath }); - findProjectSelector().vm.$emit('selectProject', projectData[0].fullPath); - - findForm().vm.$emit('submit', { - preventDefault: jest.fn(), - }); + expect(wrapper.emitted('update-in-progress')).toEqual([[true]]); await waitForPromises(); @@ -272,6 +282,7 @@ describe('WorkItemLinksForm', () => { }, }); expect(wrapper.emitted('addChild')).toEqual([[]]); + expect(wrapper.emitted('update-in-progress')[1]).toEqual([false]); expect(wrapper.emitted('cancel')).toEqual([[]]); }); @@ -283,13 +294,9 @@ describe('WorkItemLinksForm', () => { childrenType: WORK_ITEM_TYPE_ENUM_ISSUE, }); - findInput().vm.$emit('input', 'Create confidential issue'); + submitForm({ title: 'Create confidential issue', fullPath: projectData[0].fullPath }); - findProjectSelector().vm.$emit('selectProject', projectData[0].fullPath); - - findForm().vm.$emit('submit', { - preventDefault: jest.fn(), - }); + expect(wrapper.emitted('update-in-progress')).toEqual([[true]]); await waitForPromises(); @@ -304,6 +311,7 @@ describe('WorkItemLinksForm', () => { confidential: true, }, }); + expect(wrapper.emitted('update-in-progress')[1]).toEqual([false]); }); }); diff --git a/spec/lib/api/entities/group_spec.rb b/spec/lib/api/entities/group_spec.rb index d12e97d3009..a541630d676 100644 --- a/spec/lib/api/entities/group_spec.rb +++ b/spec/lib/api/entities/group_spec.rb @@ -4,21 +4,67 @@ require 'spec_helper' RSpec.describe API::Entities::Group, feature_category: :groups_and_projects do let_it_be(:group) do - base_group = create(:group) { |g| create(:project_statistics, namespace_id: g.id) } + base_group = create(:group) do |g| + create(:project_statistics, namespace_id: g.id) + create(:namespace_root_storage_statistics, namespace_id: g.id) + end Group.with_statistics.find(base_group.id) end - subject(:json) { described_class.new(group, { with_custom_attributes: true, statistics: true }).as_json } + subject(:json) { described_class.new(group, options).as_json } - it 'returns expected data' do - expect(json.keys).to( - include( - :organization_id, :path, :description, :visibility, :share_with_group_lock, :require_two_factor_authentication, - :two_factor_grace_period, :project_creation_level, :auto_devops_enabled, - :subgroup_creation_level, :emails_disabled, :emails_enabled, :lfs_enabled, :default_branch_protection, - :default_branch_protection_defaults, :avatar_url, :request_access_enabled, :full_name, :full_path, :created_at, - :parent_id, :organization_id, :shared_runners_setting, :custom_attributes, :statistics, :default_branch + context 'with statistics' do + let(:options) { { with_custom_attributes: true, statistics: true } } + + it 'returns expected data' do + expect(json.keys).to( + include( + :organization_id, :path, :description, :visibility, :share_with_group_lock, + :require_two_factor_authentication, :two_factor_grace_period, :project_creation_level, :auto_devops_enabled, + :subgroup_creation_level, :emails_disabled, :emails_enabled, :lfs_enabled, :default_branch_protection, + :default_branch_protection_defaults, :avatar_url, :request_access_enabled, :full_name, :full_path, + :created_at, :parent_id, :organization_id, :shared_runners_setting, :custom_attributes, :statistics, + :default_branch, :root_storage_statistics + ) ) - ) + end + + context 'on a sub-group' do + let(:subgroup) do + subgroup = create(:group, parent: group, path: "#{group.path}-subgroup") do |g| + create(:project_statistics, namespace_id: g.id) + end + Group.with_statistics.find(subgroup.id) + end + + subject(:json) { described_class.new(subgroup, options).as_json } + + it 'does not expose root storage statistics' do + expect(json.keys).not_to(include(:root_storage_statistics)) + end + end + + context 'on a group without root storage statistics' do + let(:group_without_root_storage_statistics) do + base_group = create(:group) do |g| + create(:project_statistics, namespace_id: g.id) + end + Group.with_statistics.find(base_group.id) + end + + subject(:json) { described_class.new(group_without_root_storage_statistics, options).as_json } + + it 'returns nil for root storage statistics' do + expect(json[:root_storage_statistics]).to be_nil + end + end + end + + context 'without statistics' do + let(:options) { { with_custom_attributes: true, statistics: false } } + + it 'does not expose statistics' do + expect(json.keys).not_to(include(:statistics, :root_storage_statistics)) + end end end diff --git a/spec/lib/api/entities/namespace/root_storage_statistics_spec.rb b/spec/lib/api/entities/namespace/root_storage_statistics_spec.rb new file mode 100644 index 00000000000..9ce1d3a0794 --- /dev/null +++ b/spec/lib/api/entities/namespace/root_storage_statistics_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Entities::Namespace::RootStorageStatistics, feature_category: :groups_and_projects do + let(:root_storage_statistics) { create(:namespace_root_storage_statistics) } + + subject(:entity) { described_class.new(root_storage_statistics).as_json } + + it 'exposes correct attributes' do + expect(entity.keys).to( + include( + :build_artifacts_size, + :container_registry_size, + :container_registry_size_is_estimated, + :dependency_proxy_size, + :lfs_objects_size, + :packages_size, + :pipeline_artifacts_size, + :repository_size, + :snippets_size, + :storage_size, + :uploads_size, + :wiki_size + ) + ) + end +end diff --git a/spec/lib/gitlab/ci/config/entry/image_spec.rb b/spec/lib/gitlab/ci/config/entry/image_spec.rb index 0a82010c20c..acbd9a22c2e 100644 --- a/spec/lib/gitlab/ci/config/entry/image_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/image_spec.rb @@ -136,7 +136,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Image do it 'raises an error' do expect(entry).not_to be_valid expect(entry.errors.first) - .to match %r{image executor opts '/docker/platform' must be a valid 'string'} + .to match %r{image executor opts value at `/docker/platform` is not a string} end end end @@ -184,7 +184,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Image do it 'raises an error' do expect(entry).not_to be_valid expect(entry.errors.first) - .to match %r{image executor opts '/docker/user' must be a valid 'string'} + .to match %r{image executor opts value at `/docker/user` is not a string} end end end @@ -194,8 +194,9 @@ RSpec.describe Gitlab::Ci::Config::Entry::Image do it 'is not valid' do expect(entry).not_to be_valid - expect(entry.errors.first) - .to match %r{image executor opts '/docker/unknown_key' must be a valid 'schema'} + expect(entry.errors.first).to match( + %r{image executor opts object property at `/docker/unknown_key` is a disallowed additional property} + ) end end end diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb index e28e01ce7f5..730922ac08f 100644 --- a/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -270,14 +270,34 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job, feature_category: :pipeline_compo using RSpec::Parameterized::TableSyntax where(:case_name, :config, :error) do - 'when only step is used without name' | { stage: 'build', -run: [{ step: 'some reference' }] } | 'job run \'/0\' must be a valid \'required\'' - 'when only script is used without name' | { stage: 'build', -run: [{ script: 'echo' }] } | 'job run \'/0\' must be a valid \'required\'' - 'when step and script are used together' | { stage: 'build', -run: [{ name: 'step1', step: 'some reference', script: 'echo' }] } | 'job run \'/0\' must be a valid \'oneof\'' - 'when a subkey does not exist' | { stage: 'build', -run: [{ name: 'step1', invalid_key: 'some value' }] } | 'job run \'/0\' must be a valid \'required\'' + 'when only step is used without name' | { + stage: 'build', + run: [{ step: 'some reference' }] + } | 'job run object at `/0` is missing required properties: name' + + 'when only script is used without name' | { + stage: 'build', + run: [{ script: 'echo' }] + } | 'job run object at `/0` is missing required properties: name' + + 'when step and script are used together' | { + stage: 'build', + run: [{ + name: 'step1', + step: 'some reference', + script: 'echo' + }] + } | 'job run value at /0 should use only one of: step, script' + + 'when a required subkey is missing' | { + stage: 'build', + run: [{ name: 'step1' }] + } | 'job run object at `/0` is missing required properties: step' + + 'when a subkey is invalid' | { + stage: 'build', + run: [{ name: 'step1', step: 'some step', invalid_key: 'some value' }] + } | 'job run object property at `/0/invalid_key` is a disallowed additional property' end with_them do @@ -292,7 +312,7 @@ run: [{ name: 'step1', invalid_key: 'some value' }] } | 'job run \'/0\' must be it 'returns error about invalid run' do expect(entry).not_to be_valid - expect(entry.errors).to include 'job run \'\' must be a valid \'array\'' + expect(entry.errors).to include 'job run value at root is not an array' end end @@ -312,7 +332,7 @@ run: [{ name: 'step1', invalid_key: 'some value' }] } | 'job run \'/0\' must be it 'returns error about invalid env' do expect(entry).not_to be_valid - expect(entry.errors).to include 'job run \'/0/env/my_var\' must be a valid \'string\'' + expect(entry.errors).to include 'job run value at `/0/env/my_var` is not a string' end end @@ -321,7 +341,7 @@ run: [{ name: 'step1', invalid_key: 'some value' }] } | 'job run \'/0\' must be it 'returns error about invalid run' do expect(entry).not_to be_valid - expect(entry.errors).to include 'job run \'/0\' must be a valid \'required\'' + expect(entry.errors).to include 'job run object at `/0` is missing required properties: step' end end end diff --git a/spec/lib/gitlab/ci/config/entry/service_spec.rb b/spec/lib/gitlab/ci/config/entry/service_spec.rb index 9f4dc3becfd..841d595c8da 100644 --- a/spec/lib/gitlab/ci/config/entry/service_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/service_spec.rb @@ -204,7 +204,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Service do it 'is not valid' do expect(entry).not_to be_valid expect(entry.errors.first) - .to match %r{service executor opts '/docker/invalid' must be a valid 'schema'} + .to match %r{service executor opts object property at `/docker/invalid` is a disallowed additional property} end end end @@ -216,7 +216,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Service do it 'is not valid' do expect(entry).not_to be_valid expect(entry.errors.first) - .to match %r{service executor opts '/docker/platform' must be a valid 'string'} + .to match %r{service executor opts value at `/docker/platform` is not a string} end end end diff --git a/spec/services/ci/create_pipeline_service/run_spec.rb b/spec/services/ci/create_pipeline_service/run_spec.rb index 6c3f94e7775..32162a31da7 100644 --- a/spec/services/ci/create_pipeline_service/run_spec.rb +++ b/spec/services/ci/create_pipeline_service/run_spec.rb @@ -137,7 +137,7 @@ RSpec.describe Ci::CreatePipelineService, :ci_config_feature_flag_correctness, it 'returns errors for invalid configuration' do expect(pipeline).not_to be_created_successfully expect(pipeline.errors.full_messages).to include( - "jobs:job run '/0' must be a valid 'required'" + "jobs:job run object at `/0` is missing required properties: name" ) end end diff --git a/spec/validators/json_schema_validator_spec.rb b/spec/validators/json_schema_validator_spec.rb index f3a22892407..212bfa31c94 100644 --- a/spec/validators/json_schema_validator_spec.rb +++ b/spec/validators/json_schema_validator_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe JsonSchemaValidator do +RSpec.describe JsonSchemaValidator, feature_category: :shared do describe '#validates_each' do let(:build_report_result) { build(:ci_build_report_result, :with_junit_success) } @@ -78,10 +78,140 @@ RSpec.describe JsonSchemaValidator do expect(build_report_result.errors.size).to eq(1) expect(build_report_result.errors.full_messages).to match_array( - ["Data '/invalid' must be a valid 'schema'"] + ["Data object property at `/invalid` is a disallowed additional property"] ) end end end + + context 'when validating config with oneOf JSON schema' do + let(:config) do + { + run: [ + { + name: 'hello_steps', + step: 'gitlab.com/gitlab-org/ci-cd/runner-tools/echo-step', + inputs: { + echo: 'hello steps!' + } + } + ] + } + end + + let(:job) { Gitlab::Ci::Config::Entry::Job.new(config, name: :rspec) } + let(:errors) { ActiveModel::Errors.new(job) } + + let(:validator) do + described_class.new( + attributes: [:run], + base_directory: 'app/validators/json_schemas', + filename: 'run_steps', + hash_conversion: true, + detail_errors: true + ) + end + + before do + job.compose! + allow(job).to receive(:errors).and_return(errors) + end + + subject { validator.validate(job) } + + context 'when the value is a valid array of hashes' do + before do + allow(job).to receive(:read_attribute_for_validation).and_return(config[:run]) + end + + it 'returns no errors' do + subject + + expect(job.errors).to be_empty + end + end + + context 'when a required property is missing' do + before do + config[:run] = [{ name: 'hello_steps' }] + allow(job).to receive(:read_attribute_for_validation).and_return(config[:run]) + end + + it 'returns an error message' do + subject + + expect(job.errors).not_to be_empty + expect("#{job.errors.first.attribute} #{job.errors.first.type}").to eq("run object at `/0` is missing required properties: step") + end + end + + context 'when oneOf validation fails' do + before do + config[:run] = [nil] + allow(job).to receive(:read_attribute_for_validation).and_return(config[:run]) + end + + it 'returns an error message' do + subject + + expect(job.errors).not_to be_empty + expect("#{job.errors.first.attribute} #{job.errors.first.type}").to eq( + "run value at /0 should use only one of: step, script" + ) + end + end + + context 'when there is a general validation error' do + before do + config[:run] = 'not an array' + allow(job).to receive(:read_attribute_for_validation).and_return(config[:run]) + end + + it 'returns an error message' do + subject + + expect(job.errors).not_to be_empty + expect("#{job.errors.first.attribute} #{job.errors.first.type}").to eq("run value at root is not an array") + end + end + + context 'when a non-array value violates oneOf constraint' do + let(:schema) do + { + "type" => "object", + "properties" => { + "run" => { + "oneOf" => [ + { required: ["step"], title: "step" }, + { required: ["script"], title: "script" } + ] + } + } + } + end + + let(:validator) do + described_class.new( + attributes: [:run], + filename: 'test_schema', + detail_errors: true + ) + end + + before do + config[:run] = 'C' + allow(job).to receive(:read_attribute_for_validation).and_return({ run: config[:run] }) + allow(JSONSchemer).to receive(:schema).and_return(JSONSchemer.schema(schema)) + allow(File).to receive(:read).with(anything).and_return(schema.to_json) + end + + it 'returns an error message for oneOf violation without data pointer' do + subject + + expect(job.errors).not_to be_empty + expect("#{job.errors.first.attribute} #{job.errors.first.type}").to eq("run should use only one of: step, script") + end + end + end end end diff --git a/workhorse/go.mod b/workhorse/go.mod index dc1d210b7a2..6df51128a11 100644 --- a/workhorse/go.mod +++ b/workhorse/go.mod @@ -25,7 +25,7 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/smartystreets/goconvey v1.8.1 github.com/stretchr/testify v1.9.0 - gitlab.com/gitlab-org/gitaly/v16 v16.11.7 + gitlab.com/gitlab-org/gitaly/v16 v16.11.8 gitlab.com/gitlab-org/labkit v1.21.0 go.uber.org/goleak v1.3.0 gocloud.dev v0.38.0 diff --git a/workhorse/go.sum b/workhorse/go.sum index f19244227cd..0e327f585bc 100644 --- a/workhorse/go.sum +++ b/workhorse/go.sum @@ -486,8 +486,8 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -gitlab.com/gitlab-org/gitaly/v16 v16.11.7 h1:csL3bPTHIEE0147aCo1HFgNLMgmHwlwr1Ns65OlcMuE= -gitlab.com/gitlab-org/gitaly/v16 v16.11.7/go.mod h1:lJizRUtXRd1SBHjNbbbL9OsGN4TiugvfRBd8bIsdWI0= +gitlab.com/gitlab-org/gitaly/v16 v16.11.8 h1:bL9F90+rXTlQcsSuZJivn+CIwKGXXc787IJi4g3XQEU= +gitlab.com/gitlab-org/gitaly/v16 v16.11.8/go.mod h1:lJizRUtXRd1SBHjNbbbL9OsGN4TiugvfRBd8bIsdWI0= gitlab.com/gitlab-org/labkit v1.21.0 h1:hLmdBDtXjD1yOmZ+uJOac3a5Tlo83QaezwhES4IYik4= gitlab.com/gitlab-org/labkit v1.21.0/go.mod h1:zeATDAaSBelPcPLbTTq8J3ZJEHyPTLVBM1q3nva+/W4= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=