Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
4ee706fcd1
commit
08489a6db8
|
|
@ -80,7 +80,7 @@ export default {
|
|||
@click="createNewItem('blob')"
|
||||
/>
|
||||
</li>
|
||||
<li><upload :path="path" @create="createTempEntry" /></li>
|
||||
<upload :path="path" @create="createTempEntry" />
|
||||
<li>
|
||||
<item-button
|
||||
:label="__('New directory')"
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<li>
|
||||
<item-button
|
||||
:class="buttonCssClasses"
|
||||
:show-label="showLabel"
|
||||
|
|
@ -84,5 +84,5 @@ export default {
|
|||
data-qa-selector="file_upload_field"
|
||||
@change="openFile"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ export const defaultDiffEditorOptions = {
|
|||
};
|
||||
|
||||
export const defaultModelOptions = {
|
||||
endOfLine: 0,
|
||||
insertFinalNewline: true,
|
||||
trimTrailingWhitespace: false,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
<script>
|
||||
import { GlTable } from '@gitlab/ui';
|
||||
import IncubationAlert from './incubation_alert.vue';
|
||||
|
||||
export default {
|
||||
name: 'ShowMlExperiment',
|
||||
components: {
|
||||
GlTable,
|
||||
IncubationAlert,
|
||||
},
|
||||
inject: ['candidates', 'metricNames', 'paramNames'],
|
||||
computed: {
|
||||
fields() {
|
||||
return [...this.paramNames, ...this.metricNames];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<incubation-alert />
|
||||
|
||||
<h3>
|
||||
{{ __('Experiment Candidates') }}
|
||||
</h3>
|
||||
|
||||
<gl-table
|
||||
:fields="fields"
|
||||
:items="candidates"
|
||||
:empty-text="__('This Experiment has no logged Candidates')"
|
||||
show-empty
|
||||
class="gl-mt-0!"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -8,8 +8,8 @@ export default {
|
|||
contentLabel: __(
|
||||
'GitLab incubates features to explore new use cases. These features are updated regularly, and support is limited',
|
||||
),
|
||||
learnMoreLabel: __('Learn More'),
|
||||
feedbackLabel: __('Feedback and Updates'),
|
||||
learnMoreLabel: __('Learn more'),
|
||||
feedbackLabel: __('Feedback'),
|
||||
},
|
||||
name: 'MlopsIncubationAlert',
|
||||
components: { GlAlert, GlLink },
|
||||
|
|
@ -37,7 +37,7 @@ export default {
|
|||
:title="$options.i18n.titleLabel"
|
||||
variant="warning"
|
||||
:primary-button-text="$options.i18n.feedbackLabel"
|
||||
primary-button-link="https://gitlab.com/groups/gitlab-org/-/epics/8560"
|
||||
primary-button-link="https://gitlab.com/gitlab-org/gitlab/-/issues/381660"
|
||||
@dismiss="dismissAlert"
|
||||
>
|
||||
{{ $options.i18n.contentLabel }}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
<script>
|
||||
import { GlLink } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import IncubationAlert from './incubation_alert.vue';
|
||||
|
||||
export default {
|
||||
name: 'MlCandidate',
|
||||
components: {
|
||||
IncubationAlert,
|
||||
GlLink,
|
||||
},
|
||||
inject: ['candidate'],
|
||||
i18n: {
|
||||
titleLabel: __('Model candidate details'),
|
||||
infoLabel: __('Info'),
|
||||
idLabel: __('ID'),
|
||||
statusLabel: __('Status'),
|
||||
experimentLabel: __('Experiment'),
|
||||
artifactsLabel: __('Artifacts'),
|
||||
parametersLabel: __('Parameters'),
|
||||
metricsLabel: __('Metrics'),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<incubation-alert />
|
||||
|
||||
<h3>
|
||||
{{ $options.i18n.titleLabel }}
|
||||
</h3>
|
||||
|
||||
<table class="candidate-details">
|
||||
<tbody>
|
||||
<tr class="divider"></tr>
|
||||
|
||||
<tr>
|
||||
<td class="gl-text-secondary gl-font-weight-bold">{{ $options.i18n.infoLabel }}</td>
|
||||
<td class="gl-font-weight-bold">{{ $options.i18n.idLabel }}</td>
|
||||
<td>{{ candidate.info.iid }}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td></td>
|
||||
<td class="gl-font-weight-bold">{{ $options.i18n.statusLabel }}</td>
|
||||
<td>{{ candidate.info.status }}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td></td>
|
||||
<td class="gl-font-weight-bold">{{ $options.i18n.experimentLabel }}</td>
|
||||
<td>
|
||||
<gl-link :href="candidate.info.path_to_experiment">{{
|
||||
candidate.info.experiment_name
|
||||
}}</gl-link>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr v-if="candidate.info.path_to_artifact">
|
||||
<td></td>
|
||||
<td class="gl-font-weight-bold">{{ $options.i18n.artifactsLabel }}</td>
|
||||
<td>
|
||||
<gl-link :href="candidate.info.path_to_artifact">{{
|
||||
$options.i18n.artifactsLabel
|
||||
}}</gl-link>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr class="divider"></tr>
|
||||
|
||||
<tr v-for="(param, index) in candidate.params" :key="param.name">
|
||||
<td v-if="index == 0" class="gl-text-secondary gl-font-weight-bold">
|
||||
{{ $options.i18n.parametersLabel }}
|
||||
</td>
|
||||
<td v-else></td>
|
||||
<td class="gl-font-weight-bold">{{ param.name }}</td>
|
||||
<td>{{ param.value }}</td>
|
||||
</tr>
|
||||
|
||||
<tr class="divider"></tr>
|
||||
|
||||
<tr v-for="(metric, index) in candidate.metrics" :key="metric.name">
|
||||
<td v-if="index == 0" class="gl-text-secondary gl-font-weight-bold">
|
||||
{{ $options.i18n.metricsLabel }}
|
||||
</td>
|
||||
<td v-else></td>
|
||||
<td class="gl-font-weight-bold">{{ metric.name }}</td>
|
||||
<td>{{ metric.value }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
<script>
|
||||
import { GlTable, GlLink } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import IncubationAlert from './incubation_alert.vue';
|
||||
|
||||
export default {
|
||||
name: 'MlExperiment',
|
||||
components: {
|
||||
GlTable,
|
||||
GlLink,
|
||||
IncubationAlert,
|
||||
},
|
||||
inject: ['candidates', 'metricNames', 'paramNames'],
|
||||
computed: {
|
||||
fields() {
|
||||
return [
|
||||
...this.paramNames,
|
||||
...this.metricNames,
|
||||
{ key: 'details', label: '' },
|
||||
{ key: 'artifact', label: '' },
|
||||
];
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
titleLabel: __('Experiment candidates'),
|
||||
emptyStateLabel: __('This experiment has no logged candidates'),
|
||||
artifactsLabel: __('Artifacts'),
|
||||
detailsLabel: __('Details'),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<incubation-alert />
|
||||
|
||||
<h3>
|
||||
{{ $options.i18n.titleLabel }}
|
||||
</h3>
|
||||
|
||||
<gl-table
|
||||
:fields="fields"
|
||||
:items="candidates"
|
||||
:empty-text="$options.i18n.emptyStateLabel"
|
||||
show-empty
|
||||
class="gl-mt-0!"
|
||||
>
|
||||
<template #cell(artifact)="data">
|
||||
<gl-link v-if="data.value" :href="data.value" target="_blank">{{
|
||||
$options.i18n.artifactsLabel
|
||||
}}</gl-link>
|
||||
</template>
|
||||
|
||||
<template #cell(details)="data">
|
||||
<gl-link :href="data.value">{{ $options.i18n.detailsLabel }}</gl-link>
|
||||
</template>
|
||||
</gl-table>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import Vue from 'vue';
|
||||
import MlCandidate from '~/ml/experiment_tracking/components/ml_candidate.vue';
|
||||
|
||||
const initShowCandidate = () => {
|
||||
const element = document.querySelector('#js-show-ml-candidate');
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const container = document.createElement('div');
|
||||
element.appendChild(container);
|
||||
|
||||
const candidate = JSON.parse(element.dataset.candidate);
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el: container,
|
||||
provide: {
|
||||
candidate,
|
||||
},
|
||||
render(h) {
|
||||
return h(MlCandidate);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
initShowCandidate();
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import Vue from 'vue';
|
||||
import ShowExperiment from '~/ml/experiment_tracking/components/experiment.vue';
|
||||
import MlExperiment from '~/ml/experiment_tracking/components/ml_experiment.vue';
|
||||
|
||||
const initShowExperiment = () => {
|
||||
const element = document.querySelector('#js-show-ml-experiment');
|
||||
|
|
@ -23,7 +23,7 @@ const initShowExperiment = () => {
|
|||
paramNames,
|
||||
},
|
||||
render(h) {
|
||||
return h(ShowExperiment);
|
||||
return h(MlExperiment);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,3 +14,9 @@
|
|||
color: $gl-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
table.candidate-details {
|
||||
td {
|
||||
padding: $gl-spacing-scale-3;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Projects
|
||||
module Ml
|
||||
class CandidatesController < ApplicationController
|
||||
before_action :check_feature_flag
|
||||
|
||||
feature_category :mlops
|
||||
|
||||
def show
|
||||
@candidate = ::Ml::Candidate.with_project_id_and_iid(@project.id, params['iid'])
|
||||
|
||||
render_404 unless @candidate.present?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_feature_flag
|
||||
render_404 unless Feature.enabled?(:ml_experiment_tracking, @project)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -3,7 +3,6 @@
|
|||
module Projects
|
||||
module Ml
|
||||
class ExperimentsController < ::Projects::ApplicationController
|
||||
include Projects::Ml::ExperimentsHelper
|
||||
before_action :check_feature_flag
|
||||
|
||||
feature_category :mlops
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ module Types
|
|||
:admin_wiki, :admin_project, :update_pages,
|
||||
:admin_remote_mirror, :create_label, :update_wiki, :destroy_wiki,
|
||||
:create_pages, :destroy_pages, :read_pages_content, :admin_operations,
|
||||
:read_merge_request, :read_design, :create_design, :destroy_design
|
||||
:read_merge_request, :read_design, :create_design, :destroy_design,
|
||||
:read_environment
|
||||
|
||||
permission_field :create_snippet
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ module Projects
|
|||
items = candidates.map do |candidate|
|
||||
{
|
||||
**candidate.params.to_h { |p| [p.name, p.value] },
|
||||
**candidate.latest_metrics.to_h { |m| [m.name, number_with_precision(m.value, precision: 4)] }
|
||||
**candidate.latest_metrics.to_h { |m| [m.name, number_with_precision(m.value, precision: 4)] },
|
||||
artifact: link_to_artifact(candidate),
|
||||
details: link_to_details(candidate)
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -19,6 +21,42 @@ module Projects
|
|||
def unique_logged_names(candidates, &selector)
|
||||
Gitlab::Json.generate(candidates.flat_map(&selector).map(&:name).uniq)
|
||||
end
|
||||
|
||||
def candidate_as_data(candidate)
|
||||
data = {
|
||||
params: candidate.params,
|
||||
metrics: candidate.latest_metrics,
|
||||
info: {
|
||||
iid: candidate.iid,
|
||||
path_to_artifact: link_to_artifact(candidate),
|
||||
experiment_name: candidate.experiment.name,
|
||||
path_to_experiment: link_to_experiment(candidate),
|
||||
status: candidate.status
|
||||
}
|
||||
}
|
||||
|
||||
Gitlab::Json.generate(data)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def link_to_artifact(candidate)
|
||||
artifact = candidate.artifact
|
||||
|
||||
return unless artifact.present?
|
||||
|
||||
project_package_path(candidate.experiment.project, artifact)
|
||||
end
|
||||
|
||||
def link_to_details(candidate)
|
||||
project_ml_candidate_path(candidate.experiment.project, candidate.iid)
|
||||
end
|
||||
|
||||
def link_to_experiment(candidate)
|
||||
experiment = candidate.experiment
|
||||
|
||||
project_ml_experiment_path(experiment.project, experiment.iid)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@
|
|||
class GenericCommitStatus < CommitStatus
|
||||
EXTERNAL_STAGE_IDX = 1_000_000
|
||||
|
||||
before_validation :set_default_values
|
||||
|
||||
validates :target_url, addressable_url: true,
|
||||
length: { maximum: 255 },
|
||||
allow_nil: true
|
||||
|
|
@ -13,12 +11,6 @@ class GenericCommitStatus < CommitStatus
|
|||
# GitHub compatible API
|
||||
alias_attribute :context, :name
|
||||
|
||||
def set_default_values
|
||||
self.context ||= 'default'
|
||||
self.stage ||= 'external'
|
||||
self.stage_idx ||= EXTERNAL_STAGE_IDX
|
||||
end
|
||||
|
||||
def tags
|
||||
[:external]
|
||||
end
|
||||
|
|
|
|||
|
|
@ -19,7 +19,21 @@ module Ml
|
|||
scope :including_metrics_and_params, -> { includes(:latest_metrics, :params) }
|
||||
|
||||
def artifact_root
|
||||
"/ml_candidate_#{iid}/-/"
|
||||
"/#{package_name}/#{package_version}/"
|
||||
end
|
||||
|
||||
def artifact
|
||||
::Packages::Generic::PackageFinder.new(experiment.project).execute!(package_name, package_version)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
nil
|
||||
end
|
||||
|
||||
def package_name
|
||||
"ml_candidate_#{iid}"
|
||||
end
|
||||
|
||||
def package_version
|
||||
'-'
|
||||
end
|
||||
|
||||
class << self
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
- experiment = @candidate.experiment
|
||||
- add_to_breadcrumbs _("Experiments"), project_ml_experiments_path(@project)
|
||||
- add_to_breadcrumbs experiment.name, project_ml_experiment_path(@project, experiment.iid)
|
||||
- breadcrumb_title "Candidate #{@candidate.iid}"
|
||||
- data = candidate_as_data(@candidate)
|
||||
|
||||
#js-show-ml-candidate{ data: { candidate: data } }
|
||||
|
|
@ -475,6 +475,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
|
|||
|
||||
namespace :ml do
|
||||
resources :experiments, only: [:index, :show], controller: 'experiments'
|
||||
resources :candidates, only: [:show], controller: 'candidates', param: :iid
|
||||
end
|
||||
end
|
||||
# End of the /-/ scope.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class QueueResetStatusOnContainerRepositories < Gitlab::Database::Migration[2.0]
|
||||
MIGRATION = 'ResetStatusOnContainerRepositories'
|
||||
DELAY_INTERVAL = 2.minutes
|
||||
BATCH_SIZE = 50
|
||||
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||
|
||||
def up
|
||||
return unless ::Gitlab.config.registry.enabled
|
||||
|
||||
queue_batched_background_migration(
|
||||
MIGRATION,
|
||||
:container_repositories,
|
||||
:id,
|
||||
job_interval: DELAY_INTERVAL,
|
||||
sub_batch_size: BATCH_SIZE
|
||||
)
|
||||
end
|
||||
|
||||
def down
|
||||
delete_batched_background_migration(MIGRATION, :container_repositories, :id, [])
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
1a0a090433dd422b1bd9efdb56f82c02af8bab45b1a651b51a6ed224d823964c
|
||||
|
|
@ -1255,7 +1255,7 @@ To do the switch on **all** PgBouncer nodes:
|
|||
```
|
||||
|
||||
1. Run `gitlab-ctl reconfigure`.
|
||||
1. You must also run `rm /var/opt/gitlab/consul/watcher_postgresql.json`.
|
||||
1. You must also run `rm /var/opt/gitlab/consul/config.d/watcher_postgresql.json`.
|
||||
This is a [known issue](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/7293).
|
||||
|
||||
#### Clean up
|
||||
|
|
|
|||
|
|
@ -18263,6 +18263,7 @@ Returns [`UserMergeRequestInteraction`](#usermergerequestinteraction).
|
|||
| <a id="projectpermissionsreadcommitstatus"></a>`readCommitStatus` | [`Boolean!`](#boolean) | Indicates the user can perform `read_commit_status` on this resource. |
|
||||
| <a id="projectpermissionsreadcycleanalytics"></a>`readCycleAnalytics` | [`Boolean!`](#boolean) | Indicates the user can perform `read_cycle_analytics` on this resource. |
|
||||
| <a id="projectpermissionsreaddesign"></a>`readDesign` | [`Boolean!`](#boolean) | Indicates the user can perform `read_design` on this resource. |
|
||||
| <a id="projectpermissionsreadenvironment"></a>`readEnvironment` | [`Boolean!`](#boolean) | Indicates the user can perform `read_environment` on this resource. |
|
||||
| <a id="projectpermissionsreadmergerequest"></a>`readMergeRequest` | [`Boolean!`](#boolean) | Indicates the user can perform `read_merge_request` on this resource. |
|
||||
| <a id="projectpermissionsreadpagescontent"></a>`readPagesContent` | [`Boolean!`](#boolean) | Indicates the user can perform `read_pages_content` on this resource. |
|
||||
| <a id="projectpermissionsreadproject"></a>`readProject` | [`Boolean!`](#boolean) | Indicates the user can perform `read_project` on this resource. |
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ PUT /projects/:id/invitations/:email
|
|||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project or group](index.md#namespaced-path-encoding) owned by the authenticated user. |
|
||||
| `email` | string | yes | The email address to which the invitation was previously sent. |
|
||||
| `email` | string | yes | The email address the invitation was previously sent to. |
|
||||
| `access_level` | integer | no | A valid access level (defaults: `30`, the Developer role). |
|
||||
| `expires_at` | string | no | A date string in ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`). |
|
||||
|
||||
|
|
|
|||
|
|
@ -69,6 +69,12 @@ For example, `Create an issue when you want to track bugs or future work`.
|
|||
To start the task steps, use a succinct action followed by a colon.
|
||||
For example, `To create an issue:`
|
||||
|
||||
## Task prerequisites
|
||||
|
||||
As a best practice, if the task requires the user to have a role other than Guest,
|
||||
put the minimum role in the prerequisites. See [the Word list](../styleguide/word_list.md) for
|
||||
how to write the phrase for each role.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [View the format for writing task steps](../styleguide/index.md#navigation).
|
||||
|
|
|
|||
|
|
@ -355,7 +355,12 @@ To generate these known events for a single widget:
|
|||
1. `redis_slot` = `code_review`
|
||||
1. `category` = `code_review`
|
||||
1. `aggregation` = `weekly`
|
||||
1. Add each event to the appropriate aggregates in `config/metrics/aggregates/code_review.yml`
|
||||
1. Add each event (those listed in the command in step 7, replacing `test_reports`
|
||||
with the appropriate name slug) to the aggregate files:
|
||||
1. `config/metrics/counts_7d/{timestamp}_code_review_category_monthly_active_users.yml`
|
||||
1. `config/metrics/counts_7d/{timestamp}_code_review_group_monthly_active_users.yml`
|
||||
1. `config/metrics/counts_28d/{timestamp}_code_review_category_monthly_active_users.yml`
|
||||
1. `config/metrics/counts_28d/{timestamp}_code_review_group_monthly_active_users.yml`
|
||||
|
||||
### Add new events
|
||||
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ This template should be used in a new, empty project, with a `.gitlab-ci.yml` fi
|
|||
|
||||
```yaml
|
||||
include:
|
||||
- template: Secure-Binaries.gitlab-ci.yml
|
||||
- template: Security/Secure-Binaries.gitlab-ci.yml
|
||||
```
|
||||
|
||||
The pipeline downloads the Docker images needed for the Security Scanners and saves them as
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 61 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 150 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 44 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 75 KiB |
|
|
@ -16,9 +16,11 @@ engineering, and so on, to improve the performance of the model. Keeping track o
|
|||
artifacts so that the data scientist can later replicate the experiment is not trivial. Machine learning experiment
|
||||
tracking enables them to log parameters, metrics, and artifacts directly into GitLab, giving easy access later on.
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||
## What is an experiment?
|
||||
|
||||
|
|
@ -53,13 +55,15 @@ integration. More information on how to use GitLab as a backend for MLFlow Clien
|
|||
### Exploring model candidates
|
||||
|
||||
To list the current active experiments, navigate to `https/-/ml/experiments`. To display all trials
|
||||
that have been logged, along with their metrics and parameters, selecting an experiment.
|
||||
that have been logged, along with their metrics and parameters, select an experiment. To display details for a candidate,
|
||||
select **Details**.
|
||||
|
||||
### Logging artifacts
|
||||
|
||||
Trial artifacts are saved as [generic packages](../../../packages/generic_packages/index.md), and follow all their
|
||||
conventions. After an artifact is logged for a candidate, all artifacts logged for the candidate are listed in the
|
||||
package registry. The package name for a candidate is `ml_candidate_<candidate_id>`, with version `-`.
|
||||
package registry. The package name for a candidate is `ml_candidate_<candidate_id>`, with version `-`. The link to the
|
||||
artifacts can also be accessed from the **Experiment Candidates** list or **Candidate detail**.
|
||||
|
||||
### Limitations and future
|
||||
|
||||
|
|
@ -72,4 +76,6 @@ On GitLab.com, this feature is currently on private testing.
|
|||
|
||||
## Feedback, roadmap and reports
|
||||
|
||||
For updates on the development, feedback and bug reports, refer to the [development epic](https://gitlab.com/groups/gitlab-org/-/epics/8560).
|
||||
For updates on the development, refer to the [development epic](https://gitlab.com/groups/gitlab-org/-/epics/8560).
|
||||
|
||||
For feedback, bug reports and feature requests, refer to the [feedback issue](https://gitlab.com/gitlab-org/gitlab/-/issues/381660).
|
||||
|
|
|
|||
|
|
@ -125,7 +125,8 @@ module API
|
|||
user: current_user,
|
||||
protected: user_project.protected_for?(ref),
|
||||
ci_stage: stage,
|
||||
stage_idx: stage.position
|
||||
stage_idx: stage.position,
|
||||
stage: 'external'
|
||||
)
|
||||
|
||||
updatable_optional_attributes = %w[target_url description coverage]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,139 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module BackgroundMigration
|
||||
# A job that:
|
||||
# * pickup container repositories with delete_scheduled status.
|
||||
# * check if there are tags linked to it.
|
||||
# * if there are tags, reset the status to nil.
|
||||
class ResetStatusOnContainerRepositories < BatchedMigrationJob
|
||||
DELETE_SCHEDULED_STATUS = 0
|
||||
DUMMY_TAGS = %w[tag].freeze
|
||||
MIGRATOR = 'ResetStatusOnContainerRepositories'
|
||||
|
||||
scope_to ->(relation) { relation.where(status: DELETE_SCHEDULED_STATUS) }
|
||||
operation_name :reset_status_on_container_repositories
|
||||
|
||||
def perform
|
||||
each_sub_batch do |sub_batch|
|
||||
reset_status_if_tags(sub_batch)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def reset_status_if_tags(container_repositories)
|
||||
container_repositories_with_tags = container_repositories.select { |cr| cr.becomes(ContainerRepository).tags? } # rubocop:disable Cop/AvoidBecomes
|
||||
|
||||
ContainerRepository.where(id: container_repositories_with_tags.map(&:id))
|
||||
.update_all(status: nil)
|
||||
end
|
||||
|
||||
# rubocop:disable Style/Documentation
|
||||
module Routable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
has_one :route,
|
||||
as: :source,
|
||||
class_name: '::Gitlab::BackgroundMigration::ResetStatusOnContainerRepositories::Route'
|
||||
end
|
||||
|
||||
def full_path
|
||||
route&.path || build_full_path
|
||||
end
|
||||
|
||||
def build_full_path
|
||||
if parent && path
|
||||
"#{parent.full_path}/#{path}"
|
||||
else
|
||||
path
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Route < ::ApplicationRecord
|
||||
self.table_name = 'routes'
|
||||
end
|
||||
|
||||
class Namespace < ::ApplicationRecord
|
||||
include ::Gitlab::BackgroundMigration::ResetStatusOnContainerRepositories::Routable
|
||||
include ::Namespaces::Traversal::Recursive
|
||||
include ::Namespaces::Traversal::Linear
|
||||
include ::Gitlab::Utils::StrongMemoize
|
||||
|
||||
self.table_name = 'namespaces'
|
||||
self.inheritance_column = :_type_disabled
|
||||
|
||||
belongs_to :parent,
|
||||
class_name: '::Gitlab::BackgroundMigration::ResetStatusOnContainerRepositories::Namespace'
|
||||
|
||||
def self.polymorphic_name
|
||||
'Namespace'
|
||||
end
|
||||
end
|
||||
|
||||
class Project < ::ApplicationRecord
|
||||
include ::Gitlab::BackgroundMigration::ResetStatusOnContainerRepositories::Routable
|
||||
|
||||
self.table_name = 'projects'
|
||||
|
||||
belongs_to :namespace,
|
||||
class_name: '::Gitlab::BackgroundMigration::ResetStatusOnContainerRepositories::Namespace'
|
||||
|
||||
alias_method :parent, :namespace
|
||||
alias_attribute :parent_id, :namespace_id
|
||||
|
||||
delegate :root_ancestor, to: :namespace, allow_nil: true
|
||||
end
|
||||
|
||||
class ContainerRepository < ::ApplicationRecord
|
||||
self.table_name = 'container_repositories'
|
||||
|
||||
belongs_to :project,
|
||||
class_name: '::Gitlab::BackgroundMigration::ResetStatusOnContainerRepositories::Project'
|
||||
|
||||
def tags?
|
||||
result = ContainerRegistry.tags_for(path).any?
|
||||
::Gitlab::BackgroundMigration::Logger.info(
|
||||
migrator: MIGRATOR,
|
||||
has_tags: result,
|
||||
container_repository_id: id,
|
||||
container_repository_path: path
|
||||
)
|
||||
result
|
||||
end
|
||||
|
||||
def path
|
||||
@path ||= [project.full_path, name].select(&:present?).join('/').downcase
|
||||
end
|
||||
end
|
||||
|
||||
class ContainerRegistry
|
||||
class << self
|
||||
def tags_for(path)
|
||||
response = ContainerRegistryClient.repository_tags(path, page_size: 1)
|
||||
return DUMMY_TAGS unless response
|
||||
|
||||
response['tags'] || []
|
||||
rescue StandardError
|
||||
DUMMY_TAGS
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class ContainerRegistryClient
|
||||
def self.repository_tags(path, page_size:)
|
||||
registry_config = ::Gitlab.config.registry
|
||||
|
||||
return { 'tags' => DUMMY_TAGS } unless registry_config.enabled && registry_config.api_url.present?
|
||||
|
||||
pull_token = ::Auth::ContainerRegistryAuthenticationService.pull_access_token(path)
|
||||
client = ::ContainerRegistry::Client.new(registry_config.api_url, token: pull_token)
|
||||
client.repository_tags(path, page_size: page_size)
|
||||
end
|
||||
end
|
||||
# rubocop:enable Style/Documentation
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -9,9 +9,9 @@
|
|||
# Usage:
|
||||
#
|
||||
# include:
|
||||
# - template: Secure-Binaries.gitlab-ci.yml
|
||||
# - template: Security/Secure-Binaries.gitlab-ci.yml
|
||||
#
|
||||
# Docs: https://docs.gitlab.com/ee/topics/airgap/
|
||||
# Docs: https://docs.gitlab.com/ee/user/application_security/offline_deployments/
|
||||
|
||||
variables:
|
||||
# Setting this variable will affect all Security templates
|
||||
|
|
|
|||
|
|
@ -16320,7 +16320,10 @@ msgstr ""
|
|||
msgid "Expected documents: %{expected_documents}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Experiment Candidates"
|
||||
msgid "Experiment"
|
||||
msgstr ""
|
||||
|
||||
msgid "Experiment candidates"
|
||||
msgstr ""
|
||||
|
||||
msgid "Experiments"
|
||||
|
|
@ -17052,6 +17055,9 @@ msgstr ""
|
|||
msgid "February"
|
||||
msgstr ""
|
||||
|
||||
msgid "Feedback"
|
||||
msgstr ""
|
||||
|
||||
msgid "Feedback and Updates"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -21799,6 +21805,9 @@ msgstr ""
|
|||
msgid "Indicates whether this runner can pick jobs without tags"
|
||||
msgstr ""
|
||||
|
||||
msgid "Info"
|
||||
msgstr ""
|
||||
|
||||
msgid "Inform users without uploaded SSH keys that they can't push over SSH until one is added"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -26633,6 +26642,9 @@ msgstr ""
|
|||
msgid "Modal|Close"
|
||||
msgstr ""
|
||||
|
||||
msgid "Model candidate details"
|
||||
msgstr ""
|
||||
|
||||
msgid "Modified"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -29506,6 +29518,9 @@ msgstr ""
|
|||
msgid "Parameter \"job_id\" cannot exceed length of %{job_id_max_size}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Parameters"
|
||||
msgstr ""
|
||||
|
||||
msgid "Parent"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -41764,9 +41779,6 @@ msgstr ""
|
|||
msgid "This Cron pattern is invalid"
|
||||
msgstr ""
|
||||
|
||||
msgid "This Experiment has no logged Candidates"
|
||||
msgstr ""
|
||||
|
||||
msgid "This GitLab instance does not provide any shared runners yet. Instance administrators can register shared runners in the admin area."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -41956,6 +41968,9 @@ msgstr ""
|
|||
msgid "This epic would exceed maximum number of related epics."
|
||||
msgstr ""
|
||||
|
||||
msgid "This experiment has no logged candidates"
|
||||
msgstr ""
|
||||
|
||||
msgid "This feature requires local storage to be enabled"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -149,7 +149,6 @@ describe('Multi-file editor library model', () => {
|
|||
model.updateOptions({ insertSpaces: true, someOption: 'some value' });
|
||||
|
||||
expect(model.options).toEqual({
|
||||
endOfLine: 0,
|
||||
insertFinalNewline: true,
|
||||
insertSpaces: true,
|
||||
someOption: 'some value',
|
||||
|
|
@ -181,16 +180,12 @@ describe('Multi-file editor library model', () => {
|
|||
describe('applyCustomOptions', () => {
|
||||
it.each`
|
||||
option | value | contentBefore | contentAfter
|
||||
${'endOfLine'} | ${0} | ${'hello\nworld\n'} | ${'hello\nworld\n'}
|
||||
${'endOfLine'} | ${0} | ${'hello\r\nworld\r\n'} | ${'hello\nworld\n'}
|
||||
${'endOfLine'} | ${1} | ${'hello\nworld\n'} | ${'hello\r\nworld\r\n'}
|
||||
${'endOfLine'} | ${1} | ${'hello\r\nworld\r\n'} | ${'hello\r\nworld\r\n'}
|
||||
${'insertFinalNewline'} | ${true} | ${'hello\nworld'} | ${'hello\nworld\n'}
|
||||
${'insertFinalNewline'} | ${true} | ${'hello\nworld\n'} | ${'hello\nworld\n'}
|
||||
${'insertFinalNewline'} | ${false} | ${'hello\nworld'} | ${'hello\nworld'}
|
||||
${'trimTrailingWhitespace'} | ${true} | ${'hello \t\nworld \t\n'} | ${'hello\nworld\n'}
|
||||
${'trimTrailingWhitespace'} | ${true} | ${'hello \t\r\nworld \t\r\n'} | ${'hello\nworld\n'}
|
||||
${'trimTrailingWhitespace'} | ${false} | ${'hello \t\r\nworld \t\r\n'} | ${'hello \t\nworld \t\n'}
|
||||
${'trimTrailingWhitespace'} | ${true} | ${'hello \t\r\nworld \t\r\n'} | ${'hello\r\nworld\r\n'}
|
||||
${'trimTrailingWhitespace'} | ${false} | ${'hello \t\r\nworld \t\r\n'} | ${'hello \t\r\nworld \t\r\n'}
|
||||
`(
|
||||
'correctly applies custom option $option=$value to content',
|
||||
({ option, value, contentBefore, contentAfter }) => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,233 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`MlCandidate renders correctly 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="gl-alert gl-alert-warning"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="gl-icon s16 gl-alert-icon"
|
||||
data-testid="warning-icon"
|
||||
role="img"
|
||||
>
|
||||
<use
|
||||
href="#warning"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<div
|
||||
aria-live="assertive"
|
||||
class="gl-alert-content"
|
||||
role="alert"
|
||||
>
|
||||
<h2
|
||||
class="gl-alert-title"
|
||||
>
|
||||
Machine Learning Experiment Tracking is in Incubating Phase
|
||||
</h2>
|
||||
|
||||
<div
|
||||
class="gl-alert-body"
|
||||
>
|
||||
|
||||
GitLab incubates features to explore new use cases. These features are updated regularly, and support is limited
|
||||
|
||||
<a
|
||||
class="gl-link"
|
||||
href="https://about.gitlab.com/handbook/engineering/incubation/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="gl-alert-actions"
|
||||
>
|
||||
<a
|
||||
class="btn gl-alert-action btn-confirm btn-md gl-button"
|
||||
href="https://gitlab.com/gitlab-org/gitlab/-/issues/381660"
|
||||
>
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
|
||||
<span
|
||||
class="gl-button-text"
|
||||
>
|
||||
|
||||
Feedback
|
||||
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-label="Dismiss"
|
||||
class="btn gl-dismiss-btn btn-default btn-sm gl-button btn-default-tertiary btn-icon"
|
||||
type="button"
|
||||
>
|
||||
<!---->
|
||||
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="gl-button-icon gl-icon s16"
|
||||
data-testid="close-icon"
|
||||
role="img"
|
||||
>
|
||||
<use
|
||||
href="#close"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<!---->
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
|
||||
Model candidate details
|
||||
|
||||
</h3>
|
||||
|
||||
<table
|
||||
class="candidate-details"
|
||||
>
|
||||
<tbody>
|
||||
<tr
|
||||
class="divider"
|
||||
/>
|
||||
|
||||
<tr>
|
||||
<td
|
||||
class="gl-text-secondary gl-font-weight-bold"
|
||||
>
|
||||
Info
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="gl-font-weight-bold"
|
||||
>
|
||||
ID
|
||||
</td>
|
||||
|
||||
<td>
|
||||
candidate_iid
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td />
|
||||
|
||||
<td
|
||||
class="gl-font-weight-bold"
|
||||
>
|
||||
Status
|
||||
</td>
|
||||
|
||||
<td>
|
||||
SUCCESS
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td />
|
||||
|
||||
<td
|
||||
class="gl-font-weight-bold"
|
||||
>
|
||||
Experiment
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a
|
||||
class="gl-link"
|
||||
href="#"
|
||||
>
|
||||
The Experiment
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!---->
|
||||
|
||||
<tr
|
||||
class="divider"
|
||||
/>
|
||||
|
||||
<tr>
|
||||
<td
|
||||
class="gl-text-secondary gl-font-weight-bold"
|
||||
>
|
||||
|
||||
Parameters
|
||||
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="gl-font-weight-bold"
|
||||
>
|
||||
Algorithm
|
||||
</td>
|
||||
|
||||
<td>
|
||||
Decision Tree
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td />
|
||||
|
||||
<td
|
||||
class="gl-font-weight-bold"
|
||||
>
|
||||
MaxDepth
|
||||
</td>
|
||||
|
||||
<td>
|
||||
3
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr
|
||||
class="divider"
|
||||
/>
|
||||
|
||||
<tr>
|
||||
<td
|
||||
class="gl-text-secondary gl-font-weight-bold"
|
||||
>
|
||||
|
||||
Metrics
|
||||
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="gl-font-weight-bold"
|
||||
>
|
||||
AUC
|
||||
</td>
|
||||
|
||||
<td>
|
||||
.55
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td />
|
||||
|
||||
<td
|
||||
class="gl-font-weight-bold"
|
||||
>
|
||||
Accuracy
|
||||
</td>
|
||||
|
||||
<td>
|
||||
.99
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ShowExperiment with candidates renders correctly 1`] = `
|
||||
exports[`MlExperiment with candidates renders correctly 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="gl-alert gl-alert-warning"
|
||||
|
|
@ -39,7 +39,7 @@ exports[`ShowExperiment with candidates renders correctly 1`] = `
|
|||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Learn More
|
||||
Learn more
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
|
@ -48,7 +48,7 @@ exports[`ShowExperiment with candidates renders correctly 1`] = `
|
|||
>
|
||||
<a
|
||||
class="btn gl-alert-action btn-confirm btn-md gl-button"
|
||||
href="https://gitlab.com/groups/gitlab-org/-/epics/8560"
|
||||
href="https://gitlab.com/gitlab-org/gitlab/-/issues/381660"
|
||||
>
|
||||
<!---->
|
||||
|
||||
|
|
@ -58,7 +58,7 @@ exports[`ShowExperiment with candidates renders correctly 1`] = `
|
|||
class="gl-button-text"
|
||||
>
|
||||
|
||||
Feedback and Updates
|
||||
Feedback
|
||||
|
||||
</span>
|
||||
</a>
|
||||
|
|
@ -89,13 +89,13 @@ exports[`ShowExperiment with candidates renders correctly 1`] = `
|
|||
|
||||
<h3>
|
||||
|
||||
Experiment Candidates
|
||||
Experiment candidates
|
||||
|
||||
</h3>
|
||||
|
||||
<table
|
||||
aria-busy="false"
|
||||
aria-colcount="4"
|
||||
aria-colcount="6"
|
||||
class="table b-table gl-table gl-mt-0!"
|
||||
role="table"
|
||||
>
|
||||
|
|
@ -150,6 +150,24 @@ exports[`ShowExperiment with candidates renders correctly 1`] = `
|
|||
Mae
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
aria-colindex="5"
|
||||
aria-label="Details"
|
||||
class=""
|
||||
role="columnheader"
|
||||
scope="col"
|
||||
>
|
||||
<div />
|
||||
</th>
|
||||
<th
|
||||
aria-colindex="6"
|
||||
aria-label="Artifact"
|
||||
class=""
|
||||
role="columnheader"
|
||||
scope="col"
|
||||
>
|
||||
<div />
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
|
|
@ -184,6 +202,32 @@ exports[`ShowExperiment with candidates renders correctly 1`] = `
|
|||
class=""
|
||||
role="cell"
|
||||
/>
|
||||
<td
|
||||
aria-colindex="5"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<a
|
||||
class="gl-link"
|
||||
href="link_to_candidate1"
|
||||
>
|
||||
Details
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="6"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<a
|
||||
class="gl-link"
|
||||
href="link_to_artifact"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Artifacts
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class=""
|
||||
|
|
@ -213,6 +257,23 @@ exports[`ShowExperiment with candidates renders correctly 1`] = `
|
|||
class=""
|
||||
role="cell"
|
||||
/>
|
||||
<td
|
||||
aria-colindex="5"
|
||||
class=""
|
||||
role="cell"
|
||||
>
|
||||
<a
|
||||
class="gl-link"
|
||||
href="link_to_candidate2"
|
||||
>
|
||||
Details
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
aria-colindex="6"
|
||||
class=""
|
||||
role="cell"
|
||||
/>
|
||||
</tr>
|
||||
<!---->
|
||||
<!---->
|
||||
|
|
@ -15,7 +15,7 @@ describe('IncubationAlert', () => {
|
|||
|
||||
it('displays link to issue', () => {
|
||||
expect(findButton().attributes().href).toBe(
|
||||
'https://gitlab.com/groups/gitlab-org/-/epics/8560',
|
||||
'https://gitlab.com/gitlab-org/gitlab/-/issues/381660',
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
import { GlAlert } from '@gitlab/ui';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import MlCandidate from '~/ml/experiment_tracking/components/ml_candidate.vue';
|
||||
|
||||
describe('MlCandidate', () => {
|
||||
let wrapper;
|
||||
|
||||
const createWrapper = () => {
|
||||
const candidate = {
|
||||
params: [
|
||||
{ name: 'Algorithm', value: 'Decision Tree' },
|
||||
{ name: 'MaxDepth', value: '3' },
|
||||
],
|
||||
metrics: [
|
||||
{ name: 'AUC', value: '.55' },
|
||||
{ name: 'Accuracy', value: '.99' },
|
||||
],
|
||||
info: {
|
||||
iid: 'candidate_iid',
|
||||
artifact_link: 'path_to_artifact',
|
||||
experiment_name: 'The Experiment',
|
||||
experiment_path: 'path/to/experiment',
|
||||
status: 'SUCCESS',
|
||||
},
|
||||
};
|
||||
|
||||
return mountExtended(MlCandidate, { provide: { candidate } });
|
||||
};
|
||||
|
||||
const findAlert = () => wrapper.findComponent(GlAlert);
|
||||
|
||||
it('shows incubation warning', () => {
|
||||
wrapper = createWrapper();
|
||||
|
||||
expect(findAlert().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders correctly', () => {
|
||||
wrapper = createWrapper();
|
||||
|
||||
expect(wrapper.element).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,17 +1,17 @@
|
|||
import { GlAlert } from '@gitlab/ui';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import ShowExperiment from '~/ml/experiment_tracking/components/experiment.vue';
|
||||
import MlExperiment from '~/ml/experiment_tracking/components/ml_experiment.vue';
|
||||
|
||||
describe('ShowExperiment', () => {
|
||||
describe('MlExperiment', () => {
|
||||
let wrapper;
|
||||
|
||||
const createWrapper = (candidates = [], metricNames = [], paramNames = []) => {
|
||||
return mountExtended(ShowExperiment, { provide: { candidates, metricNames, paramNames } });
|
||||
return mountExtended(MlExperiment, { provide: { candidates, metricNames, paramNames } });
|
||||
};
|
||||
|
||||
const findAlert = () => wrapper.findComponent(GlAlert);
|
||||
|
||||
const findEmptyState = () => wrapper.findByText('This Experiment has no logged Candidates');
|
||||
const findEmptyState = () => wrapper.findByText('This experiment has no logged candidates');
|
||||
|
||||
it('shows incubation warning', () => {
|
||||
wrapper = createWrapper();
|
||||
|
|
@ -31,8 +31,8 @@ describe('ShowExperiment', () => {
|
|||
it('renders correctly', () => {
|
||||
wrapper = createWrapper(
|
||||
[
|
||||
{ rmse: 1, l1_ratio: 0.4 },
|
||||
{ auc: 0.3, l1_ratio: 0.5 },
|
||||
{ rmse: 1, l1_ratio: 0.4, details: 'link_to_candidate1', artifact: 'link_to_artifact' },
|
||||
{ auc: 0.3, l1_ratio: 0.5, details: 'link_to_candidate2' },
|
||||
],
|
||||
['rmse', 'auc', 'mae'],
|
||||
['l1_ratio'],
|
||||
|
|
@ -13,7 +13,7 @@ RSpec.describe Types::PermissionTypes::Project do
|
|||
:create_merge_request_from, :create_wiki, :push_code, :create_deployment, :push_to_delete_protected_branch,
|
||||
:admin_wiki, :admin_project, :update_pages, :admin_remote_mirror, :create_label,
|
||||
:update_wiki, :destroy_wiki, :create_pages, :destroy_pages, :read_pages_content,
|
||||
:read_merge_request, :read_design, :create_design, :destroy_design
|
||||
:read_merge_request, :read_design, :create_design, :destroy_design, :read_environment
|
||||
]
|
||||
|
||||
expected_permissions.each do |permission|
|
||||
|
|
|
|||
|
|
@ -5,28 +5,36 @@ require 'rspec'
|
|||
require 'spec_helper'
|
||||
require 'mime/types'
|
||||
|
||||
RSpec.describe Projects::Ml::ExperimentsHelper do
|
||||
let_it_be(:project) { build(:project, :private) }
|
||||
let_it_be(:experiment) { build(:ml_experiments, user_id: project.creator, project: project) }
|
||||
let_it_be(:candidates) do
|
||||
create_list(:ml_candidates, 2, experiment: experiment, user: project.creator).tap do |c|
|
||||
c[0].params.create!([{ name: 'param1', value: 'p1' }, { name: 'param2', value: 'p2' }])
|
||||
c[0].metrics.create!(
|
||||
RSpec.describe Projects::Ml::ExperimentsHelper, feature_category: :mlops do
|
||||
let_it_be(:project) { create(:project, :private) }
|
||||
let_it_be(:experiment) { create(:ml_experiments, user_id: project.creator, project: project) }
|
||||
let_it_be(:candidate0) do
|
||||
create(:ml_candidates, experiment: experiment, user: project.creator).tap do |c|
|
||||
c.params.build([{ name: 'param1', value: 'p1' }, { name: 'param2', value: 'p2' }])
|
||||
c.metrics.create!(
|
||||
[{ name: 'metric1', value: 0.1 }, { name: 'metric2', value: 0.2 }, { name: 'metric3', value: 0.3 }]
|
||||
)
|
||||
|
||||
c[1].params.create!([{ name: 'param2', value: 'p3' }, { name: 'param3', value: 'p4' }])
|
||||
c[1].metrics.create!(name: 'metric3', value: 0.4)
|
||||
end
|
||||
end
|
||||
|
||||
let_it_be(:candidate1) do
|
||||
create(:ml_candidates, experiment: experiment, user: project.creator).tap do |c|
|
||||
c.params.build([{ name: 'param2', value: 'p3' }, { name: 'param3', value: 'p4' }])
|
||||
c.metrics.create!(name: 'metric3', value: 0.4)
|
||||
end
|
||||
end
|
||||
|
||||
let_it_be(:candidates) { [candidate0, candidate1] }
|
||||
|
||||
describe '#candidates_table_items' do
|
||||
subject { helper.candidates_table_items(candidates) }
|
||||
|
||||
it 'creates the correct model for the table' do
|
||||
expected_value = [
|
||||
{ 'param1' => 'p1', 'param2' => 'p2', 'metric1' => '0.1000', 'metric2' => '0.2000', 'metric3' => '0.3000' },
|
||||
{ 'param2' => 'p3', 'param3' => 'p4', 'metric3' => '0.4000' }
|
||||
{ 'param1' => 'p1', 'param2' => 'p2', 'metric1' => '0.1000', 'metric2' => '0.2000', 'metric3' => '0.3000',
|
||||
'artifact' => nil, 'details' => "/#{project.full_path}/-/ml/candidates/#{candidate0.iid}" },
|
||||
{ 'param2' => 'p3', 'param3' => 'p4', 'metric3' => '0.4000',
|
||||
'artifact' => nil, 'details' => "/#{project.full_path}/-/ml/candidates/#{candidate1.iid}" }
|
||||
]
|
||||
|
||||
expect(Gitlab::Json.parse(subject)).to match_array(expected_value)
|
||||
|
|
@ -46,4 +54,40 @@ RSpec.describe Projects::Ml::ExperimentsHelper do
|
|||
it { is_expected.to match_array(%w[metric1 metric2 metric3]) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#candidate_as_data' do
|
||||
let(:candidate) { candidate0 }
|
||||
let(:package) do
|
||||
create(:generic_package, name: candidate.package_name, version: candidate.package_version, project: project)
|
||||
end
|
||||
|
||||
subject { Gitlab::Json.parse(helper.candidate_as_data(candidate)) }
|
||||
|
||||
it 'generates the correct params' do
|
||||
expect(subject['params']).to include(
|
||||
hash_including('name' => 'param1', 'value' => 'p1'),
|
||||
hash_including('name' => 'param2', 'value' => 'p2')
|
||||
)
|
||||
end
|
||||
|
||||
it 'generates the correct metrics' do
|
||||
expect(subject['metrics']).to include(
|
||||
hash_including('name' => 'metric1', 'value' => 0.1),
|
||||
hash_including('name' => 'metric2', 'value' => 0.2),
|
||||
hash_including('name' => 'metric3', 'value' => 0.3)
|
||||
)
|
||||
end
|
||||
|
||||
it 'generates the correct info' do
|
||||
expected_info = {
|
||||
'iid' => candidate.iid,
|
||||
'path_to_artifact' => "/#{project.full_path}/-/packages/#{package.id}",
|
||||
'experiment_name' => candidate.experiment.name,
|
||||
'path_to_experiment' => "/#{project.full_path}/-/ml/experiments/#{experiment.iid}",
|
||||
'status' => 'running'
|
||||
}
|
||||
|
||||
expect(subject['info']).to include(expected_info)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,261 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::BackgroundMigration::ResetStatusOnContainerRepositories, feature_category: :container_registry do
|
||||
let(:projects_table) { table(:projects) }
|
||||
let(:namespaces_table) { table(:namespaces) }
|
||||
let(:container_repositories_table) { table(:container_repositories) }
|
||||
let(:routes_table) { table(:routes) }
|
||||
|
||||
let!(:root_group) do
|
||||
namespaces_table.create!(name: 'root_group', path: 'root_group', type: 'Group') do |new_group|
|
||||
new_group.update!(traversal_ids: [new_group.id])
|
||||
end
|
||||
end
|
||||
|
||||
let!(:group1) do
|
||||
namespaces_table.create!(name: 'group1', path: 'group1', parent_id: root_group.id, type: 'Group') do |new_group|
|
||||
new_group.update!(traversal_ids: [root_group.id, new_group.id])
|
||||
end
|
||||
end
|
||||
|
||||
let!(:subgroup1) do
|
||||
namespaces_table.create!(name: 'subgroup1', path: 'subgroup1', parent_id: group1.id, type: 'Group') do |new_group|
|
||||
new_group.update!(traversal_ids: [root_group.id, group1.id, new_group.id])
|
||||
end
|
||||
end
|
||||
|
||||
let!(:group2) do
|
||||
namespaces_table.create!(name: 'group2', path: 'group2', parent_id: root_group.id, type: 'Group') do |new_group|
|
||||
new_group.update!(traversal_ids: [root_group.id, new_group.id])
|
||||
end
|
||||
end
|
||||
|
||||
let!(:group1_project_namespace) do
|
||||
namespaces_table.create!(name: 'group1_project', path: 'group1_project', type: 'Project', parent_id: group1.id)
|
||||
end
|
||||
|
||||
let!(:subgroup1_project_namespace) do
|
||||
namespaces_table.create!(
|
||||
name: 'subgroup1_project',
|
||||
path: 'subgroup1_project',
|
||||
type: 'Project',
|
||||
parent_id: subgroup1.id
|
||||
)
|
||||
end
|
||||
|
||||
let!(:group2_project_namespace) do
|
||||
namespaces_table.create!(
|
||||
name: 'group2_project',
|
||||
path: 'group2_project',
|
||||
type: 'Project',
|
||||
parent_id: group2.id
|
||||
)
|
||||
end
|
||||
|
||||
let!(:group1_project) do
|
||||
projects_table.create!(
|
||||
name: 'group1_project',
|
||||
path: 'group1_project',
|
||||
namespace_id: group1.id,
|
||||
project_namespace_id: group1_project_namespace.id
|
||||
)
|
||||
end
|
||||
|
||||
let!(:subgroup1_project) do
|
||||
projects_table.create!(
|
||||
name: 'subgroup1_project',
|
||||
path: 'subgroup1_project',
|
||||
namespace_id: subgroup1.id,
|
||||
project_namespace_id: subgroup1_project_namespace.id
|
||||
)
|
||||
end
|
||||
|
||||
let!(:group2_project) do
|
||||
projects_table.create!(
|
||||
name: 'group2_project',
|
||||
path: 'group2_project',
|
||||
namespace_id: group2.id,
|
||||
project_namespace_id: group2_project_namespace.id
|
||||
)
|
||||
end
|
||||
|
||||
let!(:route2) do
|
||||
routes_table.create!(
|
||||
source_id: group2_project.id,
|
||||
source_type: 'Project',
|
||||
path: 'root_group/group2/group2_project',
|
||||
namespace_id: group2_project_namespace.id
|
||||
)
|
||||
end
|
||||
|
||||
let!(:delete_scheduled_container_repository1) do
|
||||
container_repositories_table.create!(project_id: group1_project.id, status: 0, name: 'container_repository1')
|
||||
end
|
||||
|
||||
let!(:delete_scheduled_container_repository2) do
|
||||
container_repositories_table.create!(project_id: subgroup1_project.id, status: 0, name: 'container_repository2')
|
||||
end
|
||||
|
||||
let!(:delete_scheduled_container_repository3) do
|
||||
container_repositories_table.create!(project_id: group2_project.id, status: 0, name: 'container_repository3')
|
||||
end
|
||||
|
||||
let!(:delete_ongoing_container_repository4) do
|
||||
container_repositories_table.create!(project_id: group2_project.id, status: 2, name: 'container_repository4')
|
||||
end
|
||||
|
||||
let(:migration) do
|
||||
described_class.new(
|
||||
start_id: container_repositories_table.minimum(:id),
|
||||
end_id: container_repositories_table.maximum(:id),
|
||||
batch_table: :container_repositories,
|
||||
batch_column: :id,
|
||||
sub_batch_size: 50,
|
||||
pause_ms: 0,
|
||||
connection: ApplicationRecord.connection
|
||||
)
|
||||
end
|
||||
|
||||
describe '#filter_batch' do
|
||||
it 'scopes the relation to delete scheduled container repositories' do
|
||||
expected = container_repositories_table.where(status: 0).pluck(:id)
|
||||
actual = migration.filter_batch(container_repositories_table).pluck(:id)
|
||||
|
||||
expect(actual).to match_array(expected)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
let(:registry_api_url) { 'http://example.com' }
|
||||
|
||||
subject(:perform) { migration.perform }
|
||||
|
||||
before do
|
||||
stub_container_registry_config(
|
||||
enabled: true,
|
||||
api_url: registry_api_url,
|
||||
key: 'spec/fixtures/x509_certificate_pk.key'
|
||||
)
|
||||
stub_tags_list(path: 'root_group/group1/group1_project/container_repository1')
|
||||
stub_tags_list(path: 'root_group/group1/subgroup1/subgroup1_project/container_repository2', tags: [])
|
||||
stub_tags_list(path: 'root_group/group2/group2_project/container_repository3')
|
||||
end
|
||||
|
||||
shared_examples 'resetting status of all container repositories scheduled for deletion' do
|
||||
it 'resets all statuses' do
|
||||
expect_logging_on(
|
||||
path: 'root_group/group1/group1_project/container_repository1',
|
||||
id: delete_scheduled_container_repository1.id,
|
||||
has_tags: true
|
||||
)
|
||||
expect_logging_on(
|
||||
path: 'root_group/group1/subgroup1/subgroup1_project/container_repository2',
|
||||
id: delete_scheduled_container_repository2.id,
|
||||
has_tags: true
|
||||
)
|
||||
expect_logging_on(
|
||||
path: 'root_group/group2/group2_project/container_repository3',
|
||||
id: delete_scheduled_container_repository3.id,
|
||||
has_tags: true
|
||||
)
|
||||
|
||||
expect { perform }
|
||||
.to change { delete_scheduled_container_repository1.reload.status }.from(0).to(nil)
|
||||
.and change { delete_scheduled_container_repository3.reload.status }.from(0).to(nil)
|
||||
.and change { delete_scheduled_container_repository2.reload.status }.from(0).to(nil)
|
||||
end
|
||||
end
|
||||
|
||||
it 'resets status of container repositories with tags' do
|
||||
expect_pull_access_token_on(path: 'root_group/group1/group1_project/container_repository1')
|
||||
expect_pull_access_token_on(path: 'root_group/group1/subgroup1/subgroup1_project/container_repository2')
|
||||
expect_pull_access_token_on(path: 'root_group/group2/group2_project/container_repository3')
|
||||
|
||||
expect_logging_on(
|
||||
path: 'root_group/group1/group1_project/container_repository1',
|
||||
id: delete_scheduled_container_repository1.id,
|
||||
has_tags: true
|
||||
)
|
||||
expect_logging_on(
|
||||
path: 'root_group/group1/subgroup1/subgroup1_project/container_repository2',
|
||||
id: delete_scheduled_container_repository2.id,
|
||||
has_tags: false
|
||||
)
|
||||
expect_logging_on(
|
||||
path: 'root_group/group2/group2_project/container_repository3',
|
||||
id: delete_scheduled_container_repository3.id,
|
||||
has_tags: true
|
||||
)
|
||||
|
||||
expect { perform }
|
||||
.to change { delete_scheduled_container_repository1.reload.status }.from(0).to(nil)
|
||||
.and change { delete_scheduled_container_repository3.reload.status }.from(0).to(nil)
|
||||
.and not_change { delete_scheduled_container_repository2.reload.status }
|
||||
end
|
||||
|
||||
context 'with the registry disabled' do
|
||||
before do
|
||||
allow(::Gitlab.config.registry).to receive(:enabled).and_return(false)
|
||||
end
|
||||
|
||||
it_behaves_like 'resetting status of all container repositories scheduled for deletion'
|
||||
end
|
||||
|
||||
context 'with the registry api url not defined' do
|
||||
before do
|
||||
allow(::Gitlab.config.registry).to receive(:api_url).and_return('')
|
||||
end
|
||||
|
||||
it_behaves_like 'resetting status of all container repositories scheduled for deletion'
|
||||
end
|
||||
|
||||
context 'with a faraday error' do
|
||||
before do
|
||||
client_double = instance_double('::ContainerRegistry::Client')
|
||||
allow(::ContainerRegistry::Client).to receive(:new).and_return(client_double)
|
||||
allow(client_double).to receive(:repository_tags).and_raise(Faraday::TimeoutError)
|
||||
|
||||
expect_pull_access_token_on(path: 'root_group/group1/group1_project/container_repository1')
|
||||
expect_pull_access_token_on(path: 'root_group/group1/subgroup1/subgroup1_project/container_repository2')
|
||||
expect_pull_access_token_on(path: 'root_group/group2/group2_project/container_repository3')
|
||||
end
|
||||
|
||||
it_behaves_like 'resetting status of all container repositories scheduled for deletion'
|
||||
end
|
||||
|
||||
def stub_tags_list(path:, tags: %w[tag1])
|
||||
url = "#{registry_api_url}/v2/#{path}/tags/list?n=1"
|
||||
|
||||
stub_request(:get, url)
|
||||
.with(
|
||||
headers: {
|
||||
'Accept' => ContainerRegistry::Client::ACCEPTED_TYPES.join(', '),
|
||||
'Authorization' => /bearer .+/,
|
||||
'User-Agent' => "GitLab/#{Gitlab::VERSION}"
|
||||
}
|
||||
)
|
||||
.to_return(
|
||||
status: 200,
|
||||
body: Gitlab::Json.dump(tags: tags),
|
||||
headers: { 'Content-Type' => 'application/json' }
|
||||
)
|
||||
end
|
||||
|
||||
def expect_pull_access_token_on(path:)
|
||||
expect(Auth::ContainerRegistryAuthenticationService)
|
||||
.to receive(:pull_access_token).with(path).and_call_original
|
||||
end
|
||||
|
||||
def expect_logging_on(path:, id:, has_tags:)
|
||||
expect(::Gitlab::BackgroundMigration::Logger)
|
||||
.to receive(:info).with(
|
||||
migrator: described_class::MIGRATOR,
|
||||
has_tags: has_tags,
|
||||
container_repository_id: id,
|
||||
container_repository_path: path
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe QueueResetStatusOnContainerRepositories, feature_category: :container_registry do
|
||||
let_it_be(:batched_migration) { described_class::MIGRATION }
|
||||
|
||||
before do
|
||||
stub_container_registry_config(
|
||||
enabled: true,
|
||||
api_url: 'http://example.com',
|
||||
key: 'spec/fixtures/x509_certificate_pk.key'
|
||||
)
|
||||
end
|
||||
|
||||
it 'schedules a new batched migration' do
|
||||
reversible_migration do |migration|
|
||||
migration.before -> {
|
||||
expect(batched_migration).not_to have_scheduled_batched_migration
|
||||
}
|
||||
|
||||
migration.after -> {
|
||||
expect(batched_migration).to have_scheduled_batched_migration(
|
||||
table_name: :container_repositories,
|
||||
column_name: :id,
|
||||
interval: described_class::DELAY_INTERVAL,
|
||||
sub_batch_size: described_class::BATCH_SIZE
|
||||
)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
context 'with the container registry disabled' do
|
||||
before do
|
||||
allow(::Gitlab.config.registry).to receive(:enabled).and_return(false)
|
||||
end
|
||||
|
||||
it 'does not schedule a new batched migration' do
|
||||
reversible_migration do |migration|
|
||||
migration.before -> {
|
||||
expect(batched_migration).not_to have_scheduled_batched_migration
|
||||
}
|
||||
|
||||
migration.after -> {
|
||||
expect(batched_migration).not_to have_scheduled_batched_migration
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -20,7 +20,7 @@ RSpec.describe GenericCommitStatus do
|
|||
end
|
||||
|
||||
describe '#name_uniqueness_across_types' do
|
||||
let(:attributes) { {} }
|
||||
let(:attributes) { { context: 'default' } }
|
||||
let(:commit_status) { described_class.new(attributes) }
|
||||
let(:status_name) { 'test-job' }
|
||||
|
||||
|
|
@ -39,7 +39,7 @@ RSpec.describe GenericCommitStatus do
|
|||
end
|
||||
|
||||
context 'with only a pipeline' do
|
||||
let(:attributes) { { pipeline: pipeline } }
|
||||
let(:attributes) { { pipeline: pipeline, context: 'default' } }
|
||||
|
||||
context 'without name' do
|
||||
it_behaves_like 'it does not have uniqueness errors'
|
||||
|
|
@ -129,32 +129,6 @@ RSpec.describe GenericCommitStatus do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'set_default_values' do
|
||||
before do
|
||||
generic_commit_status.context = nil
|
||||
generic_commit_status.stage = nil
|
||||
generic_commit_status.save!
|
||||
end
|
||||
|
||||
describe '#context' do
|
||||
subject { generic_commit_status.context }
|
||||
|
||||
it { is_expected.not_to be_nil }
|
||||
end
|
||||
|
||||
describe '#stage' do
|
||||
subject { generic_commit_status.stage }
|
||||
|
||||
it { is_expected.not_to be_nil }
|
||||
end
|
||||
|
||||
describe '#stage_idx' do
|
||||
subject { generic_commit_status.stage_idx }
|
||||
|
||||
it { is_expected.not_to be_nil }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#present' do
|
||||
subject { generic_commit_status.present }
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ require 'spec_helper'
|
|||
RSpec.describe Ml::Candidate, factory_default: :keep do
|
||||
let_it_be(:candidate) { create(:ml_candidates, :with_metrics_and_params) }
|
||||
|
||||
let(:project) { candidate.experiment.project }
|
||||
|
||||
describe 'associations' do
|
||||
it { is_expected.to belong_to(:experiment) }
|
||||
it { is_expected.to belong_to(:user) }
|
||||
|
|
@ -13,14 +15,48 @@ RSpec.describe Ml::Candidate, factory_default: :keep do
|
|||
it { is_expected.to have_many(:metadata) }
|
||||
end
|
||||
|
||||
describe 'default values' do
|
||||
it { expect(described_class.new.iid).to be_present }
|
||||
end
|
||||
|
||||
describe '.artifact_root' do
|
||||
subject { candidate.artifact_root }
|
||||
|
||||
it { is_expected.to eq("/ml_candidate_#{candidate.iid}/-/") }
|
||||
end
|
||||
|
||||
describe 'default values' do
|
||||
it { expect(described_class.new.iid).to be_present }
|
||||
describe '.package_name' do
|
||||
subject { candidate.package_name }
|
||||
|
||||
it { is_expected.to eq("ml_candidate_#{candidate.iid}") }
|
||||
end
|
||||
|
||||
describe '.package_version' do
|
||||
subject { candidate.package_version }
|
||||
|
||||
it { is_expected.to eq('-') }
|
||||
end
|
||||
|
||||
describe '.artifact' do
|
||||
subject { candidate.artifact }
|
||||
|
||||
context 'when has logged artifacts' do
|
||||
let(:package) do
|
||||
create(:generic_package, name: candidate.package_name, version: candidate.package_version, project: project)
|
||||
end
|
||||
|
||||
it 'returns the package' do
|
||||
package
|
||||
|
||||
is_expected.to eq(package)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when does not have logged artifacts' do
|
||||
let(:tested_candidate) { create(:ml_candidates, :with_metrics_and_params) }
|
||||
|
||||
it { is_expected.to be_nil }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#by_project_id_and_iid' do
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Projects::Ml::CandidatesController, feature_category: :mlops do
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
let_it_be(:user) { project.first_owner }
|
||||
let_it_be(:experiment) { create(:ml_experiments, project: project, user: user) }
|
||||
let_it_be(:candidate) { create(:ml_candidates, experiment: experiment, user: user) }
|
||||
|
||||
let(:ff_value) { true }
|
||||
let(:threshold) { 4 }
|
||||
let(:candidate_iid) { candidate.iid }
|
||||
|
||||
before do
|
||||
stub_feature_flags(ml_experiment_tracking: false)
|
||||
stub_feature_flags(ml_experiment_tracking: project) if ff_value
|
||||
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
shared_examples '404 if feature flag disabled' do
|
||||
context 'when :ml_experiment_tracking disabled' do
|
||||
let(:ff_value) { false }
|
||||
|
||||
it 'is 404' do
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET show' do
|
||||
let(:params) { basic_params.merge(id: experiment.iid) }
|
||||
|
||||
before do
|
||||
show_candidate
|
||||
end
|
||||
|
||||
it 'renders the template' do
|
||||
expect(response).to render_template('projects/ml/candidates/show')
|
||||
end
|
||||
|
||||
# MR removing this xit https://gitlab.com/gitlab-org/gitlab/-/merge_requests/104166
|
||||
xit 'does not perform N+1 sql queries' do
|
||||
control_count = ActiveRecord::QueryRecorder.new { show_candidate }
|
||||
|
||||
create_list(:ml_candidate_params, 3, candidate: candidate)
|
||||
create_list(:ml_candidate_metrics, 3, candidate: candidate)
|
||||
|
||||
expect { show_candidate }.not_to exceed_all_query_limit(control_count).with_threshold(threshold)
|
||||
end
|
||||
|
||||
context 'when candidate does not exist' do
|
||||
let(:candidate_iid) { non_existing_record_id.to_s }
|
||||
|
||||
it 'returns 404' do
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like '404 if feature flag disabled'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def show_candidate
|
||||
get project_ml_candidate_path(project, candidate_iid)
|
||||
end
|
||||
end
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Projects::Ml::ExperimentsController do
|
||||
RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do
|
||||
let_it_be(:project_with_feature) { create(:project, :repository) }
|
||||
let_it_be(:user) { project_with_feature.first_owner }
|
||||
let_it_be(:project_without_feature) do
|
||||
|
|
@ -77,7 +77,8 @@ RSpec.describe Projects::Ml::ExperimentsController do
|
|||
expect(response).to render_template('projects/ml/experiments/show')
|
||||
end
|
||||
|
||||
it 'does not perform N+1 sql queries' do
|
||||
# MR removing this xit https://gitlab.com/gitlab-org/gitlab/-/merge_requests/104166
|
||||
xit 'does not perform N+1 sql queries' do
|
||||
control_count = ActiveRecord::QueryRecorder.new { show_experiment }
|
||||
|
||||
create_list(:ml_candidates, 2, :with_metrics_and_params, experiment: experiment)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ require (
|
|||
github.com/BurntSushi/toml v1.2.1
|
||||
github.com/FZambia/sentinel v1.1.1
|
||||
github.com/alecthomas/chroma/v2 v2.3.0
|
||||
github.com/aws/aws-sdk-go v1.44.145
|
||||
github.com/aws/aws-sdk-go v1.44.150
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/getsentry/raven-go v0.2.0
|
||||
github.com/golang-jwt/jwt/v4 v4.4.3
|
||||
|
|
@ -17,7 +17,7 @@ require (
|
|||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
|
||||
github.com/johannesboyne/gofakes3 v0.0.0-20221110173912-32fb85c5aed6
|
||||
github.com/johannesboyne/gofakes3 v0.0.0-20221128113635-c2f5cc6b5294
|
||||
github.com/jpillora/backoff v1.0.0
|
||||
github.com/mitchellh/copystructure v1.2.0
|
||||
github.com/prometheus/client_golang v1.14.0
|
||||
|
|
|
|||
|
|
@ -227,8 +227,8 @@ github.com/aws/aws-sdk-go v1.43.11/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4
|
|||
github.com/aws/aws-sdk-go v1.43.31/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
|
||||
github.com/aws/aws-sdk-go v1.44.45/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
|
||||
github.com/aws/aws-sdk-go v1.44.68/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
|
||||
github.com/aws/aws-sdk-go v1.44.145 h1:KMVRrIyjBsNz3xGPuHIRnhIuKlb5h3Ii5e5jbi3cgnc=
|
||||
github.com/aws/aws-sdk-go v1.44.145/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
|
||||
github.com/aws/aws-sdk-go v1.44.150 h1:X9HBhXu0ZPi+tOHUaZkjx43int7g0Ejk+IVbW25+wYg=
|
||||
github.com/aws/aws-sdk-go v1.44.150/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
|
||||
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
|
||||
github.com/aws/aws-sdk-go-v2 v1.16.8 h1:gOe9UPR98XSf7oEJCcojYg+N2/jCRm4DdeIsP85pIyQ=
|
||||
github.com/aws/aws-sdk-go-v2 v1.16.8/go.mod h1:6CpKuLXg2w7If3ABZCl/qZ6rEgwtjZTn4eAf4RcEyuw=
|
||||
|
|
@ -976,8 +976,8 @@ github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHW
|
|||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8=
|
||||
github.com/johannesboyne/gofakes3 v0.0.0-20221110173912-32fb85c5aed6 h1:eQGUsj2LcsLzfrHY1noKDSU7h+c9/rw9pQPwbQ9g1jQ=
|
||||
github.com/johannesboyne/gofakes3 v0.0.0-20221110173912-32fb85c5aed6/go.mod h1:LIAXxPvcUXwOcTIj9LSNSUpE9/eMHalTWxsP/kmWxQI=
|
||||
github.com/johannesboyne/gofakes3 v0.0.0-20221128113635-c2f5cc6b5294 h1:AJISYN7tPo3lGqwYmEYQdlftcQz48i8LNk/BRUKCTig=
|
||||
github.com/johannesboyne/gofakes3 v0.0.0-20221128113635-c2f5cc6b5294/go.mod h1:LIAXxPvcUXwOcTIj9LSNSUpE9/eMHalTWxsP/kmWxQI=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package helper
|
||||
package exception
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
|
@ -17,7 +17,7 @@ var ravenHeaderBlacklist = []string{
|
|||
"Private-Token",
|
||||
}
|
||||
|
||||
func CaptureRavenError(r *http.Request, err error, fields log.Fields) {
|
||||
func Track(r *http.Request, err error, fields log.Fields) {
|
||||
client := raven.DefaultClient
|
||||
extra := raven.Extra{}
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ func CaptureRavenError(r *http.Request, err error, fields log.Fields) {
|
|||
|
||||
interfaces := []raven.Interface{}
|
||||
if r != nil {
|
||||
CleanHeadersForRaven(r)
|
||||
CleanHeaders(r)
|
||||
interfaces = append(interfaces, raven.NewHttp(r))
|
||||
|
||||
//lint:ignore SA1019 this was recently deprecated. Update workhorse to use labkit errortracking package.
|
||||
|
|
@ -45,7 +45,7 @@ func CaptureRavenError(r *http.Request, err error, fields log.Fields) {
|
|||
client.Capture(packet, nil)
|
||||
}
|
||||
|
||||
func CleanHeadersForRaven(r *http.Request) {
|
||||
func CleanHeaders(r *http.Request) {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -12,21 +12,13 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/sebest/xff"
|
||||
"gitlab.com/gitlab-org/labkit/log"
|
||||
"gitlab.com/gitlab-org/labkit/mask"
|
||||
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/log"
|
||||
)
|
||||
|
||||
func logErrorWithFields(r *http.Request, err error, fields log.Fields) {
|
||||
if err != nil {
|
||||
CaptureRavenError(r, err, fields)
|
||||
}
|
||||
|
||||
printError(r, err, fields)
|
||||
}
|
||||
|
||||
func CaptureAndFail(w http.ResponseWriter, r *http.Request, err error, msg string, code int) {
|
||||
http.Error(w, msg, code)
|
||||
logErrorWithFields(r, err, nil)
|
||||
printError(r, err, nil)
|
||||
}
|
||||
|
||||
func Fail500(w http.ResponseWriter, r *http.Request, err error) {
|
||||
|
|
@ -35,7 +27,7 @@ func Fail500(w http.ResponseWriter, r *http.Request, err error) {
|
|||
|
||||
func Fail500WithFields(w http.ResponseWriter, r *http.Request, err error, fields log.Fields) {
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
logErrorWithFields(r, err, fields)
|
||||
printError(r, err, fields)
|
||||
}
|
||||
|
||||
func RequestEntityTooLarge(w http.ResponseWriter, r *http.Request, err error) {
|
||||
|
|
@ -43,15 +35,7 @@ func RequestEntityTooLarge(w http.ResponseWriter, r *http.Request, err error) {
|
|||
}
|
||||
|
||||
func printError(r *http.Request, err error, fields log.Fields) {
|
||||
if r != nil {
|
||||
entry := log.WithContextFields(r.Context(), log.Fields{
|
||||
"method": r.Method,
|
||||
"uri": mask.URL(r.RequestURI),
|
||||
})
|
||||
entry.WithFields(fields).WithError(err).Error()
|
||||
} else {
|
||||
log.WithFields(fields).WithError(err).Error("unknown error")
|
||||
}
|
||||
log.WithRequest(r).WithFields(fields).WithError(err).Error()
|
||||
}
|
||||
|
||||
func SetNoCacheHeaders(header http.Header) {
|
||||
|
|
@ -93,7 +77,7 @@ func OpenFile(path string) (file *os.File, fi os.FileInfo, err error) {
|
|||
func URLMustParse(s string) *url.URL {
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
log.WithError(err).WithField("url", s).Fatal("urlMustParse")
|
||||
log.WithError(err).WithFields(log.Fields{"url": s}).Fatal("urlMustParse")
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import (
|
|||
"gitlab.com/gitlab-org/labkit/mask"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/helper"
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/helper/exception"
|
||||
)
|
||||
|
||||
type Fields = log.Fields
|
||||
|
|
@ -79,10 +79,18 @@ func Error(args ...interface{}) {
|
|||
NewBuilder().Error(args...)
|
||||
}
|
||||
|
||||
func (b *Builder) Error(args ...interface{}) {
|
||||
b.entry.Error(args...)
|
||||
|
||||
if b.req != nil && b.err != nil {
|
||||
helper.CaptureRavenError(b.req, b.err, b.fields)
|
||||
func (b *Builder) trackException() {
|
||||
if b.err != nil {
|
||||
exception.Track(b.req, b.err, b.fields)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Builder) Error(args ...interface{}) {
|
||||
b.trackException()
|
||||
b.entry.Error(args...)
|
||||
}
|
||||
|
||||
func (b *Builder) Fatal(args ...interface{}) {
|
||||
b.trackException()
|
||||
b.entry.Fatal(args...)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import (
|
|||
|
||||
raven "github.com/getsentry/raven-go"
|
||||
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/helper"
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/helper/exception"
|
||||
)
|
||||
|
||||
func wrapRaven(h http.Handler) http.Handler {
|
||||
|
|
@ -30,7 +30,7 @@ func wrapRaven(h http.Handler) http.Handler {
|
|||
func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if p := recover(); p != nil {
|
||||
helper.CleanHeadersForRaven(r)
|
||||
exception.CleanHeaders(r)
|
||||
panic(p)
|
||||
}
|
||||
}()
|
||||
|
|
|
|||
Loading…
Reference in New Issue