Commit 6d61497d authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 5410e709 2c39ebca
...@@ -34,7 +34,7 @@ export default { ...@@ -34,7 +34,7 @@ export default {
<span v-if="discoverProjectSecurityPath"> <span v-if="discoverProjectSecurityPath">
<gl-button <gl-button
ref="discoverProjectSecurity" ref="discoverProjectSecurity"
icon="information-o" icon="question-o"
category="tertiary" category="tertiary"
:aria-label="$options.i18n.upgradeToManageVulnerabilities" :aria-label="$options.i18n.upgradeToManageVulnerabilities"
/> />
......
...@@ -286,7 +286,7 @@ ...@@ -286,7 +286,7 @@
- if project_nav_tab? :clusters - if project_nav_tab? :clusters
- show_cluster_hint = show_gke_cluster_integration_callout?(@project) - show_cluster_hint = show_gke_cluster_integration_callout?(@project)
= nav_link(controller: [:clusters, :user, :gcp]) do = nav_link(controller: [:cluster_agents, :clusters]) do
= link_to project_clusters_path(@project), title: _('Kubernetes'), class: 'shortcuts-kubernetes' do = link_to project_clusters_path(@project), title: _('Kubernetes'), class: 'shortcuts-kubernetes' do
%span %span
= _('Kubernetes') = _('Kubernetes')
......
---
title: Use more common help icon in security report MR widget
merge_request: 55741
author:
type: other
---
title: Add DORA daily metrics modeling
merge_request: 55473
author:
type: added
...@@ -38,6 +38,7 @@ ...@@ -38,6 +38,7 @@
- design_management - design_management
- devops_reports - devops_reports
- disaster_recovery - disaster_recovery
- dora_metrics
- dynamic_application_security_testing - dynamic_application_security_testing
- editor_extension - editor_extension
- epics - epics
......
---
name: dora_daily_metrics
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55473
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/291746
milestone: '13.10'
type: development
group: group::release
default_enabled: false
...@@ -100,6 +100,8 @@ ...@@ -100,6 +100,8 @@
- 1 - 1
- - disallow_two_factor_for_subgroups - - disallow_two_factor_for_subgroups
- 1 - 1
- - dora_metrics
- 1
- - elastic_association_indexer - - elastic_association_indexer
- 1 - 1
- - elastic_commit_indexer - - elastic_commit_indexer
......
# frozen_string_literal: true
class CreateDoraDailyMetrics < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
with_lock_retries do
create_table :dora_daily_metrics, if_not_exists: true do |t|
t.references :environment, null: false, foreign_key: { on_delete: :cascade }, index: false
t.date :date, null: false
t.integer :deployment_frequency
t.integer :lead_time_for_changes_in_seconds
t.index [:environment_id, :date], unique: true
end
end
add_check_constraint :dora_daily_metrics, "deployment_frequency >= 0", 'dora_daily_metrics_deployment_frequency_positive'
add_check_constraint :dora_daily_metrics, "lead_time_for_changes_in_seconds >= 0", 'dora_daily_metrics_lead_time_for_changes_in_seconds_positive'
end
def down
with_lock_retries do
drop_table :dora_daily_metrics
end
end
end
# frozen_string_literal: true
class AddIndexForSucceededDeployments < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
INDEX_NAME = 'index_deployments_on_environment_id_status_and_finished_at'
disable_ddl_transaction!
def up
add_concurrent_index(:deployments, %i[environment_id status finished_at], name: INDEX_NAME)
end
def down
remove_concurrent_index_by_name(:deployments, INDEX_NAME)
end
end
65f27401a76856d6cb284078204bb1b80797fa344e1a4ef3d9638c296f2d72d7
\ No newline at end of file
0a5d306735047101692bbdb37aa829bf70a225af6db7213a8c2eb8168f9a30e9
\ No newline at end of file
...@@ -12025,6 +12025,25 @@ CREATE SEQUENCE diff_note_positions_id_seq ...@@ -12025,6 +12025,25 @@ CREATE SEQUENCE diff_note_positions_id_seq
ALTER SEQUENCE diff_note_positions_id_seq OWNED BY diff_note_positions.id; ALTER SEQUENCE diff_note_positions_id_seq OWNED BY diff_note_positions.id;
CREATE TABLE dora_daily_metrics (
id bigint NOT NULL,
environment_id bigint NOT NULL,
date date NOT NULL,
deployment_frequency integer,
lead_time_for_changes_in_seconds integer,
CONSTRAINT dora_daily_metrics_deployment_frequency_positive CHECK ((deployment_frequency >= 0)),
CONSTRAINT dora_daily_metrics_lead_time_for_changes_in_seconds_positive CHECK ((lead_time_for_changes_in_seconds >= 0))
);
CREATE SEQUENCE dora_daily_metrics_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE dora_daily_metrics_id_seq OWNED BY dora_daily_metrics.id;
CREATE TABLE draft_notes ( CREATE TABLE draft_notes (
id bigint NOT NULL, id bigint NOT NULL,
merge_request_id integer NOT NULL, merge_request_id integer NOT NULL,
...@@ -19007,6 +19026,8 @@ ALTER TABLE ONLY design_user_mentions ALTER COLUMN id SET DEFAULT nextval('desig ...@@ -19007,6 +19026,8 @@ ALTER TABLE ONLY design_user_mentions ALTER COLUMN id SET DEFAULT nextval('desig
ALTER TABLE ONLY diff_note_positions ALTER COLUMN id SET DEFAULT nextval('diff_note_positions_id_seq'::regclass); ALTER TABLE ONLY diff_note_positions ALTER COLUMN id SET DEFAULT nextval('diff_note_positions_id_seq'::regclass);
ALTER TABLE ONLY dora_daily_metrics ALTER COLUMN id SET DEFAULT nextval('dora_daily_metrics_id_seq'::regclass);
ALTER TABLE ONLY draft_notes ALTER COLUMN id SET DEFAULT nextval('draft_notes_id_seq'::regclass); ALTER TABLE ONLY draft_notes ALTER COLUMN id SET DEFAULT nextval('draft_notes_id_seq'::regclass);
ALTER TABLE ONLY elastic_reindexing_subtasks ALTER COLUMN id SET DEFAULT nextval('elastic_reindexing_subtasks_id_seq'::regclass); ALTER TABLE ONLY elastic_reindexing_subtasks ALTER COLUMN id SET DEFAULT nextval('elastic_reindexing_subtasks_id_seq'::regclass);
...@@ -20215,6 +20236,9 @@ ALTER TABLE ONLY design_user_mentions ...@@ -20215,6 +20236,9 @@ ALTER TABLE ONLY design_user_mentions
ALTER TABLE ONLY diff_note_positions ALTER TABLE ONLY diff_note_positions
ADD CONSTRAINT diff_note_positions_pkey PRIMARY KEY (id); ADD CONSTRAINT diff_note_positions_pkey PRIMARY KEY (id);
ALTER TABLE ONLY dora_daily_metrics
ADD CONSTRAINT dora_daily_metrics_pkey PRIMARY KEY (id);
ALTER TABLE ONLY draft_notes ALTER TABLE ONLY draft_notes
ADD CONSTRAINT draft_notes_pkey PRIMARY KEY (id); ADD CONSTRAINT draft_notes_pkey PRIMARY KEY (id);
...@@ -22095,6 +22119,8 @@ CREATE INDEX index_deployments_on_environment_id_and_id ON deployments USING btr ...@@ -22095,6 +22119,8 @@ CREATE INDEX index_deployments_on_environment_id_and_id ON deployments USING btr
CREATE INDEX index_deployments_on_environment_id_and_iid_and_project_id ON deployments USING btree (environment_id, iid, project_id); CREATE INDEX index_deployments_on_environment_id_and_iid_and_project_id ON deployments USING btree (environment_id, iid, project_id);
CREATE INDEX index_deployments_on_environment_id_status_and_finished_at ON deployments USING btree (environment_id, status, finished_at);
CREATE INDEX index_deployments_on_environment_status_sha ON deployments USING btree (environment_id, status, sha); CREATE INDEX index_deployments_on_environment_status_sha ON deployments USING btree (environment_id, status, sha);
CREATE INDEX index_deployments_on_id_and_status_and_created_at ON deployments USING btree (id, status, created_at); CREATE INDEX index_deployments_on_id_and_status_and_created_at ON deployments USING btree (id, status, created_at);
...@@ -22149,6 +22175,8 @@ CREATE UNIQUE INDEX index_design_user_mentions_on_note_id ON design_user_mention ...@@ -22149,6 +22175,8 @@ CREATE UNIQUE INDEX index_design_user_mentions_on_note_id ON design_user_mention
CREATE UNIQUE INDEX index_diff_note_positions_on_note_id_and_diff_type ON diff_note_positions USING btree (note_id, diff_type); CREATE UNIQUE INDEX index_diff_note_positions_on_note_id_and_diff_type ON diff_note_positions USING btree (note_id, diff_type);
CREATE UNIQUE INDEX index_dora_daily_metrics_on_environment_id_and_date ON dora_daily_metrics USING btree (environment_id, date);
CREATE INDEX index_draft_notes_on_author_id ON draft_notes USING btree (author_id); CREATE INDEX index_draft_notes_on_author_id ON draft_notes USING btree (author_id);
CREATE INDEX index_draft_notes_on_discussion_id ON draft_notes USING btree (discussion_id); CREATE INDEX index_draft_notes_on_discussion_id ON draft_notes USING btree (discussion_id);
...@@ -25137,6 +25165,9 @@ ALTER TABLE ONLY boards_epic_board_positions ...@@ -25137,6 +25165,9 @@ ALTER TABLE ONLY boards_epic_board_positions
ALTER TABLE ONLY geo_repository_created_events ALTER TABLE ONLY geo_repository_created_events
ADD CONSTRAINT fk_rails_1f49e46a61 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_1f49e46a61 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY dora_daily_metrics
ADD CONSTRAINT fk_rails_1fd07aff6f FOREIGN KEY (environment_id) REFERENCES environments(id) ON DELETE CASCADE;
ALTER TABLE ONLY boards_epic_lists ALTER TABLE ONLY boards_epic_lists
ADD CONSTRAINT fk_rails_1fe6b54909 FOREIGN KEY (label_id) REFERENCES labels(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_1fe6b54909 FOREIGN KEY (label_id) REFERENCES labels(id) ON DELETE CASCADE;
...@@ -246,8 +246,9 @@ have been corrupted, you should reinstall the omnibus package. ...@@ -246,8 +246,9 @@ have been corrupted, you should reinstall the omnibus package.
## Check TCP connectivity to a remote site ## Check TCP connectivity to a remote site
Sometimes you need to know if your GitLab installation can connect to a TCP Sometimes you need to know if your GitLab installation can connect to a TCP
service on another machine - perhaps a PostgreSQL or HTTPS server. A Rake task service on another machine (for example a PostgreSQL or web server)
is included to help you with this: in order to troubleshoot proxy issues.
A Rake task is included to help you with this.
**Omnibus Installation** **Omnibus Installation**
......
...@@ -2349,16 +2349,6 @@ as soon as possible. ...@@ -2349,16 +2349,6 @@ as soon as possible.
</a> </a>
</div> </div>
## Troubleshooting
See the [troubleshooting documentation](troubleshooting.md).
<div align="right">
<a type="button" class="btn btn-default" href="#setup-components">
Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
</a>
</div>
## Cloud Native Deployment (optional) ## Cloud Native Deployment (optional)
Hybrid installations leverage the benefits of both cloud native and traditional Hybrid installations leverage the benefits of both cloud native and traditional
...@@ -2411,3 +2401,9 @@ Webservice pods. Expand available resources using the ratio of 1 vCPU to 1.25 GB ...@@ -2411,3 +2401,9 @@ Webservice pods. Expand available resources using the ratio of 1 vCPU to 1.25 GB
_per each worker process_ for each additional Webservice pod. _per each worker process_ for each additional Webservice pod.
For further information on resource usage, see the [Webservice resources](https://docs.gitlab.com/charts/charts/gitlab/webservice/#resources). For further information on resource usage, see the [Webservice resources](https://docs.gitlab.com/charts/charts/gitlab/webservice/#resources).
<div align="right">
<a type="button" class="btn btn-default" href="#setup-components">
Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
</a>
</div>
...@@ -2352,13 +2352,3 @@ as soon as possible. ...@@ -2352,13 +2352,3 @@ as soon as possible.
Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i> Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
</a> </a>
</div> </div>
## Troubleshooting
See the [troubleshooting documentation](troubleshooting.md).
<div align="right">
<a type="button" class="btn btn-default" href="#setup-components">
Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
</a>
</div>
...@@ -966,13 +966,3 @@ as soon as possible. ...@@ -966,13 +966,3 @@ as soon as possible.
Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i> Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
</a> </a>
</div> </div>
## Troubleshooting
See the [troubleshooting documentation](troubleshooting.md).
<div align="right">
<a type="button" class="btn btn-default" href="#setup-components">
Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
</a>
</div>
...@@ -2032,13 +2032,3 @@ as soon as possible. ...@@ -2032,13 +2032,3 @@ as soon as possible.
Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i> Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
</a> </a>
</div> </div>
## Troubleshooting
See the [troubleshooting documentation](troubleshooting.md).
<div align="right">
<a type="button" class="btn btn-default" href="#setup-components">
Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
</a>
</div>
...@@ -2366,13 +2366,3 @@ as soon as possible. ...@@ -2366,13 +2366,3 @@ as soon as possible.
Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i> Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
</a> </a>
</div> </div>
## Troubleshooting
See the [troubleshooting documentation](troubleshooting.md).
<div align="right">
<a type="button" class="btn btn-default" href="#setup-components">
Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
</a>
</div>
...@@ -2027,13 +2027,3 @@ as soon as possible. ...@@ -2027,13 +2027,3 @@ as soon as possible.
Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i> Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
</a> </a>
</div> </div>
## Troubleshooting
See the [troubleshooting documentation](troubleshooting.md).
<div align="right">
<a type="button" class="btn btn-default" href="#setup-components">
Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
</a>
</div>
...@@ -903,6 +903,7 @@ Autogenerated return type of CiCdSettingsUpdate. ...@@ -903,6 +903,7 @@ Autogenerated return type of CiCdSettingsUpdate.
| `project` | Project | The project this cluster agent is associated with. | | `project` | Project | The project this cluster agent is associated with. |
| `tokens` | ClusterAgentTokenConnection | Tokens associated with the cluster agent. | | `tokens` | ClusterAgentTokenConnection | Tokens associated with the cluster agent. |
| `updatedAt` | Time | Timestamp the cluster agent was updated. | | `updatedAt` | Time | Timestamp the cluster agent was updated. |
| `webPath` | String | Web path of the cluster agent. |
### ClusterAgentDeletePayload ### ClusterAgentDeletePayload
......
...@@ -287,9 +287,9 @@ In GitLab 12.1 and later, only PostgreSQL is supported. In GitLab 13.0 and later ...@@ -287,9 +287,9 @@ In GitLab 12.1 and later, only PostgreSQL is supported. In GitLab 13.0 and later
```shell ```shell
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
RELEASE=$(lsb_release -cs) echo "deb http://apt.postgresql.org/pub/repos/apt/ ${RELEASE}"-pgdg main | sudo tee /etc/apt/sources.list.d/pgdg.list sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
sudo apt update sudo apt update
sudo apt -y install postgresql-11 postgresql-client-11 libpq-dev sudo apt -y install postgresql-12 postgresql-client-12 libpq-dev
``` ```
1. Verify the PostgreSQL version you have is supported by the version of GitLab you're 1. Verify the PostgreSQL version you have is supported by the version of GitLab you're
......
<script>
import { GlAlert, GlBadge, GlLoadingIcon, GlSprintf, GlTab, GlTabs } from '@gitlab/ui';
import { s__ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import getClusterAgentQuery from '../graphql/queries/get_cluster_agent.query.graphql';
import TokenTable from './token_table.vue';
export default {
i18n: {
installedInfo: s__('ClusterAgents|Created by %{name} %{time}'),
loadingError: s__('ClusterAgents|An error occurred while loading your agent'),
tokens: s__('ClusterAgents|Access tokens'),
unknownUser: s__('ClusterAgents|Unknown user'),
},
apollo: {
clusterAgent: {
query: getClusterAgentQuery,
variables() {
return {
agentName: this.agentName,
projectPath: this.projectPath,
};
},
update: (data) => data?.project?.clusterAgent,
error() {
this.clusterAgent = null;
},
},
},
components: {
GlAlert,
GlBadge,
GlLoadingIcon,
GlSprintf,
GlTab,
GlTabs,
TimeAgoTooltip,
TokenTable,
},
props: {
agentName: {
required: true,
type: String,
},
projectPath: {
required: true,
type: String,
},
},
computed: {
createdAt() {
return this.clusterAgent?.createdAt;
},
createdBy() {
return this.clusterAgent?.createdByUser?.name || this.$options.i18n.unknownUser;
},
isLoading() {
return this.$apollo.queries.clusterAgent.loading;
},
tokenCount() {
return this.clusterAgent?.tokens?.count;
},
tokens() {
return this.clusterAgent?.tokens?.nodes || [];
},
},
};
</script>
<template>
<section>
<h2>{{ agentName }}</h2>
<gl-loading-icon v-if="isLoading" size="lg" class="gl-m-3" />
<div v-else-if="clusterAgent">
<p data-testid="cluster-agent-create-info">
<gl-sprintf :message="$options.i18n.installedInfo">
<template #name>
{{ createdBy }}
</template>
<template #time>
<time-ago-tooltip :time="createdAt" />
</template>
</gl-sprintf>
</p>
<gl-tabs>
<gl-tab>
<template slot="title">
<span data-testid="cluster-agent-token-count">
{{ $options.i18n.tokens }}
<gl-badge v-if="tokenCount" size="sm" class="gl-tab-counter-badge">{{
tokenCount
}}</gl-badge>
</span>
</template>
<TokenTable :tokens="tokens" />
</gl-tab>
</gl-tabs>
</div>
<gl-alert v-else variant="danger" :dismissible="false">
{{ $options.i18n.loadingError }}
</gl-alert>
</section>
</template>
<script>
import { GlEmptyState, GlLink, GlTable, GlTooltip, GlTruncate } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
components: {
GlEmptyState,
GlLink,
GlTable,
GlTooltip,
GlTruncate,
TimeAgoTooltip,
},
i18n: {
createdBy: s__('ClusterAgents|Created by'),
createToken: s__('ClusterAgents|You will need to create a token to connect to your agent'),
dateCreated: s__('ClusterAgents|Date created'),
description: s__('ClusterAgents|Description'),
learnMore: s__('ClusterAgents|Learn how to create an agent access token'),
noTokens: s__('ClusterAgents|This agent has no tokens'),
unknownUser: s__('ClusterAgents|Unknown user'),
},
props: {
tokens: {
required: true,
type: Array,
},
},
computed: {
fields() {
return [
{
key: 'createdAt',
label: this.$options.i18n.dateCreated,
tdAttr: { 'data-testid': 'agent-token-created-time' },
},
{
key: 'createdBy',
label: this.$options.i18n.createdBy,
tdAttr: { 'data-testid': 'agent-token-created-user' },
},
{
key: 'description',
label: this.$options.i18n.description,
tdAttr: { 'data-testid': 'agent-token-description' },
},
];
},
learnMoreUrl() {
return helpPagePath('user/clusters/agent/index.md', {
anchor: 'create-an-agent-record-in-gitlab',
});
},
},
methods: {
createdByName(token) {
return token?.createdByUser?.name || this.$options.i18n.unknownUser;
},
},
};
</script>
<template>
<div v-if="tokens.length">
<div class="gl-text-right gl-my-5">
<gl-link target="_blank" :href="learnMoreUrl">
{{ $options.i18n.learnMore }}
</gl-link>
</div>
<gl-table :items="tokens" :fields="fields" fixed stacked="md">
<template #cell(createdAt)="{ item }">
<time-ago-tooltip :time="item.createdAt" />
</template>
<template #cell(createdBy)="{ item }">
<span>{{ createdByName(item) }}</span>
</template>
<template #cell(description)="{ item }">
<div v-if="item.description" :id="`tooltip-description-container-${item.id}`">
<gl-truncate :id="`tooltip-description-${item.id}`" :text="item.description" />
<gl-tooltip
:container="`tooltip-description-container-${item.id}`"
:target="`tooltip-description-${item.id}`"
placement="top"
>
{{ item.description }}
</gl-tooltip>
</div>
</template>
</gl-table>
</div>
<gl-empty-state
v-else
:title="$options.i18n.noTokens"
:primary-button-link="learnMoreUrl"
:primary-button-text="$options.i18n.learnMore"
/>
</template>
fragment Token on ClusterAgentToken {
id
createdAt
description
createdByUser {
name
}
}
#import "../fragments/cluster_agent_token.fragment.graphql"
query getClusterAgent($projectPath: ID!, $agentName: String!) {
project(fullPath: $projectPath) {
clusterAgent(name: $agentName) {
id
createdAt
createdByUser {
name
}
tokens {
count
nodes {
...Token
}
}
}
}
}
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import AgentShowPage from './components/show.vue';
Vue.use(VueApollo);
export default () => {
const el = document.querySelector('#js-cluster-agent-details');
if (!el) {
return null;
}
const defaultClient = createDefaultClient();
const { agentName, projectPath } = el.dataset;
return new Vue({
el,
apolloProvider: new VueApollo({ defaultClient }),
render(createElement) {
return createElement(AgentShowPage, {
props: {
agentName,
projectPath,
},
});
},
});
};
import loadClusterAgentVues from 'ee/clusters/agents';
loadClusterAgentVues();
# frozen_string_literal: true
class Projects::ClusterAgentsController < Projects::ApplicationController
before_action :authorize_can_read_cluster_agent!
feature_category :kubernetes_management
def show
@agent_name = params[:name]
end
private
def authorize_can_read_cluster_agent!
return if can?(current_user, :admin_cluster, project) && project.feature_available?(:cluster_agents)
access_denied!
end
end
...@@ -43,9 +43,18 @@ module Types ...@@ -43,9 +43,18 @@ module Types
null: true, null: true,
description: 'Timestamp the cluster agent was updated.' description: 'Timestamp the cluster agent was updated.'
field :web_path,
GraphQL::STRING_TYPE,
null: true,
description: 'Web path of the cluster agent.'
def project def project
Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, object.project_id).find Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, object.project_id).find
end end
def web_path
::Gitlab::Routing.url_helpers.project_cluster_agent_path(object.project, object.name)
end
end end
end end
end end
...@@ -19,6 +19,7 @@ module EE ...@@ -19,6 +19,7 @@ module EE
override :sidebar_operations_paths override :sidebar_operations_paths
def sidebar_operations_paths def sidebar_operations_paths
super + %w[ super + %w[
cluster_agents
oncall_schedules oncall_schedules
] ]
end end
......
# frozen_string_literal: true
module Projects::ClusterAgentsHelper
def js_cluster_agent_details_data(agent_name, project)
{
agent_name: agent_name,
project_path: project.full_path
}
end
end
# frozen_string_literal: true
module Dora
# DevOps Research and Assessment (DORA) key metrics. Deployment Frequency,
# Lead Time for Changes, Change Failure Rate and Time to Restore Service
# are tracked as daily summary.
# Reference: https://cloud.google.com/blog/products/devops-sre/using-the-four-keys-to-measure-your-devops-performance
class DailyMetrics < ApplicationRecord
belongs_to :environment
self.table_name = 'dora_daily_metrics'
class << self
def refresh!(environment, date)
raise ArgumentError unless environment.is_a?(::Environment) && date.is_a?(Date)
deployment_frequency = deployment_frequency(environment, date)
lead_time_for_changes = lead_time_for_changes(environment, date)
# This query is concurrent safe upsert with the unique index.
connection.execute(<<~SQL)
INSERT INTO #{table_name} (
environment_id,
date,
deployment_frequency,
lead_time_for_changes_in_seconds
)
VALUES (
#{environment.id},
#{ActiveRecord::Base.connection.quote(date.to_s)},
(#{deployment_frequency}),
(#{lead_time_for_changes})
)
ON CONFLICT (environment_id, date)
DO UPDATE SET
deployment_frequency = (#{deployment_frequency}),
lead_time_for_changes_in_seconds = (#{lead_time_for_changes})
SQL
end
private
# Compose a query to calculate "Deployment Frequency" of the date
def deployment_frequency(environment, date)
deployments = Deployment.arel_table
deployments
.project(deployments[:id].count)
.where(eligible_deployments(environment, date))
.to_sql
end
# Compose a query to calculate "Lead Time for Changes" of the date
def lead_time_for_changes(environment, date)
deployments = Deployment.arel_table
deployment_merge_requests = DeploymentMergeRequest.arel_table
merge_request_metrics = MergeRequest::Metrics.arel_table
deployments
.project(
Arel.sql(
'PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY EXTRACT(EPOCH FROM (deployments.finished_at - merge_request_metrics.merged_at)))'
)
)
.join(deployment_merge_requests).on(
deployment_merge_requests[:deployment_id].eq(deployments[:id])
)
.join(merge_request_metrics).on(
merge_request_metrics[:merge_request_id].eq(deployment_merge_requests[:merge_request_id])
)
.where(eligible_deployments(environment, date))
.to_sql
end
def eligible_deployments(environment, date)
deployments = Deployment.arel_table
[deployments[:environment_id].eq(environment.id),
deployments[:finished_at].gteq(date.beginning_of_day),
deployments[:finished_at].lteq(date.end_of_day),
deployments[:status].eq(Deployment.statuses[:success])].reduce(&:and)
end
end
end
end
...@@ -10,6 +10,22 @@ module EE ...@@ -10,6 +10,22 @@ module EE
prepended do prepended do
include UsageStatistics include UsageStatistics
state_machine :status do
after_transition any => :success do |deployment|
next unless ::Feature.enabled?(:dora_daily_metrics, deployment.project, default_enabled: :yaml)
deployment.run_after_commit do
# Schedule to refresh the DORA daily metrics.
# It has 5 minutes delay due to waiting for the other async processes
# (e.g. `LinkMergeRequestWorker`) to be finished before collecting metrics.
::Dora::DailyMetrics::RefreshWorker
.perform_in(5.minutes,
deployment.environment_id,
Time.current.to_date.to_s)
end
end
end
end end
end end
end end
...@@ -7,6 +7,8 @@ module EE ...@@ -7,6 +7,8 @@ module EE
include ::Gitlab::Utils::StrongMemoize include ::Gitlab::Utils::StrongMemoize
prepended do prepended do
has_many :dora_daily_metrics, class_name: 'Dora::DailyMetrics'
# Returns environments where its latest deployment is to a cluster # Returns environments where its latest deployment is to a cluster
scope :deployed_to_cluster, -> (cluster) do scope :deployed_to_cluster, -> (cluster) do
environments = model.arel_table environments = model.arel_table
......
- add_to_breadcrumbs _('Kubernetes'), project_clusters_path(@project)
- page_title @agent_name
#js-cluster-agent-details{ data: js_cluster_agent_details_data(@agent_name, @project) }
...@@ -339,6 +339,14 @@ ...@@ -339,6 +339,14 @@
:weight: 3 :weight: 3
:idempotent: true :idempotent: true
:tags: [] :tags: []
- :name: dora_metrics:dora_daily_metrics_refresh
:feature_category: :dora_metrics
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
- :name: epics:epics_update_epics_dates - :name: epics:epics_update_epics_dates
:feature_category: :epics :feature_category: :epics
:has_external_dependencies: :has_external_dependencies:
......
# frozen_string_literal: true
module Dora
class DailyMetrics
class RefreshWorker
include ApplicationWorker
deduplicate :until_executing
idempotent!
queue_namespace :dora_metrics
feature_category :dora_metrics
def perform(environment_id, date)
Environment.find_by_id(environment_id).try do |environment|
::Dora::DailyMetrics.refresh!(environment, Date.parse(date))
end
end
end
end
end
---
title: Added cluster agent details page
merge_request: 54106
author:
type: added
---
title: Only set note visibility_level if associated to a project
merge_request: 55111
author:
type: fixed
...@@ -127,6 +127,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -127,6 +127,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
namespace :incident_management, path: '' do namespace :incident_management, path: '' do
resources :oncall_schedules, only: [:index], path: 'oncall_schedules' resources :oncall_schedules, only: [:index], path: 'oncall_schedules'
end end
resources :cluster_agents, only: [:show], param: :name
end end
# End of the /-/ scope. # End of the /-/ scope.
......
...@@ -54,9 +54,11 @@ module Elastic ...@@ -54,9 +54,11 @@ module Elastic
nil nil
end end
# protect against missing project_feature and set visibility to PRIVATE # protect against missing project and project_feature and set visibility to PRIVATE
# if the project_feature is missing on a project # if the project_feature is missing on a project
def safely_read_project_feature_for_elasticsearch(feature) def safely_read_project_feature_for_elasticsearch(feature)
return unless target.project
if target.project.project_feature if target.project.project_feature
target.project.project_feature.access_level(feature) target.project.project_feature.access_level(feature)
else else
......
...@@ -22,9 +22,10 @@ module Elastic ...@@ -22,9 +22,10 @@ module Elastic
} }
end end
# only attempt to set project permissions if associated to a project
# do not add the permission fields unless the `remove_permissions_data_from_notes_documents` # do not add the permission fields unless the `remove_permissions_data_from_notes_documents`
# migration has completed otherwise the migration will never finish # migration has completed otherwise the migration will never finish
if Elastic::DataMigrationService.migration_has_finished?(:remove_permissions_data_from_notes_documents) if target.project && Elastic::DataMigrationService.migration_has_finished?(:remove_permissions_data_from_notes_documents)
data['visibility_level'] = target.project.visibility_level data['visibility_level'] = target.project.visibility_level
merge_project_feature_access_level(data, noteable) merge_project_feature_access_level(data, noteable)
end end
......
# frozen_string_literal: true
FactoryBot.define do
factory :dora_daily_metrics, class: 'Dora::DailyMetrics' do
environment
date { Time.current.to_date }
end
end
...@@ -3,8 +3,9 @@ ...@@ -3,8 +3,9 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'ClusterAgents', :js do RSpec.describe 'ClusterAgents', :js do
let_it_be(:agent) { create(:cluster_agent) } let_it_be(:token) { create(:cluster_agent_token, description: 'feature test token')}
let(:agent) { token.agent }
let(:project) { agent.project } let(:project) { agent.project }
let(:user) { project.creator } let(:user) { project.creator }
...@@ -27,6 +28,17 @@ RSpec.describe 'ClusterAgents', :js do ...@@ -27,6 +28,17 @@ RSpec.describe 'ClusterAgents', :js do
expect(page).not_to have_content('GitLab Agent managed clusters') expect(page).not_to have_content('GitLab Agent managed clusters')
end end
end end
context 'when user visits agents show page' do
before do
visit project_cluster_agent_path(project, agent.name)
end
it 'displays not found' do
expect(page).to have_title('Not Found')
expect(page).to have_content('Page Not Found')
end
end
end end
context 'premium user' do context 'premium user' do
...@@ -44,20 +56,35 @@ RSpec.describe 'ClusterAgents', :js do ...@@ -44,20 +56,35 @@ RSpec.describe 'ClusterAgents', :js do
it 'displays empty state', :aggregate_failures do it 'displays empty state', :aggregate_failures do
click_link 'GitLab Agent managed clusters' click_link 'GitLab Agent managed clusters'
expect(page).to have_link('Integrate with the GitLab Agent') expect(page).to have_link('Integrate with the GitLab Agent')
expect(page).to have_selector('.empty-state') expect(page).to have_selector('.empty-state')
end end
end end
context 'when user has an agent and visits the index page' do context 'when user has an agent' do
before do context 'when visiting the index page' do
visit project_clusters_path(project) before do
visit project_clusters_path(project)
end
it 'displays a table with agent', :aggregate_failures do
click_link 'GitLab Agent managed clusters'
expect(page).to have_content(agent.name)
expect(page).to have_selector('[data-testid="cluster-agent-list-table"] tbody tr', count: 1)
end
end end
it 'displays a table with agent', :aggregate_failures do context 'when visiting the show page' do
click_link 'GitLab Agent managed clusters' before do
expect(page).to have_content(agent.name) visit project_cluster_agent_path(project, agent.name)
expect(page).to have_selector('[data-testid="cluster-agent-list-table"] tbody tr', count: 1) end
it 'displays agent and token information', :aggregate_failures do
expect(page).to have_content(agent.name)
expect(page).to have_content(token.description)
end
end end
end end
end end
......
import { GlAlert, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import ClusterAgentShow from 'ee/clusters/agents/components/show.vue';
import TokenTable from 'ee/clusters/agents/components/token_table.vue';
import getAgentQuery from 'ee/clusters/agents/graphql/queries/get_cluster_agent.query.graphql';
import { useFakeDate } from 'helpers/fake_date';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('ClusterAgentShow', () => {
let wrapper;
useFakeDate([2021, 2, 15]);
const propsData = {
agentName: 'cluster-agent',
projectPath: 'path/to/project',
};
const defaultClusterAgent = {
id: '1',
createdAt: '2021-02-13T00:00:00Z',
createdByUser: {
name: 'user-1',
},
tokens: {
count: 1,
nodes: [],
},
};
const createWrapper = ({ clusterAgent, queryResponse = null }) => {
const agentQueryResponse =
queryResponse || jest.fn().mockResolvedValue({ data: { project: { clusterAgent } } });
const apolloProvider = createMockApollo([[getAgentQuery, agentQueryResponse]]);
wrapper = shallowMount(ClusterAgentShow, {
localVue,
apolloProvider,
propsData,
stubs: { GlSprintf, TimeAgoTooltip },
});
};
const findCreatedText = () => wrapper.find('[data-testid="cluster-agent-create-info"]').text();
const findTokenCount = () => wrapper.find('[data-testid="cluster-agent-token-count"]').text();
beforeEach(() => {
return createWrapper({ clusterAgent: defaultClusterAgent });
});
afterEach(() => {
wrapper.destroy();
});
it('displays the agent name', () => {
expect(wrapper.text()).toContain(propsData.agentName);
});
it('displays agent create information', () => {
expect(findCreatedText()).toMatchInterpolatedText('Created by user-1 2 days ago');
});
describe('when create user is unknown', () => {
const missingUser = {
...defaultClusterAgent,
createdByUser: null,
};
beforeEach(() => {
return createWrapper({ clusterAgent: missingUser });
});
it('displays agent create information with unknown user', () => {
expect(findCreatedText()).toMatchInterpolatedText('Created by Unknown user 2 days ago');
});
});
it('displays token count', () => {
expect(findTokenCount()).toMatchInterpolatedText(
`${ClusterAgentShow.i18n.tokens} ${defaultClusterAgent.tokens.count}`,
);
});
describe('when token count is missing', () => {
const missingTokens = {
...defaultClusterAgent,
tokens: null,
};
beforeEach(() => {
return createWrapper({ clusterAgent: missingTokens });
});
it('displays token header with no count', () => {
expect(findTokenCount()).toMatchInterpolatedText(`${ClusterAgentShow.i18n.tokens}`);
});
});
it('renders token table', () => {
expect(wrapper.find(TokenTable).exists()).toBe(true);
});
describe('when the agent query is loading', () => {
beforeEach(() => {
return createWrapper({
clusterAgent: null,
queryResponse: jest.fn().mockReturnValue(new Promise(() => {})),
});
});
it('displays a loading icon', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
});
describe('when the agent query has errored', () => {
beforeEach(() => {
createWrapper({ clusterAgent: null, queryResponse: jest.fn().mockRejectedValue() });
return waitForPromises();
});
it('displays an alert message', () => {
expect(wrapper.find(GlAlert).exists()).toBe(true);
expect(wrapper.text()).toContain(ClusterAgentShow.i18n.loadingError);
});
});
});
import { GlEmptyState, GlLink, GlTooltip, GlTruncate } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import TokenTable from 'ee/clusters/agents/components/token_table.vue';
import { useFakeDate } from 'helpers/fake_date';
describe('ClusterAgentTokenTable', () => {
let wrapper;
useFakeDate([2021, 2, 15]);
const defaultTokens = [
{
id: '1',
createdAt: '2021-02-13T00:00:00Z',
description: 'Description of token 1',
createdByUser: {
name: 'user-1',
},
},
{
id: '2',
createdAt: '2021-02-10T00:00:00Z',
description: null,
createdByUser: null,
},
];
const createComponent = (tokens) => {
wrapper = mount(TokenTable, { propsData: { tokens } });
return wrapper.vm.$nextTick();
};
const findEmptyState = () => wrapper.find(GlEmptyState);
const findLink = () => wrapper.find(GlLink);
beforeEach(() => {
return createComponent(defaultTokens);
});
afterEach(() => {
wrapper.destroy();
});
it('displays a learn more link', () => {
const learnMoreLink = findLink();
expect(learnMoreLink.exists()).toBe(true);
expect(learnMoreLink.text()).toBe(TokenTable.i18n.learnMore);
});
it.each`
createdText | lineNumber
${'2 days ago'} | ${0}
${'5 days ago'} | ${1}
`(
'displays created information "$createdText" for line "$lineNumber"',
({ createdText, lineNumber }) => {
const tokens = wrapper.findAll('[data-testid="agent-token-created-time"]');
const token = tokens.at(lineNumber);
expect(token.text()).toBe(createdText);
},
);
it.each`
createdBy | lineNumber
${'user-1'} | ${0}
${'Unknown user'} | ${1}
`(
'displays creator information "$createdBy" for line "$lineNumber"',
({ createdBy, lineNumber }) => {
const tokens = wrapper.findAll('[data-testid="agent-token-created-user"]');
const token = tokens.at(lineNumber);
expect(token.text()).toBe(createdBy);
},
);
it.each`
description | truncatesText | hasTooltip | lineNumber
${'Description of token 1'} | ${true} | ${true} | ${0}
${''} | ${false} | ${false} | ${1}
`(
'displays description information "$description" for line "$lineNumber"',
({ description, truncatesText, hasTooltip, lineNumber }) => {
const tokens = wrapper.findAll('[data-testid="agent-token-description"]');
const token = tokens.at(lineNumber);
expect(token.text()).toContain(description);
expect(token.find(GlTruncate).exists()).toBe(truncatesText);
expect(token.find(GlTooltip).exists()).toBe(hasTooltip);
},
);
describe('when there are no tokens', () => {
beforeEach(() => {
return createComponent([]);
});
it('displays an empty state', () => {
const emptyState = findEmptyState();
expect(emptyState.exists()).toBe(true);
expect(emptyState.text()).toContain(TokenTable.i18n.noTokens);
});
});
});
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe GitlabSchema.types['ClusterAgent'] do RSpec.describe GitlabSchema.types['ClusterAgent'] do
let(:fields) { %i[created_at created_by_user id name project updated_at tokens] } let(:fields) { %i[created_at created_by_user id name project updated_at tokens web_path] }
it { expect(described_class.graphql_name).to eq('ClusterAgent') } it { expect(described_class.graphql_name).to eq('ClusterAgent') }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::ClusterAgentsHelper do
describe '#js_cluster_agent_details_data' do
let_it_be(:project) { create(:project) }
let(:agent_name) { 'agent-name' }
subject { helper.js_cluster_agent_details_data(agent_name, project) }
it 'returns name' do
expect(subject[:agent_name]).to eq(agent_name)
end
it 'returns project path' do
expect(subject[:project_path]).to eq(project.full_path)
end
end
end
...@@ -128,10 +128,10 @@ RSpec.describe Note, :elastic do ...@@ -128,10 +128,10 @@ RSpec.describe Note, :elastic do
expect { note.__elasticsearch__.as_indexed_json }.not_to raise_error expect { note.__elasticsearch__.as_indexed_json }.not_to raise_error
end end
where(:note_type, :permission, :access_level) do where(:note_type, :project_permission, :access_level) do
:note_on_issue | ProjectFeature::ENABLED | 'issues_access_level' :note_on_issue | ProjectFeature::ENABLED | 'issues_access_level'
:note_on_project_snippet | ProjectFeature::DISABLED | 'snippets_access_level' :note_on_project_snippet | ProjectFeature::DISABLED | 'snippets_access_level'
:note_on_personal_snippet | ProjectFeature::DISABLED | 'snippets_access_level' :note_on_personal_snippet | nil | false
:note_on_merge_request | ProjectFeature::PUBLIC | 'merge_requests_access_level' :note_on_merge_request | ProjectFeature::PUBLIC | 'merge_requests_access_level'
:note_on_commit | ProjectFeature::PRIVATE | 'repository_access_level' :note_on_commit | ProjectFeature::PRIVATE | 'repository_access_level'
:diff_note_on_merge_request | ProjectFeature::PUBLIC | 'merge_requests_access_level' :diff_note_on_merge_request | ProjectFeature::PUBLIC | 'merge_requests_access_level'
...@@ -141,25 +141,26 @@ RSpec.describe Note, :elastic do ...@@ -141,25 +141,26 @@ RSpec.describe Note, :elastic do
:legacy_diff_note_on_commit | ProjectFeature::PRIVATE | 'repository_access_level' :legacy_diff_note_on_commit | ProjectFeature::PRIVATE | 'repository_access_level'
:note_on_alert | ProjectFeature::PRIVATE | false :note_on_alert | ProjectFeature::PRIVATE | false
:note_on_design | ProjectFeature::ENABLED | false :note_on_design | ProjectFeature::ENABLED | false
:note_on_epic | ProjectFeature::ENABLED | false :note_on_epic | nil | false
:note_on_vulnerability | ProjectFeature::PRIVATE | false :note_on_vulnerability | ProjectFeature::PRIVATE | false
:discussion_note_on_vulnerability | ProjectFeature::PRIVATE | false :discussion_note_on_vulnerability | ProjectFeature::PRIVATE | false
:discussion_note_on_merge_request | ProjectFeature::PUBLIC | 'merge_requests_access_level' :discussion_note_on_merge_request | ProjectFeature::PUBLIC | 'merge_requests_access_level'
:discussion_note_on_issue | ProjectFeature::ENABLED | 'issues_access_level' :discussion_note_on_issue | ProjectFeature::ENABLED | 'issues_access_level'
:discussion_note_on_project_snippet | ProjectFeature::DISABLED | 'snippets_access_level' :discussion_note_on_project_snippet | ProjectFeature::DISABLED | 'snippets_access_level'
:discussion_note_on_personal_snippet | ProjectFeature::DISABLED | 'snippets_access_level' :discussion_note_on_personal_snippet | nil | false
:note_on_merge_request | ProjectFeature::PUBLIC | 'merge_requests_access_level' :note_on_merge_request | ProjectFeature::PUBLIC | 'merge_requests_access_level'
:discussion_note_on_commit | ProjectFeature::PRIVATE | 'repository_access_level' :discussion_note_on_commit | ProjectFeature::PRIVATE | 'repository_access_level'
:track_mr_picking_note | ProjectFeature::PUBLIC | 'merge_requests_access_level' :track_mr_picking_note | ProjectFeature::PUBLIC | 'merge_requests_access_level'
end end
with_them do with_them do
let_it_be(:project) { create(:project, :repository) } let!(:note) { create(note_type) } # rubocop:disable Rails/SaveBang
let!(:note) { create(note_type, project: project) } # rubocop:disable Rails/SaveBang let(:project) { note.project }
let(:note_json) { note.__elasticsearch__.as_indexed_json } let(:note_json) { note.__elasticsearch__.as_indexed_json }
before do before do
project.project_feature.update_attribute(access_level.to_sym, permission) if access_level.present? project.project_feature.update_attribute(access_level.to_sym, project_permission) if access_level.present?
end end
it 'does not contain permissions if remove_permissions_data_from_notes_documents is not finished' do it 'does not contain permissions if remove_permissions_data_from_notes_documents is not finished' do
...@@ -174,11 +175,15 @@ RSpec.describe Note, :elastic do ...@@ -174,11 +175,15 @@ RSpec.describe Note, :elastic do
it 'contains the correct permissions', :aggregate_failures do it 'contains the correct permissions', :aggregate_failures do
if access_level if access_level
expect(note_json).to have_key(access_level) expect(note_json).to have_key(access_level)
expect(note_json[access_level]).to eq(permission) expect(note_json[access_level]).to eq(project_permission)
end end
expect(note_json).to have_key('visibility_level') if project_permission
expect(note_json['visibility_level']).to eq(project.visibility_level) expect(note_json).to have_key('visibility_level')
expect(note_json['visibility_level']).to eq(project.visibility_level)
else
expect(note_json).not_to have_key('visibility_level')
end
end end
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Deployment do
describe 'state machine' do
context 'when deployment succeeded' do
let(:deployment) { create(:deployment, :running) }
it 'schedules Dora::DailyMetrics::RefreshWorker' do
freeze_time do
expect(::Dora::DailyMetrics::RefreshWorker)
.to receive(:perform_in).with(
5.minutes,
deployment.environment_id,
Time.current.to_date.to_s)
deployment.succeed!
end
end
context 'when dora_daily_metrics feature flag is disabled' do
before do
stub_feature_flags(dora_daily_metrics: false)
end
it 'does not schedule Dora::DailyMetrics::RefreshWorker' do
expect(::Dora::DailyMetrics::RefreshWorker).not_to receive(:perform_in)
deployment.succeed!
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Dora::DailyMetrics, type: :model do
describe 'associations' do
it { is_expected.to belong_to(:environment) }
end
describe '.refresh!' do
subject { described_class.refresh!(environment, date) }
around do |example|
freeze_time { example.run }
end
let_it_be(:project) { create(:project, :repository) }
let_it_be(:environment) { create(:environment, project: project) }
let(:date) { 1.day.ago.to_date }
context 'with finished deployments' do
before do
# Deployment finished before the date
previous_date = date - 1.day
create(:deployment, :success, environment: environment, finished_at: previous_date)
create(:deployment, :failed, environment: environment, finished_at: previous_date)
# Deployment finished on the date
create(:deployment, :success, environment: environment, finished_at: date)
create(:deployment, :failed, environment: environment, finished_at: date)
# Deployment finished after the date
next_date = date + 1.day
create(:deployment, :success, environment: environment, finished_at: next_date)
create(:deployment, :failed, environment: environment, finished_at: next_date)
end
it 'inserts the daily metrics' do
expect { subject }.to change { Dora::DailyMetrics.count }.by(1)
metrics = environment.dora_daily_metrics.find_by_date(date)
expect(metrics.deployment_frequency).to eq(1)
expect(metrics.lead_time_for_changes_in_seconds).to be_nil
end
context 'when there is an existing daily metric' do
before do
create(:dora_daily_metrics, environment: environment, date: date, deployment_frequency: 0)
end
it 'updates the daily metrics' do
expect { subject }.not_to change { Dora::DailyMetrics.count }
metrics = environment.dora_daily_metrics.find_by_date(date)
expect(metrics.deployment_frequency).to eq(1)
end
end
end
context 'with finished deployments and merged MRs' do
before do
merge_requests = []
# Merged 1 day ago
merge_requests << create(:merge_request, :with_merged_metrics, project: project).tap do |merge_request|
merge_request.metrics.update!(merged_at: date - 1.day)
end
# Merged 2 days ago
merge_requests << create(:merge_request, :with_merged_metrics, project: project).tap do |merge_request|
merge_request.metrics.update!(merged_at: date - 2.days)
end
# Merged 3 days ago
merge_requests << create(:merge_request, :with_merged_metrics, project: project).tap do |merge_request|
merge_request.metrics.update!(merged_at: date - 3.days)
end
# Deployment finished on the date
create(:deployment, :success, environment: environment, finished_at: date, merge_requests: merge_requests)
end
it 'inserts the daily metrics' do
subject
metrics = environment.dora_daily_metrics.find_by_date(date)
expect(metrics.lead_time_for_changes_in_seconds).to eq(2.days.to_i) # median
end
context 'when there is an existing daily metric' do
let!(:dora_daily_metrics) { create(:dora_daily_metrics, environment: environment, date: date, lead_time_for_changes_in_seconds: nil) }
it 'updates the daily metrics' do
expect { subject }
.to change { dora_daily_metrics.reload.lead_time_for_changes_in_seconds }
.from(nil)
.to(2.days.to_i)
end
end
end
context 'when date is invalid type' do
let(:date) { '2021-02-03' }
it 'raises an error' do
expect { subject }.to raise_error(ArgumentError)
end
end
end
end
...@@ -8,6 +8,8 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do ...@@ -8,6 +8,8 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:environment) { create(:environment, project: project) } let(:environment) { create(:environment, project: project) }
it { is_expected.to have_many(:dora_daily_metrics) }
describe '.deployed_to_cluster' do describe '.deployed_to_cluster' do
let!(:environment) { create(:environment) } let!(:environment) { create(:environment) }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::ClusterAgentsController do
let_it_be(:cluster_agent) { create(:cluster_agent) }
let(:project) { cluster_agent.project }
describe 'GET #show' do
subject { get project_cluster_agent_path(project, cluster_agent.name) }
context 'when user is unauthorized' do
let_it_be(:user) { create(:user) }
before do
project.add_developer(user)
sign_in(user)
subject
end
it 'shows 404' do
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when user is authorized' do
let(:user) { project.creator }
context 'without premium plan' do
before do
sign_in(user)
subject
end
it 'shows 404' do
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'with premium plan' do
before do
stub_licensed_features(cluster_agents: true)
sign_in(user)
subject
end
it 'renders content' do
expect(response).to be_successful
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Dora::DailyMetrics::RefreshWorker do
let_it_be(:environment) { create(:environment) }
let(:worker) { described_class.new }
describe '#perform' do
subject { worker.perform(environment_id, date.to_s) }
let(:environment_id) { environment.id }
let(:date) { Time.current.to_date }
it 'refreshes the DORA metrics on the environment and date' do
expect(::Dora::DailyMetrics).to receive(:refresh!).with(environment, date)
subject
end
context 'when the date is not parsable' do
let(:date) { 'abc' }
it 'raises an error' do
expect { subject }.to raise_error(Date::Error)
end
end
context 'when an environment does not exist' do
let(:environment_id) { non_existing_record_id }
it 'does not refresh' do
expect(::Dora::DailyMetrics).not_to receive(:refresh!)
subject
end
end
end
end
...@@ -6317,21 +6317,42 @@ msgstr "" ...@@ -6317,21 +6317,42 @@ msgstr ""
msgid "Cluster type must be specificed for Stages::ClusterEndpointInserter" msgid "Cluster type must be specificed for Stages::ClusterEndpointInserter"
msgstr "" msgstr ""
msgid "ClusterAgents|Access tokens"
msgstr ""
msgid "ClusterAgents|An error occurred while loading your GitLab Agents" msgid "ClusterAgents|An error occurred while loading your GitLab Agents"
msgstr "" msgstr ""
msgid "ClusterAgents|An error occurred while loading your agent"
msgstr ""
msgid "ClusterAgents|Configuration" msgid "ClusterAgents|Configuration"
msgstr "" msgstr ""
msgid "ClusterAgents|Connect your cluster with the GitLab Agent" msgid "ClusterAgents|Connect your cluster with the GitLab Agent"
msgstr "" msgstr ""
msgid "ClusterAgents|Created by"
msgstr ""
msgid "ClusterAgents|Created by %{name} %{time}"
msgstr ""
msgid "ClusterAgents|Date created"
msgstr ""
msgid "ClusterAgents|Description"
msgstr ""
msgid "ClusterAgents|Integrate Kubernetes with a GitLab Agent" msgid "ClusterAgents|Integrate Kubernetes with a GitLab Agent"
msgstr "" msgstr ""
msgid "ClusterAgents|Integrate with the GitLab Agent" msgid "ClusterAgents|Integrate with the GitLab Agent"
msgstr "" msgstr ""
msgid "ClusterAgents|Learn how to create an agent access token"
msgstr ""
msgid "ClusterAgents|Name" msgid "ClusterAgents|Name"
msgstr "" msgstr ""
...@@ -6341,6 +6362,15 @@ msgstr "" ...@@ -6341,6 +6362,15 @@ msgstr ""
msgid "ClusterAgents|The GitLab Kubernetes Agent allows an Infrastructure as Code, GitOps approach to integrating Kubernetes clusters with GitLab. %{linkStart}Learn more.%{linkEnd}" msgid "ClusterAgents|The GitLab Kubernetes Agent allows an Infrastructure as Code, GitOps approach to integrating Kubernetes clusters with GitLab. %{linkStart}Learn more.%{linkEnd}"
msgstr "" msgstr ""
msgid "ClusterAgents|This agent has no tokens"
msgstr ""
msgid "ClusterAgents|Unknown user"
msgstr ""
msgid "ClusterAgents|You will need to create a token to connect to your agent"
msgstr ""
msgid "ClusterAgent|This feature is only available for premium plans" msgid "ClusterAgent|This feature is only available for premium plans"
msgstr "" msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment