diff --git a/app/assets/javascripts/admin/broadcast_messages/components/base.vue b/app/assets/javascripts/admin/broadcast_messages/components/base.vue
index 2eb77eff138..ffdf74bade3 100644
--- a/app/assets/javascripts/admin/broadcast_messages/components/base.vue
+++ b/app/assets/javascripts/admin/broadcast_messages/components/base.vue
@@ -89,17 +89,22 @@ export default {
closeAddForm() {
this.showAddForm = false;
},
+ setVisibleMessages({ index, message, value }) {
+ const copy = [...this.visibleMessages];
+ copy[index] = { ...message, disable_delete: value };
+ this.visibleMessages = copy;
+ },
async deleteMessage(messageId) {
const index = this.visibleMessages.findIndex((m) => m.id === messageId);
if (!index === -1) return;
const message = this.visibleMessages[index];
- this.$set(this.visibleMessages, index, { ...message, disable_delete: true });
+ this.setVisibleMessages({ index, message, value: true });
try {
await axios.delete(message.delete_path);
} catch (e) {
- this.$set(this.visibleMessages, index, { ...message, disable_delete: false });
+ this.setVisibleMessages({ index, message, value: false });
createAlert({ message: this.$options.i18n.deleteError, variant: VARIANT_DANGER });
return;
}
diff --git a/app/assets/javascripts/ci/admin/jobs_table/admin_jobs_table_app.vue b/app/assets/javascripts/ci/admin/jobs_table/admin_jobs_table_app.vue
index 55ff647e25f..61a619580c1 100644
--- a/app/assets/javascripts/ci/admin/jobs_table/admin_jobs_table_app.vue
+++ b/app/assets/javascripts/ci/admin/jobs_table/admin_jobs_table_app.vue
@@ -7,6 +7,7 @@ import JobsTableTabs from '~/ci/jobs_page/components/jobs_table_tabs.vue';
import JobsFilteredSearch from '~/ci/common/private/jobs_filtered_search/app.vue';
import JobsTableEmptyState from '~/ci/jobs_page/components/jobs_table_empty_state.vue';
import { createAlert } from '~/alert';
+import { InternalEvents } from '~/tracking';
import {
TOKEN_TYPE_STATUS,
TOKEN_TYPE_JOBS_RUNNER_TYPE,
@@ -19,6 +20,7 @@ import {
JOBS_FETCH_ERROR_MSG,
LOADING_ARIA_LABEL,
CANCELABLE_JOBS_ERROR_MSG,
+ VIEW_ADMIN_JOBS_PAGELOAD,
} from './constants';
import JobsSkeletonLoader from './components/jobs_skeleton_loader.vue';
import GetAllJobs from './graphql/queries/get_all_jobs.query.graphql';
@@ -44,7 +46,7 @@ export default {
GlIntersectionObserver,
GlLoadingIcon,
},
- mixins: [glFeatureFlagsMixin()],
+ mixins: [glFeatureFlagsMixin(), InternalEvents.mixin()],
inject: {
jobStatuses: {
default: null,
@@ -155,6 +157,9 @@ export default {
this.count = newCount;
},
},
+ mounted() {
+ this.trackEvent(VIEW_ADMIN_JOBS_PAGELOAD);
+ },
methods: {
updateHistoryAndFetchCount(filterParams = {}) {
this.$apollo.queries.jobsCount.refetch(filterParams);
diff --git a/app/assets/javascripts/ci/admin/jobs_table/constants.js b/app/assets/javascripts/ci/admin/jobs_table/constants.js
index 015749a2f59..9b09bfe7a55 100644
--- a/app/assets/javascripts/ci/admin/jobs_table/constants.js
+++ b/app/assets/javascripts/ci/admin/jobs_table/constants.js
@@ -30,3 +30,4 @@ export const DEFAULT_FIELDS_ADMIN = [
];
export const RAW_TEXT_WARNING_ADMIN = RAW_TEXT_WARNING;
+export const VIEW_ADMIN_JOBS_PAGELOAD = 'view_admin_jobs_pageload';
diff --git a/app/assets/javascripts/observability/components/observability_no_data_empty_state.vue b/app/assets/javascripts/observability/components/observability_no_data_empty_state.vue
new file mode 100644
index 00000000000..6d3d07246c1
--- /dev/null
+++ b/app/assets/javascripts/observability/components/observability_no_data_empty_state.vue
@@ -0,0 +1,24 @@
+
+
+
+
+
+ {{ $options.i18n.description }}
+
+
+
diff --git a/app/assets/javascripts/observability/components/observability_empty_state.vue b/app/assets/javascripts/observability/components/observability_not_enabled_empty_state.vue
similarity index 100%
rename from app/assets/javascripts/observability/components/observability_empty_state.vue
rename to app/assets/javascripts/observability/components/observability_not_enabled_empty_state.vue
diff --git a/app/assets/javascripts/observability/components/provisioned_observability_container.vue b/app/assets/javascripts/observability/components/provisioned_observability_container.vue
index 95ffd54fd1d..79b794b96bd 100644
--- a/app/assets/javascripts/observability/components/provisioned_observability_container.vue
+++ b/app/assets/javascripts/observability/components/provisioned_observability_container.vue
@@ -3,12 +3,12 @@ import { GlLoadingIcon } from '@gitlab/ui';
import ObservabilityContainer from '~/observability/components/observability_container.vue';
import { s__ } from '~/locale';
import { createAlert } from '~/alert';
-import ObservabilityEmptyState from './observability_empty_state.vue';
+import ObservabilityNotEnabledEmptyState from './observability_not_enabled_empty_state.vue';
export default {
components: {
ObservabilityContainer,
- ObservabilityEmptyState,
+ ObservabilityNotEnabledEmptyState,
GlLoadingIcon,
},
props: {
@@ -85,7 +85,7 @@ export default {
-
diff --git a/config/events/view_admin_jobs_pageload.yml b/config/events/view_admin_jobs_pageload.yml
new file mode 100644
index 00000000000..6f5ff1c969b
--- /dev/null
+++ b/config/events/view_admin_jobs_pageload.yml
@@ -0,0 +1,16 @@
+---
+description: Tracks pageviews for the admin jobs page
+internal_events: true
+action: view_admin_jobs_pageload
+identifiers:
+- user
+product_group: personal_productivity
+milestone: '17.2'
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156814
+distributions:
+- ce
+- ee
+tiers:
+- free
+- premium
+- ultimate
diff --git a/config/feature_flags/gitlab_com_derisk/assign_resource_worker_deduplicate_until_executing.yml b/config/feature_flags/beta/assign_resource_worker_deduplicate_until_executing.yml
similarity index 87%
rename from config/feature_flags/gitlab_com_derisk/assign_resource_worker_deduplicate_until_executing.yml
rename to config/feature_flags/beta/assign_resource_worker_deduplicate_until_executing.yml
index 79bff9aad28..a264ba76d8e 100644
--- a/config/feature_flags/gitlab_com_derisk/assign_resource_worker_deduplicate_until_executing.yml
+++ b/config/feature_flags/beta/assign_resource_worker_deduplicate_until_executing.yml
@@ -5,5 +5,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/152303
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/460793
milestone: '17.1'
group: group::environments
-type: gitlab_com_derisk
-default_enabled: false
+type: beta
+default_enabled: true
diff --git a/config/metrics/counts_28d/count_distinct_user_id_from_view_admin_jobs_pageload_monthly.yml b/config/metrics/counts_28d/count_distinct_user_id_from_view_admin_jobs_pageload_monthly.yml
new file mode 100644
index 00000000000..680a6bda917
--- /dev/null
+++ b/config/metrics/counts_28d/count_distinct_user_id_from_view_admin_jobs_pageload_monthly.yml
@@ -0,0 +1,22 @@
+---
+key_path: redis_hll_counters.count_distinct_user_id_from_view_admin_jobs_pageload_monthly
+description: Monthly count of unique users who visit the admin jobs page
+product_group: personal_productivity
+performance_indicator_type: []
+value_type: number
+status: active
+milestone: '17.2'
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156814
+time_frame: 28d
+data_source: internal_events
+data_category: optional
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+events:
+- name: view_admin_jobs_pageload
+ unique: user.id
diff --git a/config/metrics/counts_28d/count_total_view_admin_jobs_pageload_monthly.yml b/config/metrics/counts_28d/count_total_view_admin_jobs_pageload_monthly.yml
new file mode 100644
index 00000000000..9f71e252c2e
--- /dev/null
+++ b/config/metrics/counts_28d/count_total_view_admin_jobs_pageload_monthly.yml
@@ -0,0 +1,21 @@
+---
+key_path: counts.count_total_view_admin_jobs_pageload_monthly
+description: Monthly count of total users who visit the admin jobs page
+product_group: personal_productivity
+performance_indicator_type: []
+value_type: number
+status: active
+milestone: '17.2'
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156814
+time_frame: 28d
+data_source: internal_events
+data_category: optional
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+events:
+- name: view_admin_jobs_pageload
diff --git a/config/metrics/counts_7d/count_distinct_user_id_from_view_admin_jobs_pageload_weekly.yml b/config/metrics/counts_7d/count_distinct_user_id_from_view_admin_jobs_pageload_weekly.yml
new file mode 100644
index 00000000000..a04e7796113
--- /dev/null
+++ b/config/metrics/counts_7d/count_distinct_user_id_from_view_admin_jobs_pageload_weekly.yml
@@ -0,0 +1,22 @@
+---
+key_path: redis_hll_counters.count_distinct_user_id_from_view_admin_jobs_pageload_weekly
+description: Weekly count of unique users who visit the admin jobs page
+product_group: personal_productivity
+performance_indicator_type: []
+value_type: number
+status: active
+milestone: '17.2'
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156814
+time_frame: 7d
+data_source: internal_events
+data_category: optional
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+events:
+- name: view_admin_jobs_pageload
+ unique: user.id
diff --git a/config/metrics/counts_7d/count_total_view_admin_jobs_pageload_weekly.yml b/config/metrics/counts_7d/count_total_view_admin_jobs_pageload_weekly.yml
new file mode 100644
index 00000000000..b88101e2916
--- /dev/null
+++ b/config/metrics/counts_7d/count_total_view_admin_jobs_pageload_weekly.yml
@@ -0,0 +1,21 @@
+---
+key_path: counts.count_total_view_admin_jobs_pageload_weekly
+description: Weekly count of total users who visit the admin jobs page
+product_group: personal_productivity
+performance_indicator_type: []
+value_type: number
+status: active
+milestone: '17.2'
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156814
+time_frame: 7d
+data_source: internal_events
+data_category: optional
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+events:
+- name: view_admin_jobs_pageload
diff --git a/doc/administration/gitaly/troubleshooting.md b/doc/administration/gitaly/troubleshooting.md
index 0d3c7ddabc0..08674850b66 100644
--- a/doc/administration/gitaly/troubleshooting.md
+++ b/doc/administration/gitaly/troubleshooting.md
@@ -331,9 +331,9 @@ This information in the logs is a gRPC call
If this error occurs, even though
[the Gitaly auth tokens are set up correctly](troubleshooting_gitaly_cluster.md#praefect-errors-in-logs),
it's likely that the Gitaly servers are experiencing
-[clock drift](https://en.wikipedia.org/wiki/Clock_drift).
+[clock drift](https://en.wikipedia.org/wiki/Clock_drift). The auth tokens sent to Gitaly include a timestamp. To be considered valid, Gitaly requires that timestamp to be within 60 seconds of the Gitaly server time.
-Ensure the Gitaly clients and servers are synchronized, and use an NTP time
+Ensure the Gitaly clients and servers are synchronized, and use a Network Time Protocol (NTP) time
server to keep them synchronized.
## Gitaly not listening on new address after reconfiguring
diff --git a/doc/administration/gitaly/troubleshooting_gitaly_cluster.md b/doc/administration/gitaly/troubleshooting_gitaly_cluster.md
index 40bf9d79e8c..a5403296b51 100644
--- a/doc/administration/gitaly/troubleshooting_gitaly_cluster.md
+++ b/doc/administration/gitaly/troubleshooting_gitaly_cluster.md
@@ -77,7 +77,7 @@ If this check fails:
### Check clock synchronization
Authentication between Praefect and the Gitaly servers requires the server times to be
-in sync so the token check succeeds.
+within 60 seconds of each other, so that the token check succeeds.
This check helps identify the root cause of `permission denied`
[errors being logged by Praefect](troubleshooting.md#permission-denied-errors-appearing-in-gitaly-or-praefect-logs-when-accessing-repositories).
@@ -90,7 +90,7 @@ checking with NTP service at and allowed clock drift 60000ms [correlation_id: <
Failed (fatal) error: gitaly node at tcp://[gitlab.example-instance.com]:8075: rpc error: code = DeadlineExceeded desc = context deadline exceeded
```
-To resolve this issue, set an environment variable on all Praefect servers to point to an accessible internal NTP server. For example:
+To resolve this issue, set an environment variable on all Praefect servers to point to an accessible internal [Network Time Protocol](https://en.wikipedia.org/wiki/Network_Time_Protocol) (NTP) server. For example:
```shell
export NTP_HOST=ntp.example.com
diff --git a/doc/architecture/blueprints/gitaly_plugins/index.md b/doc/architecture/blueprints/gitaly_plugins/index.md
new file mode 100644
index 00000000000..3d6fa5b8476
--- /dev/null
+++ b/doc/architecture/blueprints/gitaly_plugins/index.md
@@ -0,0 +1,342 @@
+---
+status: proposed
+creation-date: "2023-02-01"
+authors: [ "@samihiltunen" ]
+owning-stage: "~devops::enablement"
+---
+
+# Gitaly Plugins
+
+## Summary
+
+This blueprint describes a plugin interface for Gitaly. Plugins would be executables that Gitaly invokes before and after committing a transaction.
+
+Plugins would allow for implementing access check logic locally on the Gitaly node. Performing the access checks locally on the Gitaly node improves performance. Plugins would reduce complexity by hiding internal details that are currently leaking through Gitaly's API.
+
+The hard-coded access check logic and custom hooks would be replaced by plugins.
+
+## Motivation
+
+### Background
+
+Gitaly is a database system for storing Git repositories and strives to be decoupled from rest of GitLab. While separation of concerns in general is a good practice, Gitaly is also used outside of GitLab. The decoupling functions as a guideline on what sort of functionality should be implemented in Gitaly. Only functionality supporting Gitaly's main goal of providing repository storage and access should be implemented directly in Gitaly.
+
+Some use cases require tighter integration with Gitaly's write flows. For example:
+
+- Authorization checks must run before Gitaly accepts a write in order to reject unauthorized writes.
+- Notifications should be sent after writes in order to trigger CI jobs.
+
+This logic is not built directly into Gitaly to separate concerns. Gitaly calls [the Rails application's internal API](../../../development/internal_api/index.md) for both cases:
+
+- Before accepting a write, Gitaly calls `/internal/allowed`. The response from the endpoint decides whether or not Gitaly accepts the write.
+- After accepting a write, Gitaly calls `/internal/post_receive`.
+
+In addition to calling Rails application's internal API, Gitaly supports [custom hooks](../../../administration/server_hooks.md). Custom hooks are
+executables Gitaly invokes before and after accepting a write and conform to the interface of
+[Git hooks](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks). The `pre-receive` hook can reject a write, `update` hook can drop single reference
+update, and `post-receive` can be used to be notified of accepted writes.
+
+'Custom logic' is used to refer to both of the internal API calls and custom hooks in the blueprint.
+
+### Problems
+
+#### Overlapping functionality
+
+Both the internal API and custom hooks are invoked in the same locations, and have similar functionality. They provide redundant functionality with the main difference being the author of the logic:
+
+- Internal API is used by GitLab.
+- Custom hooks are used on self-managed instances to plug in custom logic.
+
+Maintaining redundant functionality is a burden. Custom hooks not being used by GitLab means they are not thoroughly exercised in production on GitLab.com.
+The internal API calls are essentially GitLab-specific hooks that are hard-coded into Gitaly.
+
+#### Not all writes can be hooked into
+
+Gitaly is hard-coded to invoke the custom logic only during certain write operations. These operations are decided by where GitLab needs to invoke custom
+logic. Roughly, these points are:
+
+- The various `*ReceivePack` variants that invoke custom logic. These are used to run the custom logic on pushes.
+- `OperationService` RPCs that are invoked by Rails when a user performs changes in a repository. Custom logic is invoked during these because the Rails
+ application wants to run authorization checks to see whether the user is allowed to make the change.
+
+Custom logic is not invoked for other writes as GitLab doesn't need it. For example, excluded writes include repository creations, repository deletions, and reference updates through `WriteRef`.
+
+This creates a tight coupling with the needs of GitLab.
+
+#### Custom hooks interface determined by Git
+
+Custom hooks conform to the interface of Git's hooks. This can be limiting and has led to an inconsistent interface:
+
+- Reference updates are streamed through stdin.
+- Other variables are passed in through environment variables.
+- Custom prefixes required on error messages to pass them through Gitaly to the user.
+- Invoking `update` hook for each reference is inefficient.
+- No natural point to extend the payload to cover other supported writes than reference updates.
+
+A better interface could be defined as Gitaly executes the custom hooks. There's no need to conform to the interface Git uses for hooks.
+
+#### Performance issues
+
+Rails performs authorization checks in the internal API by fetching data through Gitaly's public API. This leads to performance issues as Rails may need to fetch the complete write to perform its checks. For example, [pre-receive secret detection](../secret_detection/index.md) would require fetching all new blobs in a write to perform its checks. This could be avoided if there was a way to run the check locally on the Gitaly node.
+
+#### Leaking internals
+
+Before a write is accepted, Gitaly holds the new objects in a quarantine directory. These objects should not be accessible through Gitaly's API for other users. The authorization checks need access to these objects though. This is handled by sending the path of the quarantine directory to Rails, and Rails passes this to Gitaly in follow-up calls. This leaks internal details:
+
+- The quarantines are exposed in the public API.
+- On Gitaly Cluster, the quarantine directory is in a different location on each node. This requires Praefect to support force routing calls to the primary replica so the quarantine path points to the correct location in the follow-up calls.
+- This gets even more complicated with [transactions](../gitaly_transaction_management/index.md). The quarantine paths are relative to the transaction's snapshot. In order for the quarantine paths to apply, the relative path sent in the request should be the snapshot repository's relative path. Praefect however requires the original relative path of the repository to route the request to the correct Gitaly node.
+
+Leaking internals adds complexity and has to be worked around.
+
+#### Complexity
+
+In addition to the performance issues and leaking internal, the current access check flow has proven to be complex and difficult to understand:
+
+1. The Rails application calling Gitaly for an RPC.
+1. Gitaly calling back to the Rails application for access checks.
+1. The Rails application calling Gitaly multiple times again to fetch data required by the access checks.
+
+This complexity could be reduced by not fanning out multiple other RPCs from a given RPC but instead keeping all of the checks in the context of
+currently-executing RPC.
+
+## Solution
+
+Define a plugin interface in Gitaly that enables efficient implementation of pre-commit checks without leaking internal details to the API.
+
+### Goals
+
+- Single plugin interface to replace calls to internal API and custom hooks.
+- Enable efficient execution of access checks.
+ - Enables execution of the checks close to the data on the Gitaly node.
+ - Do not dictate the implementation, and enable the plugin authors to implement their plugin
+ in the best manner for the use case.
+- Remove GitLab-specific assumptions from the API.
+ - Hard-coded calls to the internal API removed and moved behind the plugin interface.
+ - Invoke the plugin for every write.
+ - Remove GitLab specific fields such as `gl_repository` and `gl_project_path` from Gitaly's API.
+- Do not leak internal details.
+ - Remove quarantine directories from the API.
+ - Remove force routing to primary.
+ - Remove the need to pipe the transaction's snapshot path through the public API.
+- A clean, well-defined interface set by the needs of Gitaly and its users, not Git.
+
+## Proposal
+
+The proposal is written with Gitaly's upcoming [transaction management](../gitaly_transaction_management/index.md) in mind.
+
+### Plugins
+
+Plugins would be executables that Gitaly invokes at certain points during transaction execution. Plugins being executables ensures:
+
+- Protection: plugins would run in separate processes. Gitaly would be guarded against memory leaks and crashes in the plugins.
+- Flexibility:
+ - Plugins could be implemented using whatever tools the authors prefer.
+ - Plugins could be implemented as one-off executables, or may call into a server daemon.
+- Separation of concerns: Gitaly would just need to execute an executable and defers other responsibility to it.
+
+Plugins would be invoked at two points during transaction execution:
+
+- `before-commit`:
+ - Invoked before a transaction is committed.
+ - Allows for rejecting a transaction.
+- `after-commit`:
+ - Invoked after a transaction is committed.
+ - Transaction is already committed, so this just serves as a notification.
+
+The names are chosen to disambiguate from Git's `pre-commit` and `post-commit` hooks. `Plugin` is used to further disambiguate from Git's hooks.
+
+Gitaly would allow for configuring a single plugin. Gitaly would be relieved from deciding whether to execute multiple plugins concurrently or sequentially, in
+which order, and whether a single plugin failing the write stops the execution of further plugins. These decisions would be delegated to plugins. Support for
+running multiple plugin executables can be implemented in a plugin if truly needed.
+
+The single plugin covers all partitions/repositories. Repository-specific logic can be implemented in the plugin to support similar use cases previously served by repository-specific custom hooks.
+
+Gitaly and the plugin would communicate over `stdin` and `stdout` of the plugin process. Each message written to the other process would be prefixed with an `uint64` describing
+the length of the payload that follows. This would make for a simple message passing protocol. Protocol buffers would be used to define the schema and serialize the payload.
+
+When the plugin is invoked, it would writes its supported protocol version to `stdout` as a big-endian encoded `uint16`. This would enable Gitaly to evolve the API in
+backwards-incompatible manner by supporting multiple versions of the protocol side-by-side. If the plugin's protocol was not supported, Gitaly would kill the plugin and fail
+the write. If the protocol is supported, Gitaly would proceed.
+
+The initial version of the protocol is described below.
+
+After the version negotiation, Gitaly sends messages to the plugin via `stdin`. Below is the message schema:
+
+```protobuf
+// PluginRequest is the payload that describes the transaction being executed.
+message PluginRequest {
+ // ReferenceChanges describes reference changes of the transaction.
+ message ReferenceChanges {
+ // Change describes a single reference change made in the transaction.
+ message Change {
+ // reference_name is the name of the reference being changed.
+ bytes reference_name = 1;
+ // old_oid is the reference's previous object ID. Zero OID indicates the reference did not exist.
+ string old_oid = 2;
+ // new_oid is the reference's new object ID. Zero OID indicates a reference deletion.
+ string new_oid = 3;
+ }
+
+ repeated Change changes = 1;
+ }
+
+ // Header contains details of the plugin's execution environment and the transaction.
+ message Header {
+ // storage is the name of the target storage of the transaction.
+ string storage = 1;
+ // relative_path is the relative path of the target repository of the transaction.
+ string relative_path = 2;
+ // push_options contains the push options sent by the client during a push, if any.
+ repeated bytes push_options = 3;
+ // client_metadata contains a blob sent by the client to Gitaly to pass through to
+ // the plugin. It allows the client to send parameters to the plugin transparently
+ // through Gitaly. The metadata is sent by the client by setting the gRPC metadata
+ // header `gitaly-plugin-metadata-bin` in the request to Gitaly.
+ //
+ // This can be used to pipe GitLab specific data from Rails to the plugin, such as
+ // `gl_project` and `gl_user`.
+ bytes plugin_metadata = 4;
+
+ // git_command_path contains the absolute path to the Git command that should be used to
+ // access the repository.
+ string git_command_path = 5;
+ // repository_path contains the absolute path of the transaction's target repository. It
+ // points a snapshot of the actual repository.
+ string repository_path = 6;
+ // git_object_directory is an absolute path to the transaction's quarantine directory
+ // where the new objects are written. It must be set for the Git invocations as an
+ // environment variable through `GIT_OBJECT_DIRECTORY` for the objects to be readable.
+ string git_object_directory = 7;
+ // git_alternate_object_directories points to the object database that contains the objects
+ // that existed prior to the transaction. It must be set for the Git invocation as an
+ // environment variable through `GIT_ALTERNATE_OBJECT_DIRECTORIES` for the objects to be
+ // readable.
+ //
+ // `GIT_ALTERNATE_OBJECT_DIRECTORIES` can be left unset if the Git invocation should only
+ // read the new objects introduced in the transaction. This can be useful for some operations
+ // that may for example want to scan only new blobs.
+ string git_alternate_object_directories = 8;
+ }
+
+ oneof message {
+ // header is always the first message sent to the plugin.
+ Header header = 1;
+ // reference_changes are the reference_changes being performed by this transaction. reference_changes
+ // may be chunked over multiple messages.
+ ReferenceChanges reference_changes = 2;
+ }
+```
+
+The header is always sent in the first message. Reference changes follow the header and may be chunked over multiple messages. Gitaly closes the plugin's `stdin` once it is done sending messages.
+
+The initial protocol supports only hooking into reference changes of a single repository as hooks currently do. The protocol can later be extended as needed to support for example:
+
+- Other write types, such as repository creations and deletions.
+- Transactions targeting multiple repositories.
+
+After receiving the payload, the plugin would run its logic. It would access the Git repository at the given path through the provided Git command to
+retrieve the data it needs. On finishing, the plugin would write a response to stdout that contains
+[a status](https://github.com/googleapis/googleapis/blob/master/google/rpc/status.proto#L35) to indicate whether or not the transaction should be committed
+or aborted.
+
+```protobuf
+// PluginResponse is the response message the plugin passes back to Gitaly after finishing.
+message PluginResponse {
+ // status indicates whether the transaction should succeed or fail.
+ google.rpc.Status status = 1
+}
+```
+
+- If the status has code `OK`, the transaction is committed.
+- If the status has any other code, the transaction is aborted. The status is passed back
+ to the client as is. This enables the plugin to communicate a rejection reason with an
+ accurate status code, message, and additional details.
+
+The plugin should return a zero exit code if it executed successfully. This should be done even if the transaction is rejected.
+
+If the plugin returns a non-zero exit code, the plugin is considered to have failed. This is different from the plugin rejecting a write. When the plugin fails, `stdout` is ignored and an error is returned to the client. Gitaly logs an error with the contents of `stderr` and rejects the write.
+
+If the plugin is taking too long, as defined by RPC deadlines, Gitaly kills the plugin. This is handled as a plugin failure as described above.
+
+#### Compatibility guarantees
+
+Gitaly would guarantee the repository at the given path is accessible through the `git` command at the given path. All access to the repository must go through the provided
+`git` binary.
+
+The repository's layout, location, file formats, and storage details are not included in the compatibility guarantee. This enables Gitaly to iterate freely
+on the storage formats. Modifying the repository is not supported.
+
+The payload's format should be guaranteed to remain backwards compatible within a protocol version. New keys may be introduced without bumping the version.
+
+#### Custom hooks
+
+Because Gitaly would support only configuring a single plugin, a question may arise on how the GitLab plugin could be configured along with a custom one. While running multiple
+plugins wouldn't be directly supported, a custom plugin could be configured and the plugin could invoke the GitLab plugin. This would allow for plugging in custom logic
+alongside the GitLab access check plugin. Because the custom plugin would control calling the GitLab plugin, it would have full control on whether to run GitLab plugin before,
+after, or concurrently with its own logic.
+
+### Migration
+
+Migration to plugins would take a few steps:
+
+1. Create a project for the GitLab access check plugin. This is where the existing internal API logic will be migrated.
+1. Package the plugin in the distributions and deploy and configure it on the Gitaly nodes.
+1. Migrate the internal API calling logic from Gitaly to the plugin.
+1. At this point, we'd have the existing internal API calling logic from Gitaly executed as a plugin.
+1. We can now begin migrating the access checks step by step into the plugin.
+
+All access to the new objects of a transaction should go through the plugin. Gitaly provides just the interface to hook into its writes.
+
+How exactly the access checks are performed is left up to the teams responsible for the access checks. The access checks could fetch only policy from Rails and run the checks locally if the check in question depends heavily on the Git data. This improves efficiency by removing the loopback calls and accessing the data directly on the Gitaly node. Some access check logic could still remain in Rails if it is best fit for it.
+
+Once all of repository access in access checks goes through the plugin, the quarantine directories and the force-route-to-primary functionality can be removed from the API of Gitaly.
+
+#### End state
+
+The following diagram demonstrates the desired end state:
+
+1. Gitaly no longer calls to Rails directly.
+1. The plugin fetches only access check policy from Rails. The access checks are run locally on the repository.
+1. Gitaly commits or aborts the transaction based on the plugin's result.
+
+```mermaid
+sequenceDiagram
+ Rails->>+Gitaly: Mutator RPC
+ Gitaly-->>Gitaly: Perform Operations
+ Gitaly-->>Gitaly: Queue for Commit
+ Gitaly->>+Plugin: Execute
+ Plugin->>Gitaly: Version Negotiation
+ Gitaly->>Plugin: Payload
+ Plugin->>+Rails: Fetch Access Check Policy
+ Plugin->>+Repository: Run Access Checks
+ Plugin->>-Gitaly: Respond
+ Gitaly->>Gitaly: Commit / Abort
+ Gitaly->>-Rails: Respond
+```
+
+### Considerations
+
+#### Security
+
+The plugins should only be configurable by administrators only and are considered trusted. There are no plans to sandbox them.
+
+#### Subprocesses
+
+If the plugin launches a subprocess, it should keep it in the same process group so Gitaly can kill the entire process group if it wants to terminate the plugin. Gitaly should consider running the plugins in their own cgroups so we can guarantee killing all subprocesses.
+
+#### `update` hook's functionality is not supported
+
+The plugin currently does not support dropping certain reference updates as is possible with `update` hook. Support for the hook was left out as it's not clear how useful it is. If necessary, it could be supported by having the plugin write messages to Gitaly on which references to drop from the update.
+
+### Future opportunities
+
+#### Guaranteed post-receive notification deliveries ([#5411](https://gitlab.com/gitlab-org/gitaly/-/issues/5411))
+
+Currently Gitaly does not guarantee the delivery of post-receive notifications. The delivery could fail for any reason. For example, Gitaly could crash or
+the Rails application could be unavailable. This can lead to unexpected behavior.
+
+1. The notifications trigger CI pipelines after writes. If the delivery fails, the pipelines may not be triggered.
+1. Code search indexes new changes based on the notifications. If the delivery fails, the indexes can become stale.
+
+With transactions stored in the write-ahead log, Gitaly could guarantee the delivery of the `after-commit` notifications after a crash by recovering the transactions from the log and reattempting delivery. To facilitate this, all necessary information should be stored in the log entry, for example the push options.
diff --git a/lib/gitlab/sidekiq_middleware.rb b/lib/gitlab/sidekiq_middleware.rb
index 399ab2fd20b..2faebc55c59 100644
--- a/lib/gitlab/sidekiq_middleware.rb
+++ b/lib/gitlab/sidekiq_middleware.rb
@@ -22,6 +22,8 @@ module Gitlab
chain.add ::Labkit::Middleware::Sidekiq::Server
chain.add ::Gitlab::SidekiqMiddleware::RequestStoreMiddleware
+ chain.add ::Gitlab::QueryLimiting::SidekiqMiddleware if ::Gitlab::QueryLimiting.enabled_for_env?
+
if metrics
chain.add ::Gitlab::SidekiqMiddleware::ServerMetrics
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 3fcc724499f..b309a9cf3b0 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -35829,9 +35829,6 @@ msgstr ""
msgid "ObservabilityLogs|Attributes"
msgstr ""
-msgid "ObservabilityLogs|Check again"
-msgstr ""
-
msgid "ObservabilityLogs|Count"
msgstr ""
@@ -35865,9 +35862,6 @@ msgstr ""
msgid "ObservabilityLogs|Monitor log events captured from your systems. Send log data to this project using OpenTelemetry. %{docsLink}"
msgstr ""
-msgid "ObservabilityLogs|No logs to display."
-msgstr ""
-
msgid "ObservabilityLogs|Resource Attribute"
msgstr ""
@@ -36027,6 +36021,12 @@ msgstr ""
msgid "Observability|Monitor your applications with GitLab Observability."
msgstr ""
+msgid "Observability|Sorry, your filter produced no results"
+msgstr ""
+
+msgid "Observability|To widen your search, change or remove filters above"
+msgstr ""
+
msgid "Oct"
msgstr ""
diff --git a/spec/frontend/admin/broadcast_messages/components/base_spec.js b/spec/frontend/admin/broadcast_messages/components/base_spec.js
index 23a7767add8..0c976e07376 100644
--- a/spec/frontend/admin/broadcast_messages/components/base_spec.js
+++ b/spec/frontend/admin/broadcast_messages/components/base_spec.js
@@ -64,7 +64,7 @@ describe('BroadcastMessagesBase', () => {
await waitForPromises();
expect(axiosMock.history.delete).toHaveLength(0);
- expect(wrapper.vm.visibleMessages.length).toBe(MOCK_MESSAGES.length);
+ expect(findTable().props('messages')).toHaveLength(MOCK_MESSAGES.length);
});
it('does not remove a deleted message if the request fails', async () => {
@@ -75,7 +75,11 @@ describe('BroadcastMessagesBase', () => {
findTable().vm.$emit('delete-message', id);
await waitForPromises();
- expect(wrapper.vm.visibleMessages.find((m) => m.id === id)).not.toBeUndefined();
+ expect(
+ findTable()
+ .props('messages')
+ .find((m) => m.id.id === id.id),
+ ).not.toBeUndefined();
expect(createAlert).toHaveBeenCalledWith(
expect.objectContaining({
message: BroadcastMessagesBase.i18n.deleteError,
@@ -91,8 +95,12 @@ describe('BroadcastMessagesBase', () => {
findTable().vm.$emit('delete-message', id);
await waitForPromises();
- expect(wrapper.vm.visibleMessages.find((m) => m.id === id)).toBeUndefined();
- expect(wrapper.vm.totalMessages).toBe(MOCK_MESSAGES.length - 1);
+ expect(
+ findTable()
+ .props('messages')
+ .find((m) => m.id.id === id.id),
+ ).toBeUndefined();
+ expect(findPagination().props('totalItems')).toBe(MOCK_MESSAGES.length - 1);
});
it('redirects to the first page when totalMessages changes from 21 to 20', async () => {
diff --git a/spec/frontend/ci/admin/jobs_table/admin_job_table_app_spec.js b/spec/frontend/ci/admin/jobs_table/admin_job_table_app_spec.js
index d14b78d2f4d..0c7af8bebc4 100644
--- a/spec/frontend/ci/admin/jobs_table/admin_job_table_app_spec.js
+++ b/spec/frontend/ci/admin/jobs_table/admin_job_table_app_spec.js
@@ -16,12 +16,14 @@ import { createAlert } from '~/alert';
import { TEST_HOST } from 'spec/test_constants';
import JobsFilteredSearch from '~/ci/common/private/jobs_filtered_search/app.vue';
import * as urlUtils from '~/lib/utils/url_utility';
+import { useMockInternalEventsTracking } from 'helpers/tracking_internal_events_helper';
import {
JOBS_FETCH_ERROR_MSG,
CANCELABLE_JOBS_ERROR_MSG,
LOADING_ARIA_LABEL,
RAW_TEXT_WARNING_ADMIN,
JOBS_COUNT_ERROR_MESSAGE,
+ VIEW_ADMIN_JOBS_PAGELOAD,
} from '~/ci/admin/jobs_table/constants';
import { TOKEN_TYPE_JOBS_RUNNER_TYPE } from '~/vue_shared/components/filtered_search_bar/constants';
import {
@@ -96,6 +98,16 @@ describe('Job table app', () => {
});
};
+ describe('on page load', () => {
+ const { bindInternalEventDocument } = useMockInternalEventsTracking();
+
+ it('tracks view_admin_jobs_pageload event', () => {
+ createComponent();
+ const { trackEventSpy } = bindInternalEventDocument(wrapper.element);
+ expect(trackEventSpy).toHaveBeenCalledWith(VIEW_ADMIN_JOBS_PAGELOAD, {}, undefined);
+ });
+ });
+
describe('loading state', () => {
it('should display skeleton loader when loading', () => {
createComponent();
diff --git a/spec/frontend/observability/components/observability_no_data_empty_state_spec.js b/spec/frontend/observability/components/observability_no_data_empty_state_spec.js
new file mode 100644
index 00000000000..84b0d1fe8a2
--- /dev/null
+++ b/spec/frontend/observability/components/observability_no_data_empty_state_spec.js
@@ -0,0 +1,22 @@
+import { GlEmptyState } from '@gitlab/ui';
+import ObservabilityNoDataEmptyState from '~/observability/components/observability_no_data_empty_state.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+describe('ObservabilityNoDataEmptyState', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = shallowMountExtended(ObservabilityNoDataEmptyState);
+ });
+
+ it('passes the correct title', () => {
+ expect(wrapper.findComponent(GlEmptyState).props('title')).toBe(
+ 'Sorry, your filter produced no results',
+ );
+ });
+
+ it('displays the correct description', () => {
+ const description = wrapper.find('span').text();
+ expect(description).toBe('To widen your search, change or remove filters above');
+ });
+});
diff --git a/spec/frontend/observability/observability_empty_state_spec.js b/spec/frontend/observability/observability_not_enabled_empty_state_spec.js
similarity index 80%
rename from spec/frontend/observability/observability_empty_state_spec.js
rename to spec/frontend/observability/observability_not_enabled_empty_state_spec.js
index ce420dd076d..9c992de2839 100644
--- a/spec/frontend/observability/observability_empty_state_spec.js
+++ b/spec/frontend/observability/observability_not_enabled_empty_state_spec.js
@@ -1,14 +1,14 @@
import { GlButton, GlEmptyState } from '@gitlab/ui';
-import ObservabilityEmptyState from '~/observability/components/observability_empty_state.vue';
+import ObservabilityNotEnabledEmptyState from '~/observability/components/observability_not_enabled_empty_state.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-describe('ObservabilityEmptyState', () => {
+describe('ObservabilityNotEnabledEmptyState', () => {
let wrapper;
const findEnableButton = () => wrapper.findComponent(GlButton);
beforeEach(() => {
- wrapper = shallowMountExtended(ObservabilityEmptyState);
+ wrapper = shallowMountExtended(ObservabilityNotEnabledEmptyState);
});
it('passes the correct title', () => {
diff --git a/spec/frontend/observability/provisioned_observability_container_spec.js b/spec/frontend/observability/provisioned_observability_container_spec.js
index a2e8b60dc9f..a3ebad10e91 100644
--- a/spec/frontend/observability/provisioned_observability_container_spec.js
+++ b/spec/frontend/observability/provisioned_observability_container_spec.js
@@ -1,7 +1,7 @@
import { GlLoadingIcon } from '@gitlab/ui';
import ProvisionedObservabilityContainer from '~/observability/components/provisioned_observability_container.vue';
import ObservabilityContainer from '~/observability/components/observability_container.vue';
-import ObservabilityEmptyState from '~/observability/components/observability_empty_state.vue';
+import ObservabilityNotEnabledEmptyState from '~/observability/components/observability_not_enabled_empty_state.vue';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
@@ -27,7 +27,7 @@ describe('ProvisionedObservabilityContainer', () => {
};
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findEmptyState = () => wrapper.findComponent(ObservabilityEmptyState);
+ const findEmptyState = () => wrapper.findComponent(ObservabilityNotEnabledEmptyState);
const findSlotComponent = () => wrapper.findComponent({ name: 'MockComponent' });
const props = {
diff --git a/spec/requests/api/invitations_spec.rb b/spec/requests/api/invitations_spec.rb
index aa0ea390b14..d75e6da0fcf 100644
--- a/spec/requests/api/invitations_spec.rb
+++ b/spec/requests/api/invitations_spec.rb
@@ -435,7 +435,7 @@ RSpec.describe API::Invitations, feature_category: :user_profile do
emails = 'email3@example.com,email4@example.com,email5@example.com,email6@example.com,email7@example.com,' \
'EMAIL8@EXamPle.com'
- unresolved_n_plus_ones = 77 # currently there are 10 queries added per email
+ unresolved_n_plus_ones = 80 # currently there are 10 queries added per email, checking if we should dispatch AuthorizationsAddedEvent makes 1 query per event (3 events dispatched)
expect do
post invitations_url(project, maintainer), params: { email: emails, access_level: Member::DEVELOPER }
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 2b888e4a177..4128682cce6 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -3964,7 +3964,7 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and
measure_project.add_developer(create(:user))
measure_project.add_developer(create(:user)) # make this 2nd one to find any n+1
- unresolved_n_plus_ones = 27 # 27 queries added per member
+ unresolved_n_plus_ones = 28 # 28 queries added per member
expect do
post api("/projects/#{project.id}/import_project_members/#{measure_project.id}", user)
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index de5e236b141..ce4a9d941fd 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -440,7 +440,6 @@ RSpec.configure do |config|
skip_jobs: false # We're not skipping jobs for inline tests
).call(chain)
- chain.insert_after ::Gitlab::SidekiqMiddleware::RequestStoreMiddleware, ::Gitlab::QueryLimiting::SidekiqMiddleware
chain.insert_after ::Gitlab::SidekiqMiddleware::RequestStoreMiddleware, IsolatedRequestStore
example.run