diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2024-01-15 09:10:15 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2024-01-15 09:10:15 +0300 |
commit | 500f4288e71e39d6cabbb0faad0e6170a8e792b4 (patch) | |
tree | 0724c43280d0c01f24f19a73febd7e1c6ffb91cc | |
parent | d0830d520a7eb2be16338f7f36158b522deb68ec (diff) |
Add latest changes from gitlab-org/gitlab@master
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 |