Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2024-01-15 09:10:15 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2024-01-15 09:10:15 +0300
commit500f4288e71e39d6cabbb0faad0e6170a8e792b4 (patch)
tree0724c43280d0c01f24f19a73febd7e1c6ffb91cc
parentd0830d520a7eb2be16338f7f36158b522deb68ec (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--GITLAB_KAS_VERSION2
-rw-r--r--app/finders/projects/ml/experiment_finder.rb46
-rw-r--r--app/models/ml/experiment.rb3
-rw-r--r--app/services/work_items/callbacks/start_and_due_date.rb16
-rw-r--r--app/services/work_items/widgets/start_and_due_date_service/update_service.rb18
-rw-r--r--db/migrate/20231123160255_add_token_to_chat_names.rb20
-rw-r--r--doc/development/ai_features/index.md174
-rw-r--r--lib/api/entities/ml/mlflow/search_experiments.rb14
-rw-r--r--lib/api/ml/mlflow/experiments.rb36
-rwxr-xr-xscripts/internal_events/cli/metric.rb11
-rw-r--r--spec/finders/projects/ml/experiment_finder_spec.rb51
-rw-r--r--spec/fixtures/api/schemas/ml/search_experiments.json39
-rw-r--r--spec/fixtures/scripts/internal_events/metrics/ee_total_28d_single_event.yml1
-rw-r--r--spec/fixtures/scripts/internal_events/metrics/ee_total_7d_single_event.yml1
-rw-r--r--spec/fixtures/scripts/internal_events/metrics/ee_total_single_event.yml1
-rw-r--r--spec/fixtures/scripts/internal_events/metrics/keyboard_smashed_metric_28d.yml1
-rw-r--r--spec/fixtures/scripts/internal_events/metrics/keyboard_smashed_metric_7d.yml1
-rw-r--r--spec/fixtures/scripts/internal_events/metrics/project_id_28d_multiple_events.yml1
-rw-r--r--spec/fixtures/scripts/internal_events/metrics/project_id_7d_multiple_events.yml1
-rw-r--r--spec/fixtures/scripts/internal_events/metrics/total_single_event.yml1
-rw-r--r--spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event.yml1
-rw-r--r--spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event.yml1
-rw-r--r--spec/models/ml/experiment_spec.rb14
-rw-r--r--spec/requests/api/ml/mlflow/experiments_spec.rb77
-rw-r--r--spec/services/work_items/callbacks/start_and_due_date_spec.rb (renamed from spec/services/work_items/widgets/start_and_due_date_service/update_service_spec.rb)20
25 files changed, 413 insertions, 138 deletions
diff --git a/GITLAB_KAS_VERSION b/GITLAB_KAS_VERSION
index 34326d62e6f..13f7272cfef 100644
--- a/GITLAB_KAS_VERSION
+++ b/GITLAB_KAS_VERSION
@@ -1 +1 @@
-v16.8.0
+v16.9.0-rc1
diff --git a/app/finders/projects/ml/experiment_finder.rb b/app/finders/projects/ml/experiment_finder.rb
new file mode 100644
index 00000000000..0363cc6ec39
--- /dev/null
+++ b/app/finders/projects/ml/experiment_finder.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Projects
+ module Ml
+ class ExperimentFinder
+ include Gitlab::Utils::StrongMemoize
+
+ VALID_ORDER_BY = %w[name created_at updated_at id].freeze
+ VALID_SORT = %w[asc desc].freeze
+
+ def initialize(project, params = {})
+ @project = project
+ @params = params
+ end
+
+ def execute
+ relation
+ end
+
+ private
+
+ def relation
+ @experiments = ::Ml::Experiment
+ .by_project(project)
+ .including_project
+
+ ordered
+ end
+
+ def ordered
+ order_by = valid_or_default(params[:order_by]&.downcase, VALID_ORDER_BY, 'id')
+ sort = valid_or_default(params[:sort]&.downcase, VALID_SORT, 'desc')
+
+ experiments.order_by("#{order_by}_#{sort}").with_order_id_desc
+ end
+
+ def valid_or_default(value, valid_values, default)
+ return value if valid_values.include?(value)
+
+ default
+ end
+
+ attr_reader :params, :project, :experiments
+ end
+ end
+end
diff --git a/app/models/ml/experiment.rb b/app/models/ml/experiment.rb
index ad6c6b7b3bf..456c23df0e0 100644
--- a/app/models/ml/experiment.rb
+++ b/app/models/ml/experiment.rb
@@ -3,6 +3,7 @@
module Ml
class Experiment < ApplicationRecord
include AtomicInternalId
+ include Sortable
PACKAGE_PREFIX = 'ml_experiment_'
@@ -15,6 +16,8 @@ module Ml
has_many :candidates, class_name: 'Ml::Candidate'
has_many :metadata, class_name: 'Ml::ExperimentMetadata'
+ scope :including_project, -> { includes(:project) }
+ scope :by_project, ->(project) { where(project: project) }
scope :with_candidate_count, -> {
left_outer_joins(:candidates)
.select("ml_experiments.*, count(ml_candidates.id) as candidate_count")
diff --git a/app/services/work_items/callbacks/start_and_due_date.rb b/app/services/work_items/callbacks/start_and_due_date.rb
new file mode 100644
index 00000000000..b7318dcfcf4
--- /dev/null
+++ b/app/services/work_items/callbacks/start_and_due_date.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module WorkItems
+ module Callbacks
+ class StartAndDueDate < Base
+ def before_update
+ return work_item.assign_attributes({ start_date: nil, due_date: nil }) if excluded_in_new_type?
+
+ return if params.blank?
+ return unless has_permission?(:set_work_item_metadata)
+
+ work_item.assign_attributes(params.slice(:start_date, :due_date))
+ end
+ end
+ end
+end
diff --git a/app/services/work_items/widgets/start_and_due_date_service/update_service.rb b/app/services/work_items/widgets/start_and_due_date_service/update_service.rb
deleted file mode 100644
index 5d47b3a1516..00000000000
--- a/app/services/work_items/widgets/start_and_due_date_service/update_service.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-module WorkItems
- module Widgets
- module StartAndDueDateService
- class UpdateService < WorkItems::Widgets::BaseService
- def before_update_callback(params: {})
- return widget.work_item.assign_attributes({ start_date: nil, due_date: nil }) if new_type_excludes_widget?
-
- return if params.blank?
- return unless has_permission?(:set_work_item_metadata)
-
- widget.work_item.assign_attributes(params.slice(:start_date, :due_date))
- end
- end
- end
- end
-end
diff --git a/db/migrate/20231123160255_add_token_to_chat_names.rb b/db/migrate/20231123160255_add_token_to_chat_names.rb
index f35a6b812f3..af8550b9020 100644
--- a/db/migrate/20231123160255_add_token_to_chat_names.rb
+++ b/db/migrate/20231123160255_add_token_to_chat_names.rb
@@ -5,8 +5,22 @@ class AddTokenToChatNames < Gitlab::Database::Migration[2.2]
milestone '16.7'
- def change
- add_column :chat_names, :encrypted_token, :binary
- add_column :chat_names, :encrypted_token_iv, :binary
+ # This migration was added as different filenames across GitLab
+ # versions in security releases:
+ #
+ # 16.8 - db/migrate/20231123160255_add_token_to_chat_names.rb
+ # 16.7 - db/migrate/20231219120134_add_token_to_chat_names.rb
+ # 16.6 - db/migrate/20231215135014_add_token_to_chat_names.rb
+ # 16.5 - db/migrate/20231215145632_add_token_to_chat_names.rb
+ #
+ # This migration needs to be idempotent to prevent upgrade failures.
+ def up
+ add_column :chat_names, :encrypted_token, :binary unless column_exists?(:chat_names, :encrypted_token)
+ add_column :chat_names, :encrypted_token_iv, :binary unless column_exists?(:chat_names, :encrypted_token_iv)
+ end
+
+ def down
+ remove_column :chat_names, :encrypted_token, :binary if column_exists?(:chat_names, :encrypted_token)
+ remove_column :chat_names, :encrypted_token_iv, :binary if column_exists?(:chat_names, :encrypted_token_iv)
end
end
diff --git a/doc/development/ai_features/index.md b/doc/development/ai_features/index.md
index da652cc2c44..f8680ef91c9 100644
--- a/doc/development/ai_features/index.md
+++ b/doc/development/ai_features/index.md
@@ -97,11 +97,11 @@ In order to obtain a GCP service key for local development, follow the steps bel
- Create a sandbox GCP project by visiting [this page](https://about.gitlab.com/handbook/infrastructure-standards/#individual-environment) and following the instructions, or by requesting access to our existing group GCP project by using [this template](https://gitlab.com/gitlab-com/it/infra/issue-tracker/-/issues/new?issuable_template=gcp_group_account_iam_update_request).
- If you are using an individual GCP project, you may also need to enable the Vertex AI API:
- 1. Visit [welcome page](https://console.cloud.google.com/welcome), choose your project (e.g. jdoe-5d23dpe).
- 1. Go to **APIs & Services > Enabled APIs & services**.
- 1. Select **+ Enable APIs and Services**.
- 1. Search for `Vertex AI API`.
- 1. Select **Vertex AI API**, then select **Enable**.
+ 1. Visit [welcome page](https://console.cloud.google.com/welcome), choose your project (e.g. jdoe-5d23dpe).
+ 1. Go to **APIs & Services > Enabled APIs & services**.
+ 1. Select **+ Enable APIs and Services**.
+ 1. Search for `Vertex AI API`.
+ 1. Select **Vertex AI API**, then select **Enable**.
- Install the [`gcloud` CLI](https://cloud.google.com/sdk/docs/install)
- Authenticate locally with GCP using the [`gcloud auth application-default login`](https://cloud.google.com/sdk/gcloud/reference/auth/application-default/login) command.
- Open the Rails console. Update the settings to:
@@ -176,121 +176,121 @@ Therefore, a different setup is required from the [SaaS-only AI features](#test-
### Setup
-1. Setup AI Gateway:
- 1. [Install it](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist#how-to-run-the-server-locally).
- 1. Ensure that the following environment variables are set in the `.env` file:
+1. Set up AI Gateway:
+ 1. [Install it](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist#how-to-run-the-server-locally).
+ 1. Ensure that the following environment variables are set in the `.env` file:
- ```shell
- AIGW_AUTH__BYPASS_EXTERNAL=true
- ANTHROPIC_API_KEY="[REDACTED]" # IMPORTANT: Ensure you use Corp account. See https://gitlab.com/gitlab-org/gitlab/-/issues/435911#note_1701762954.
- AIGW_VERTEX_TEXT_MODEL__PROJECT="[REDACTED]"
- ```
+ ```shell
+ AIGW_AUTH__BYPASS_EXTERNAL=true
+ ANTHROPIC_API_KEY="[REDACTED]" # IMPORTANT: Ensure you use Corp account. See https://gitlab.com/gitlab-org/gitlab/-/issues/435911#note_1701762954.
+ AIGW_VERTEX_TEXT_MODEL__PROJECT="[REDACTED]"
+ ```
- 1. Run `poetry run ai_gateway`.
- 1. Visit [OpenAPI playground](http://0.0.0.0:5052/docs), try an endpoint (e.g. `/v1/chat/agent`) and make sure you get a successful response.
- If something went wrong, check `modelgateway_debug.log` if it contains error information.
+ 1. Run `poetry run ai_gateway`.
+ 1. Visit OpenAPI playground (`http://0.0.0.0:5052/docs`), try an endpoint (e.g. `/v1/chat/agent`) and make sure you get a successful response.
+ If something went wrong, check `modelgateway_debug.log` if it contains error information.
1. Setup GitLab Development Kit (GDK):
- 1. [Install it](https://gitlab.com/gitlab-org/gitlab-development-kit#installation).
- 1. [Set up `gdk.test` hostname](https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/main/doc/index.md#set-up-gdktest-hostname).
- 1. [Activate GitLab Enterprise license](https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/main/doc/index.md#use-gitlab-enterprise-features) (e.g. Ultimate).
- 1. Export these environment variables in the same terminal session with `gdk start`:
+ 1. [Install it](https://gitlab.com/gitlab-org/gitlab-development-kit#installation).
+ 1. [Set up `gdk.test` hostname](https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/main/doc/index.md#set-up-gdktest-hostname).
+ 1. [Activate GitLab Enterprise license](https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/main/doc/index.md#use-gitlab-enterprise-features) (e.g. Ultimate).
+ 1. Export these environment variables in the same terminal session with `gdk start`:
- ```shell
- export CODE_SUGGESTIONS_BASE_URL=http://0.0.0.0:5052 # URL to the local AI Gateway instance
- export LLM_DEBUG=1 # Enable debug logging
- ```
+ ```shell
+ export CODE_SUGGESTIONS_BASE_URL=http://0.0.0.0:5052 # URL to the local AI Gateway instance
+ export LLM_DEBUG=1 # Enable debug logging
+ ```
- Alternatively, you can create an `env.runit` file in the root of your GDK with the above snippet.
- 1. Enable the following feature flags via `gdk rails console`:
+ Alternatively, you can create an `env.runit` file in the root of your GDK with the above snippet.
+ 1. Enable the following feature flags via `gdk rails console`:
- ```ruby
- # NOTE: This feature flag name might be changed. See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/140352.
- ::Feature.enable(:ai_global_switch)
+ ```ruby
+ # NOTE: This feature flag name might be changed. See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/140352.
+ ::Feature.enable(:ai_global_switch)
- # This is to request to AI Gateway instead of built-in Anthropic client. See https://gitlab.com/gitlab-org/gitlab/-/issues/433213 for more info.
- ::Feature.enable(:gitlab_duo_chat_requests_to_ai_gateway)
- ```
+ # This is to request to AI Gateway instead of built-in Anthropic client. See https://gitlab.com/gitlab-org/gitlab/-/issues/433213 for more info.
+ ::Feature.enable(:gitlab_duo_chat_requests_to_ai_gateway)
+ ```
- 1. Create a dummy access token via `gdk rails console` OR skip this step and setup GitLab or Customer Dot as OIDC provider (See the following section):
+ 1. Create a dummy access token via `gdk rails console` OR skip this step and setup GitLab or Customer Dot as OIDC provider (See the following section):
- ```ruby
- # Creating dummy token, and this will work as long as `AIGW_AUTH__BYPASS_EXTERNAL=true` in AI Gateway.
- ::Ai::ServiceAccessToken.create!(token: 'dummy', expires_at: 1.month.from_now)
- ```
+ ```ruby
+ # Creating dummy token, and this will work as long as `AIGW_AUTH__BYPASS_EXTERNAL=true` in AI Gateway.
+ ::Ai::ServiceAccessToken.create!(token: 'dummy', expires_at: 1.month.from_now)
+ ```
- 1. Ensure GitLab-Rails can talk to the AI Gateway. Run `gdk rails console` and execute:
+ 1. Ensure GitLab-Rails can talk to the AI Gateway. Run `gdk rails console` and execute:
- ```ruby
- user = User.first
- Gitlab::Llm::AiGateway::Client.new(user).stream(prompt: "\n\nHuman: Hi, how are you?\n\nAssistant:")
- ```
+ ```ruby
+ user = User.first
+ Gitlab::Llm::AiGateway::Client.new(user).stream(prompt: "\n\nHuman: Hi, how are you?\n\nAssistant:")
+ ```
#### Verify the setup with GraphQL
1. Visit [GraphQL explorer](../../api/graphql/index.md#interactive-graphql-explorer).
1. Execute the `aiAction` mutation. Here is an example:
- ```graphql
- mutation {
- aiAction(
- input: {
- chat: {
- resourceId: "gid://gitlab/User/1",
- content: "Hello"
- }
- }
- ){
- requestId
- errors
- }
- }
- ```
+ ```graphql
+ mutation {
+ aiAction(
+ input: {
+ chat: {
+ resourceId: "gid://gitlab/User/1",
+ content: "Hello"
+ }
+ }
+ ){
+ requestId
+ errors
+ }
+ }
+ ```
1. (GitLab Duo Chat only) Execute the following query to fetch the response:
- ```graphql
- query {
- aiMessages {
- nodes {
- requestId
- content
- role
- timestamp
- chunkId
- errors
- }
- }
- }
- ```
+ ```graphql
+ query {
+ aiMessages {
+ nodes {
+ requestId
+ content
+ role
+ timestamp
+ chunkId
+ errors
+ }
+ }
+ }
+ ```
- If you can't fetch the response, check `graphql_json.log`, `sidekiq_json.log`, `llm.log` or `modelgateway_debug.log` if it contains error information.
+ If you can't fetch the response, check `graphql_json.log`, `sidekiq_json.log`, `llm.log` or `modelgateway_debug.log` if it contains error information.
### Use GitLab as OIDC provider in AI Gateway
1. Reconfigure AI Gateway:
- 1. Additionally, ensure that the following environment variables are set in the `.env` file:
+ 1. Additionally, ensure that the following environment variables are set in the `.env` file:
- ```shell
- AIGW_GITLAB_URL="http://gdk.test:3000/"
- AIGW_GITLAB_API_URL="http://gdk.test:3000/api/v4/"
- AIGW_AUTH__BYPASS_EXTERNAL=False
- ```
+ ```shell
+ AIGW_GITLAB_URL="http://gdk.test:3000/"
+ AIGW_GITLAB_API_URL="http://gdk.test:3000/api/v4/"
+ AIGW_AUTH__BYPASS_EXTERNAL=False
+ ```
- 1. Restart AI Gateway.
+ 1. Restart AI Gateway.
1. Reconfigure GitLab Development Kit (GDK):
- 1. Additionally, export the following environment variables:
+ 1. Additionally, export the following environment variables:
- ```shell
- export GITLAB_SIMULATE_SAAS=1 # Simulate a SaaS instance. See https://docs.gitlab.com/ee/development/ee_features.html#simulate-a-saas-instance.
- ```
+ ```shell
+ export GITLAB_SIMULATE_SAAS=1 # Simulate a SaaS instance. See https://docs.gitlab.com/ee/development/ee_features.html#simulate-a-saas-instance.
+ ```
- 1. Restart GDK.
+ 1. Restart GDK.
### Use Customer Dot as OIDC provider in AI Gateway
1. AI Gateway:
- 1. Ensure `AIGW_CUSTOMER_PORTAL_BASE_URL` in the `.env` file points to your Customer Dot URL.
- 1. Restart
+ 1. Ensure `AIGW_CUSTOMER_PORTAL_BASE_URL` in the `.env` file points to your Customer Dot URL.
+ 1. Restart
## Experimental REST API
@@ -336,7 +336,7 @@ As an example, assume we want to build an "explain code" action. To do this, we
```graphql
mutation {
- aiAction(input: {explainCode: {resourceId: "gid://gitlab/MergeRequest/52", code: "foo() { console.log()" }}) {
+ aiAction(input: {explainCode: {resourceId: "gid://gitlab/MergeRequest/52", code: "foo() { console.log() }" }}) {
clientMutationId
}
}
diff --git a/lib/api/entities/ml/mlflow/search_experiments.rb b/lib/api/entities/ml/mlflow/search_experiments.rb
new file mode 100644
index 00000000000..9673cb1b6fd
--- /dev/null
+++ b/lib/api/entities/ml/mlflow/search_experiments.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Ml
+ module Mlflow
+ class SearchExperiments < Grape::Entity # rubocop:disable Search/NamespacedClass -- Not related to search
+ expose :experiments, with: Experiment
+ expose :next_page_token
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/ml/mlflow/experiments.rb b/lib/api/ml/mlflow/experiments.rb
index 1a501291941..511922782e8 100644
--- a/lib/api/ml/mlflow/experiments.rb
+++ b/lib/api/ml/mlflow/experiments.rb
@@ -47,6 +47,42 @@ module API
present response, with: Entities::Ml::Mlflow::ListExperiment
end
+ desc 'Search experiments' do
+ success Entities::Ml::Mlflow::ListExperiment
+ detail 'https://www.mlflow.org/docs/latest/rest-api.html#list-experiments'
+ end
+ params do
+ optional :max_results,
+ type: Integer,
+ desc: 'Maximum number of experiments to fetch in a page. Default is 200, maximum is 1000.',
+ default: 200
+ optional :order_by,
+ type: String,
+ desc: 'Order criteria. Can be by a column of the experiment (created_at, name).',
+ default: 'created_at DESC'
+ optional :page_token,
+ type: String,
+ desc: 'Token for pagination'
+ optional :filter,
+ type: String,
+ desc: 'This parameter is ignored'
+ end
+ post 'search', urgency: :low do
+ max_results = [params[:max_results], 1000].min
+
+ finder_params = model_order_params(params)
+
+ finder = ::Projects::Ml::ExperimentFinder.new(user_project, finder_params)
+ paginator = finder.execute.keyset_paginate(cursor: params[:page_token], per_page: max_results)
+
+ result = {
+ experiments: paginator.records,
+ next_page_token: paginator.cursor_for_next_page
+ }
+
+ present result, with: Entities::Ml::Mlflow::SearchExperiments
+ end
+
desc 'Create experiment' do
success Entities::Ml::Mlflow::NewExperiment
detail 'https://www.mlflow.org/docs/1.28.0/rest-api.html#create-experiment'
diff --git a/scripts/internal_events/cli/metric.rb b/scripts/internal_events/cli/metric.rb
index 63961d29810..0d39b0b4e2b 100755
--- a/scripts/internal_events/cli/metric.rb
+++ b/scripts/internal_events/cli/metric.rb
@@ -16,7 +16,6 @@ module InternalEventsCli
:data_source,
:data_category,
:product_category,
- :instrumentation_class,
:distribution,
:tier,
:options,
@@ -50,7 +49,6 @@ module InternalEventsCli
.merge(to_h.compact)
.merge(
key_path: key_path,
- instrumentation_class: instrumentation_class,
events: events)
.slice(*NEW_METRIC_FIELDS)
.transform_keys(&:to_s)
@@ -82,10 +80,6 @@ module InternalEventsCli
self[:key_path] ||= "#{key_path_prefix}.#{key}"
end
- def instrumentation_class
- self[:instrumentation_class] ||= identifier ? 'RedisHLLMetric' : 'TotalCountMetric'
- end
-
def events
self[:events] ||= actions.map do |action|
if identifier
@@ -100,10 +94,9 @@ module InternalEventsCli
end
def key_path_prefix
- case instrumentation_class
- when 'RedisHLLMetric'
+ if identifier
'redis_hll_counters'
- when 'TotalCountMetric'
+ else
'counts'
end
end
diff --git a/spec/finders/projects/ml/experiment_finder_spec.rb b/spec/finders/projects/ml/experiment_finder_spec.rb
new file mode 100644
index 00000000000..2cf5086d5db
--- /dev/null
+++ b/spec/finders/projects/ml/experiment_finder_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::Ml::ExperimentFinder, feature_category: :mlops do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:experiment1) { create(:ml_experiments, project: project) }
+ let_it_be(:experiment2) { create(:ml_experiments, project: project) }
+ let_it_be(:experiment3) do
+ create(:ml_experiments, name: "#{experiment1.name}_1", project: project, updated_at: 1.week.ago)
+ end
+
+ let_it_be(:other_experiment) { create(:ml_experiments) }
+ let_it_be(:project_experiments) { [experiment1, experiment2, experiment3] }
+
+ let(:params) { {} }
+
+ subject(:experiments) { described_class.new(project, params).execute.to_a }
+
+ describe 'default params' do
+ it 'returns models for project ordered by id, descending' do
+ is_expected.to eq([experiment3, experiment2, experiment1])
+ end
+
+ it 'including the latest version and project', :aggregate_failures do
+ expect(experiments[0].association_cached?(:project)).to be(true)
+ end
+
+ it 'does not return models belonging to a different project' do
+ is_expected.not_to include(other_experiment)
+ end
+ end
+
+ describe 'sorting' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:test_case, :order_by, :direction, :expected_order) do
+ 'default params' | nil | nil | [2, 1, 0]
+ 'ascending order' | 'id' | 'ASC' | [0, 1, 2]
+ 'by column' | 'name' | 'ASC' | [0, 2, 1]
+ 'invalid sort' | nil | 'UP' | [2, 1, 0]
+ 'invalid order by' | 'INVALID' | nil | [2, 1, 0]
+ 'order by updated_at' | 'updated_at' | nil | [1, 0, 2]
+ end
+ with_them do
+ let(:params) { { order_by: order_by, sort: direction } }
+
+ it { is_expected.to eq(project_experiments.values_at(*expected_order)) }
+ end
+ end
+end
diff --git a/spec/fixtures/api/schemas/ml/search_experiments.json b/spec/fixtures/api/schemas/ml/search_experiments.json
new file mode 100644
index 00000000000..3df7ff1a676
--- /dev/null
+++ b/spec/fixtures/api/schemas/ml/search_experiments.json
@@ -0,0 +1,39 @@
+{
+ "type": "object",
+ "required": [
+ "experiments",
+ "next_page_token"
+ ],
+ "properties": {
+ "experiments": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "experiment_id",
+ "name",
+ "artifact_location",
+ "lifecycle_stage"
+ ],
+ "properties": {
+ "experiment_id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "artifact_location": {
+ "type": "string"
+ },
+ "lifecycle_stage": {
+ "type": "string",
+ "enum": [
+ "active",
+ "deleted"
+ ]
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/spec/fixtures/scripts/internal_events/metrics/ee_total_28d_single_event.yml b/spec/fixtures/scripts/internal_events/metrics/ee_total_28d_single_event.yml
index ba56d782871..5238e997044 100644
--- a/spec/fixtures/scripts/internal_events/metrics/ee_total_28d_single_event.yml
+++ b/spec/fixtures/scripts/internal_events/metrics/ee_total_28d_single_event.yml
@@ -12,7 +12,6 @@ introduced_by_url: TODO
time_frame: 28d
data_source: internal_events
data_category: optional
-instrumentation_class: TotalCountMetric
distribution:
- ee
tier:
diff --git a/spec/fixtures/scripts/internal_events/metrics/ee_total_7d_single_event.yml b/spec/fixtures/scripts/internal_events/metrics/ee_total_7d_single_event.yml
index e6bdcb9d2ae..fdbf137f699 100644
--- a/spec/fixtures/scripts/internal_events/metrics/ee_total_7d_single_event.yml
+++ b/spec/fixtures/scripts/internal_events/metrics/ee_total_7d_single_event.yml
@@ -12,7 +12,6 @@ introduced_by_url: TODO
time_frame: 7d
data_source: internal_events
data_category: optional
-instrumentation_class: TotalCountMetric
distribution:
- ee
tier:
diff --git a/spec/fixtures/scripts/internal_events/metrics/ee_total_single_event.yml b/spec/fixtures/scripts/internal_events/metrics/ee_total_single_event.yml
index b1bf89dc095..e928869ca9a 100644
--- a/spec/fixtures/scripts/internal_events/metrics/ee_total_single_event.yml
+++ b/spec/fixtures/scripts/internal_events/metrics/ee_total_single_event.yml
@@ -12,7 +12,6 @@ introduced_by_url: TODO
time_frame: all
data_source: internal_events
data_category: optional
-instrumentation_class: TotalCountMetric
distribution:
- ee
tier:
diff --git a/spec/fixtures/scripts/internal_events/metrics/keyboard_smashed_metric_28d.yml b/spec/fixtures/scripts/internal_events/metrics/keyboard_smashed_metric_28d.yml
index 8476cb8561b..4d40e2122cb 100644
--- a/spec/fixtures/scripts/internal_events/metrics/keyboard_smashed_metric_28d.yml
+++ b/spec/fixtures/scripts/internal_events/metrics/keyboard_smashed_metric_28d.yml
@@ -12,7 +12,6 @@ introduced_by_url: TODO
time_frame: 28d
data_source: internal_events
data_category: optional
-instrumentation_class: RedisHLLMetric
distribution:
- ce
- ee
diff --git a/spec/fixtures/scripts/internal_events/metrics/keyboard_smashed_metric_7d.yml b/spec/fixtures/scripts/internal_events/metrics/keyboard_smashed_metric_7d.yml
index b4cc2fc8b55..166cef90412 100644
--- a/spec/fixtures/scripts/internal_events/metrics/keyboard_smashed_metric_7d.yml
+++ b/spec/fixtures/scripts/internal_events/metrics/keyboard_smashed_metric_7d.yml
@@ -12,7 +12,6 @@ introduced_by_url: TODO
time_frame: 7d
data_source: internal_events
data_category: optional
-instrumentation_class: RedisHLLMetric
distribution:
- ce
- ee
diff --git a/spec/fixtures/scripts/internal_events/metrics/project_id_28d_multiple_events.yml b/spec/fixtures/scripts/internal_events/metrics/project_id_28d_multiple_events.yml
index 754702c8c74..122043e6cc0 100644
--- a/spec/fixtures/scripts/internal_events/metrics/project_id_28d_multiple_events.yml
+++ b/spec/fixtures/scripts/internal_events/metrics/project_id_28d_multiple_events.yml
@@ -12,7 +12,6 @@ introduced_by_url: TODO
time_frame: 28d
data_source: internal_events
data_category: optional
-instrumentation_class: RedisHLLMetric
distribution:
- ce
- ee
diff --git a/spec/fixtures/scripts/internal_events/metrics/project_id_7d_multiple_events.yml b/spec/fixtures/scripts/internal_events/metrics/project_id_7d_multiple_events.yml
index 95f429e9b40..11a4ba41c07 100644
--- a/spec/fixtures/scripts/internal_events/metrics/project_id_7d_multiple_events.yml
+++ b/spec/fixtures/scripts/internal_events/metrics/project_id_7d_multiple_events.yml
@@ -12,7 +12,6 @@ introduced_by_url: TODO
time_frame: 7d
data_source: internal_events
data_category: optional
-instrumentation_class: RedisHLLMetric
distribution:
- ce
- ee
diff --git a/spec/fixtures/scripts/internal_events/metrics/total_single_event.yml b/spec/fixtures/scripts/internal_events/metrics/total_single_event.yml
index 5bdb4c45a52..038fc738f25 100644
--- a/spec/fixtures/scripts/internal_events/metrics/total_single_event.yml
+++ b/spec/fixtures/scripts/internal_events/metrics/total_single_event.yml
@@ -12,7 +12,6 @@ introduced_by_url: TODO
time_frame: all
data_source: internal_events
data_category: optional
-instrumentation_class: TotalCountMetric
distribution:
- ce
- ee
diff --git a/spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event.yml b/spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event.yml
index b176b23b46a..b27e69bd43b 100644
--- a/spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event.yml
+++ b/spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event.yml
@@ -12,7 +12,6 @@ introduced_by_url: TODO
time_frame: 28d
data_source: internal_events
data_category: optional
-instrumentation_class: RedisHLLMetric
distribution:
- ce
- ee
diff --git a/spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event.yml b/spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event.yml
index 8a0fca2cbdc..e08733a6bc9 100644
--- a/spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event.yml
+++ b/spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event.yml
@@ -12,7 +12,6 @@ introduced_by_url: TODO
time_frame: 7d
data_source: internal_events
data_category: optional
-instrumentation_class: RedisHLLMetric
distribution:
- ce
- ee
diff --git a/spec/models/ml/experiment_spec.rb b/spec/models/ml/experiment_spec.rb
index 36bdb611833..1864c04d2fd 100644
--- a/spec/models/ml/experiment_spec.rb
+++ b/spec/models/ml/experiment_spec.rb
@@ -37,6 +37,20 @@ RSpec.describe Ml::Experiment, feature_category: :mlops do
end
end
+ describe '.by_project' do
+ subject { described_class.by_project(exp.project) }
+
+ it { is_expected.to match_array([exp, exp2]) }
+ end
+
+ describe '.including_project' do
+ subject { described_class.including_project }
+
+ it 'loads latest version' do
+ expect(subject.first.association_cached?(:project)).to be(true)
+ end
+ end
+
describe '#by_project_id_and_iid' do
subject { described_class.by_project_id_and_iid(exp.project_id, iid) }
diff --git a/spec/requests/api/ml/mlflow/experiments_spec.rb b/spec/requests/api/ml/mlflow/experiments_spec.rb
index 409b4529699..ac2d5539408 100644
--- a/spec/requests/api/ml/mlflow/experiments_spec.rb
+++ b/spec/requests/api/ml/mlflow/experiments_spec.rb
@@ -207,4 +207,81 @@ RSpec.describe API::Ml::Mlflow::Experiments, feature_category: :mlops do
it_behaves_like 'MLflow|Bad Request on missing required', [:key, :value]
end
end
+
+ describe 'GET /projects/:id/ml/mlflow/api/2.0/mlflow/experiments/search' do
+ let_it_be(:experiment_b) do
+ create(:ml_experiments, project: project, name: "#{experiment.name}_2")
+ end
+
+ let_it_be(:experiment_c) do
+ create(:ml_experiments, project: project, name: "#{experiment.name}_1")
+ end
+
+ let(:order_by) { nil }
+ let(:default_params) do
+ {
+ 'max_results' => 2,
+ 'order_by' => order_by
+ }
+ end
+
+ let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/experiments/search" }
+ let(:request) { post api(route), params: default_params.merge(**params), headers: headers }
+
+ it 'returns all the models', :aggregate_failures do
+ is_expected.to have_gitlab_http_status(:ok)
+ is_expected.to match_response_schema('ml/search_experiments')
+ expect(json_response["experiments"].count).to be(2)
+ end
+
+ describe 'pagination and ordering' do
+ RSpec.shared_examples 'a paginated search experiments request with order' do
+ it 'paginates respecting the provided order by' do
+ first_page_experiments = json_response['experiments']
+ expect(first_page_experiments.size).to eq(2)
+
+ expect(first_page_experiments[0]['experiment_id'].to_i).to eq(expected_order[0].iid)
+ expect(first_page_experiments[1]['experiment_id'].to_i).to eq(expected_order[1].iid)
+
+ params = default_params.merge(page_token: json_response['next_page_token'])
+
+ post api(route), params: params, headers: headers
+
+ second_page_response = Gitlab::Json.parse(response.body)
+ second_page_experiments = second_page_response['experiments']
+
+ expect(second_page_response['next_page_token']).to be_nil
+ expect(second_page_experiments.size).to eq(1)
+ expect(second_page_experiments[0]['experiment_id'].to_i).to eq(expected_order[2].iid)
+ end
+ end
+
+ let(:default_order) { [experiment_c, experiment_b, experiment] }
+
+ context 'when ordering is not provided' do
+ let(:expected_order) { default_order }
+
+ it_behaves_like 'a paginated search experiments request with order'
+ end
+
+ context 'when order by column is provided', 'and column exists' do
+ let(:order_by) { 'name ASC' }
+ let(:expected_order) { [experiment, experiment_c, experiment_b] }
+
+ it_behaves_like 'a paginated search experiments request with order'
+ end
+
+ context 'when order by column is provided', 'and column does not exist' do
+ let(:order_by) { 'something DESC' }
+ let(:expected_order) { default_order }
+
+ it_behaves_like 'a paginated search experiments request with order'
+ end
+ end
+
+ describe 'Error States' do
+ it_behaves_like 'MLflow|shared error cases'
+ it_behaves_like 'MLflow|Requires api scope and write permission'
+ end
+ end
end
diff --git a/spec/services/work_items/widgets/start_and_due_date_service/update_service_spec.rb b/spec/services/work_items/callbacks/start_and_due_date_spec.rb
index f9708afd313..b26a33976fa 100644
--- a/spec/services/work_items/widgets/start_and_due_date_service/update_service_spec.rb
+++ b/spec/services/work_items/callbacks/start_and_due_date_spec.rb
@@ -2,19 +2,19 @@
require 'spec_helper'
-RSpec.describe WorkItems::Widgets::StartAndDueDateService::UpdateService, feature_category: :portfolio_management do
+RSpec.describe WorkItems::Callbacks::StartAndDueDate, feature_category: :portfolio_management do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user).tap { |user| project.add_reporter(user) } }
let_it_be_with_reload(:work_item) { create(:work_item, project: project) }
- let(:widget) { work_item.widgets.find { |widget| widget.is_a?(WorkItems::Widgets::StartAndDueDate) } }
+ let(:widget) { work_item.widgets.find { |widget| widget.is_a?(WorkItems::Callbacks::StartAndDueDate) } }
describe '#before_update_callback' do
let(:start_date) { Date.today }
let(:due_date) { 1.week.from_now.to_date }
- let(:service) { described_class.new(widget: widget, current_user: user) }
+ let(:service) { described_class.new(issuable: work_item, current_user: user, params: params) }
- subject(:update_params) { service.before_update_callback(params: params) }
+ subject(:update_params) { service.before_update }
context 'when start and due date params are present' do
let(:params) { { start_date: Date.today, due_date: 1.week.from_now.to_date } }
@@ -22,8 +22,8 @@ RSpec.describe WorkItems::Widgets::StartAndDueDateService::UpdateService, featur
it 'correctly sets date values' do
expect do
update_params
- end.to change(work_item, :start_date).from(nil).to(start_date).and(
- change(work_item, :due_date).from(nil).to(due_date)
+ end.to change { work_item.start_date }.from(nil).to(start_date).and(
+ change { work_item.due_date }.from(nil).to(due_date)
)
end
@@ -59,7 +59,7 @@ RSpec.describe WorkItems::Widgets::StartAndDueDateService::UpdateService, featur
it 'sets only one date to null' do
expect do
update_params
- end.to change(work_item, :start_date).from(start_date).to(nil).and(
+ end.to change { work_item.start_date }.from(start_date).to(nil).and(
not_change(work_item, :due_date).from(due_date)
)
end
@@ -70,15 +70,15 @@ RSpec.describe WorkItems::Widgets::StartAndDueDateService::UpdateService, featur
let(:params) { {} }
before do
- allow(service).to receive(:new_type_excludes_widget?).and_return(true)
+ allow(service).to receive(:excluded_in_new_type?).and_return(true)
work_item.update!(start_date: start_date, due_date: due_date)
end
it 'sets both dates to null' do
expect do
update_params
- end.to change(work_item, :start_date).from(start_date).to(nil).and(
- change(work_item, :due_date).from(due_date).to(nil)
+ end.to change { work_item.start_date }.from(start_date).to(nil).and(
+ change { work_item.due_date }.from(due_date).to(nil)
)
end
end