diff options
47 files changed, 855 insertions, 205 deletions
diff --git a/.gitlab/ci/docs.gitlab-ci.yml b/.gitlab/ci/docs.gitlab-ci.yml index c93b503139f..00013d5d2ba 100644 --- a/.gitlab/ci/docs.gitlab-ci.yml +++ b/.gitlab/ci/docs.gitlab-ci.yml @@ -76,8 +76,7 @@ graphql-reference-verify: - .default-only - .default-before_script - .only:changes-graphql - variables: - SETUP_DB: "false" + - .use-pg9 stage: test needs: ["setup-test-env"] script: diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 908dc730aa4..735cbb8e356 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -2,6 +2,8 @@ import $ from 'jquery'; import _ from 'underscore'; import axios from './lib/utils/axios_utils'; import { joinPaths } from './lib/utils/url_utility'; +import flash from '~/flash'; +import { __ } from '~/locale'; const Api = { groupsPath: '/api/:version/groups.json', @@ -29,6 +31,7 @@ const Api = { usersPath: '/api/:version/users.json', userPath: '/api/:version/users/:id', userStatusPath: '/api/:version/users/:id/status', + userProjectsPath: '/api/:version/users/:id/projects', userPostStatusPath: '/api/:version/user/status', commitPath: '/api/:version/projects/:id/repository/commits', applySuggestionPath: '/api/:version/suggestions/:id/apply', @@ -239,7 +242,8 @@ const Api = { .get(url, { params: Object.assign({}, defaults, options), }) - .then(({ data }) => callback(data)); + .then(({ data }) => callback(data)) + .catch(() => flash(__('Something went wrong while fetching projects'))); }, commitMultiple(id, data) { @@ -348,6 +352,20 @@ const Api = { }); }, + userProjects(userId, query, options, callback) { + const url = Api.buildUrl(Api.userProjectsPath).replace(':id', userId); + const defaults = { + search: query, + per_page: 20, + }; + return axios + .get(url, { + params: Object.assign({}, defaults, options), + }) + .then(({ data }) => callback(data)) + .catch(() => flash(__('Something went wrong while fetching projects'))); + }, + branches(id, query = '', options = {}) { const url = Api.buildUrl(this.createBranchPath).replace(':id', encodeURIComponent(id)); diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index 17251ccdffb..d471dcb1b06 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -13,6 +13,7 @@ import ClustersService from './services/clusters_service'; import ClustersStore from './stores/clusters_store'; import Applications from './components/applications.vue'; import setupToggleButtons from '../toggle_buttons'; +import initProjectSelectDropdown from '~/project_select'; const Environments = () => import('ee_component/clusters/components/environments.vue'); @@ -110,8 +111,10 @@ export default class Clusters { this.ingressDomainHelpText && this.ingressDomainHelpText.querySelector('.js-ingress-domain-snippet'); + initProjectSelectDropdown(); Clusters.initDismissableCallout(); initSettingsPanels(); + const toggleButtonsContainer = document.querySelector('.js-cluster-enable-toggle-area'); if (toggleButtonsContainer) { setupToggleButtons(toggleButtonsContainer); diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 725af920691..94c7bf1cee4 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -174,7 +174,7 @@ export default { return this.customMetricsAvailable && this.customMetricsPath.length; }, ...mapState('monitoringDashboard', [ - 'groups', + 'dashboard', 'emptyState', 'showEmptyState', 'environments', @@ -238,6 +238,7 @@ export default { 'setGettingStartedEmptyState', 'setEndpoints', 'setDashboardEnabled', + 'setPanelGroupMetrics', ]), chartsWithData(charts) { if (!this.useDashboardEndpoint) { @@ -274,10 +275,17 @@ export default { this.$toast.show(__('Link copied')); }, // TODO: END - removeGraph(metrics, graphIndex) { - // At present graphs will not be removed, they should removed using the vuex store - // See https://gitlab.com/gitlab-org/gitlab/issues/27835 - metrics.splice(graphIndex, 1); + updateMetrics(key, metrics) { + this.setPanelGroupMetrics({ + metrics, + key, + }); + }, + removeMetric(key, metrics, graphIndex) { + this.setPanelGroupMetrics({ + metrics: metrics.filter((v, i) => i !== graphIndex), + key, + }); }, showInvalidDateError() { createFlash(s__('Metrics|Link contains an invalid time window.')); @@ -447,7 +455,7 @@ export default { <div v-if="!showEmptyState"> <graph-group - v-for="(groupData, index) in groups" + v-for="(groupData, index) in dashboard.panel_groups" :key="`${groupData.group}.${groupData.priority}`" :name="groupData.group" :show-panels="showPanels" @@ -455,10 +463,11 @@ export default { > <template v-if="additionalPanelTypesEnabled"> <vue-draggable - :list="groupData.metrics" + :value="groupData.metrics" group="metrics-dashboard" :component-data="{ attrs: { class: 'row mx-0 w-100' } }" :disabled="!isRearrangingPanels" + @input="updateMetrics(groupData.key, $event)" > <div v-for="(graphData, graphIndex) in groupData.metrics" @@ -470,7 +479,7 @@ export default { <div v-if="isRearrangingPanels" class="draggable-remove js-draggable-remove p-2 w-100 position-absolute d-flex justify-content-end" - @click="removeGraph(groupData.metrics, graphIndex)" + @click="removeMetric(groupData.key, groupData.metrics, graphIndex)" > <a class="mx-2 p-2 draggable-remove-link" :aria-label="__('Remove')" ><icon name="close" diff --git a/app/assets/javascripts/monitoring/components/embed.vue b/app/assets/javascripts/monitoring/components/embed.vue index 7857aaa6ecc..80e9b7a9c57 100644 --- a/app/assets/javascripts/monitoring/components/embed.vue +++ b/app/assets/javascripts/monitoring/components/embed.vue @@ -35,9 +35,9 @@ export default { }; }, computed: { - ...mapState('monitoringDashboard', ['groups', 'metricsWithData']), + ...mapState('monitoringDashboard', ['dashboard', 'metricsWithData']), charts() { - const groupWithMetrics = this.groups.find(group => + const groupWithMetrics = this.dashboard.panel_groups.find(group => group.metrics.find(chart => this.chartHasData(chart)), ) || { metrics: [] }; diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js index 2cf34ddb45b..2f793a9e162 100644 --- a/app/assets/javascripts/monitoring/stores/actions.js +++ b/app/assets/javascripts/monitoring/stores/actions.js @@ -166,7 +166,7 @@ export const fetchPrometheusMetrics = ({ state, commit, dispatch }, params) => { commit(types.REQUEST_METRICS_DATA); const promises = []; - state.groups.forEach(group => { + state.dashboard.panel_groups.forEach(group => { group.panels.forEach(panel => { panel.metrics.forEach(metric => { promises.push(dispatch('fetchPrometheusMetric', { metric, params })); @@ -221,5 +221,15 @@ export const fetchEnvironmentsData = ({ state, dispatch }) => { }); }; +/** + * Set a new array of metrics to a panel group + * @param {*} data An object containing + * - `key` with a unique panel key + * - `metrics` with the metrics array + */ +export const setPanelGroupMetrics = ({ commit }, data) => { + commit(types.SET_PANEL_GROUP_METRICS, data); +}; + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js index 9c546427c6e..4d05fa7beb9 100644 --- a/app/assets/javascripts/monitoring/stores/mutation_types.js +++ b/app/assets/javascripts/monitoring/stores/mutation_types.js @@ -16,3 +16,4 @@ export const SET_ENDPOINTS = 'SET_ENDPOINTS'; export const SET_GETTING_STARTED_EMPTY_STATE = 'SET_GETTING_STARTED_EMPTY_STATE'; export const SET_NO_DATA_EMPTY_STATE = 'SET_NO_DATA_EMPTY_STATE'; export const SET_SHOW_ERROR_BANNER = 'SET_SHOW_ERROR_BANNER'; +export const SET_PANEL_GROUP_METRICS = 'SET_PANEL_GROUP_METRICS'; diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js index 320b33d3d69..9e38314f8af 100644 --- a/app/assets/javascripts/monitoring/stores/mutations.js +++ b/app/assets/javascripts/monitoring/stores/mutations.js @@ -1,6 +1,7 @@ import Vue from 'vue'; +import { slugify } from '~/lib/utils/text_utility'; import * as types from './mutation_types'; -import { normalizeMetrics, sortMetrics, normalizeMetric, normalizeQueryResult } from './utils'; +import { normalizeMetrics, normalizeMetric, normalizeQueryResult } from './utils'; const normalizePanel = panel => panel.metrics.map(normalizeMetric); @@ -10,10 +11,12 @@ export default { state.showEmptyState = true; }, [types.RECEIVE_METRICS_DATA_SUCCESS](state, groupData) { - state.groups = groupData.map(group => { + state.dashboard.panel_groups = groupData.map((group, i) => { + const key = `${slugify(group.group || 'default')}-${i}`; let { metrics = [], panels = [] } = group; // each panel has metric information that needs to be normalized + panels = panels.map(panel => ({ ...panel, metrics: normalizePanel(panel), @@ -32,11 +35,12 @@ export default { return { ...group, panels, - metrics: normalizeMetrics(sortMetrics(metrics)), + key, + metrics: normalizeMetrics(metrics), }; }); - if (!state.groups.length) { + if (!state.dashboard.panel_groups.length) { state.emptyState = 'noData'; } else { state.showEmptyState = false; @@ -65,7 +69,7 @@ export default { state.showEmptyState = false; - state.groups.forEach(group => { + state.dashboard.panel_groups.forEach(group => { group.metrics.forEach(metric => { metric.queries.forEach(query => { if (query.metric_id === metricId) { @@ -105,4 +109,8 @@ export default { [types.SET_SHOW_ERROR_BANNER](state, enabled) { state.showErrorBanner = enabled; }, + [types.SET_PANEL_GROUP_METRICS](state, payload) { + const panelGroup = state.dashboard.panel_groups.find(pg => payload.key === pg.key); + panelGroup.metrics = payload.metrics; + }, }; diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js index e894e988f6a..5d7c46ca0c5 100644 --- a/app/assets/javascripts/monitoring/stores/state.js +++ b/app/assets/javascripts/monitoring/stores/state.js @@ -12,7 +12,9 @@ export default () => ({ emptyState: 'gettingStarted', showEmptyState: true, showErrorBanner: true, - groups: [], + dashboard: { + panel_groups: [], + }, deploymentData: [], environments: [], metricsWithData: [], diff --git a/app/assets/javascripts/monitoring/stores/utils.js b/app/assets/javascripts/monitoring/stores/utils.js index a19829f0c65..8a396b15a31 100644 --- a/app/assets/javascripts/monitoring/stores/utils.js +++ b/app/assets/javascripts/monitoring/stores/utils.js @@ -82,12 +82,6 @@ export const normalizeMetric = (metric = {}) => 'id', ); -export const sortMetrics = metrics => - _.chain(metrics) - .sortBy('title') - .sortBy('weight') - .value(); - export const normalizeQueryResult = timeSeries => { let normalizedResult = {}; diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index 0fbb7e5ca42..81f2fc2e686 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -9,7 +9,9 @@ const projectSelect = () => { $('.ajax-project-select').each(function(i, select) { var placeholder; const simpleFilter = $(select).data('simpleFilter') || false; + const isInstantiated = $(select).data('select2'); this.groupId = $(select).data('groupId'); + this.userId = $(select).data('userId'); this.includeGroups = $(select).data('includeGroups'); this.allProjects = $(select).data('allProjects') || false; this.orderBy = $(select).data('orderBy') || 'id'; @@ -63,6 +65,18 @@ const projectSelect = () => { }, projectsCallback, ); + } else if (_this.userId) { + return Api.userProjects( + _this.userId, + query.term, + { + with_issues_enabled: _this.withIssuesEnabled, + with_merge_requests_enabled: _this.withMergeRequestsEnabled, + with_shared: _this.withShared, + include_subgroups: _this.includeProjectsInSubgroups, + }, + projectsCallback, + ); } else { return Api.projects( query.term, @@ -96,7 +110,7 @@ const projectSelect = () => { dropdownCssClass: 'ajax-project-dropdown', }); - if (simpleFilter) return select; + if (isInstantiated || simpleFilter) return select; return new ProjectSelectComboButton(select); }); }; diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb index 1899278ff3c..a5ddf316572 100644 --- a/app/graphql/gitlab_schema.rb +++ b/app/graphql/gitlab_schema.rb @@ -46,7 +46,7 @@ class GitlabSchema < GraphQL::Schema super(query_str, **kwargs) end - def id_from_object(object) + def id_from_object(object, _type = nil, _ctx = nil) unless object.respond_to?(:to_global_id) # This is an error in our schema and needs to be solved. So raise a # more meaningful error message @@ -57,7 +57,7 @@ class GitlabSchema < GraphQL::Schema object.to_global_id end - def object_from_id(global_id) + def object_from_id(global_id, _ctx = nil) gid = GlobalID.parse(global_id) unless gid diff --git a/app/graphql/mutations/merge_requests/set_milestone.rb b/app/graphql/mutations/merge_requests/set_milestone.rb new file mode 100644 index 00000000000..707d6677952 --- /dev/null +++ b/app/graphql/mutations/merge_requests/set_milestone.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Mutations + module MergeRequests + class SetMilestone < Base + graphql_name 'MergeRequestSetMilestone' + + argument :milestone_id, + GraphQL::ID_TYPE, + required: false, + loads: Types::MilestoneType, + description: <<~DESC + The milestone to assign to the merge request. + DESC + + def resolve(project_path:, iid:, milestone: nil) + merge_request = authorized_find!(project_path: project_path, iid: iid) + project = merge_request.project + + ::MergeRequests::UpdateService.new(project, current_user, milestone: milestone) + .execute(merge_request) + + { + merge_request: merge_request, + errors: merge_request.errors.full_messages + } + end + end + end +end diff --git a/app/graphql/types/milestone_type.rb b/app/graphql/types/milestone_type.rb index 1d3a1231bca..9c3afb28674 100644 --- a/app/graphql/types/milestone_type.rb +++ b/app/graphql/types/milestone_type.rb @@ -6,6 +6,8 @@ module Types authorize :read_milestone + field :id, GraphQL::ID_TYPE, null: false, + description: 'ID of the milestone' field :description, GraphQL::STRING_TYPE, null: true, description: 'Description of the milestone' field :title, GraphQL::STRING_TYPE, null: false, diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 17f922a5e54..c636bf0e31f 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -9,6 +9,7 @@ module Types mount_mutation Mutations::AwardEmojis::Add mount_mutation Mutations::AwardEmojis::Remove mount_mutation Mutations::AwardEmojis::Toggle + mount_mutation Mutations::MergeRequests::SetMilestone mount_mutation Mutations::MergeRequests::SetWip, calls_gitaly: true mount_mutation Mutations::Notes::Create::Note, calls_gitaly: true mount_mutation Mutations::Notes::Create::DiffNote, calls_gitaly: true diff --git a/app/models/project_snippet.rb b/app/models/project_snippet.rb index b3585c4cf4c..e732c1bd86f 100644 --- a/app/models/project_snippet.rb +++ b/app/models/project_snippet.rb @@ -2,13 +2,6 @@ class ProjectSnippet < Snippet belongs_to :project - belongs_to :author, class_name: "User" validates :project, presence: true - - # Scopes - scope :fresh, -> { order("created_at DESC") } - - participant :author - participant :notes_with_associations end diff --git a/app/serializers/projects/serverless/service_entity.rb b/app/serializers/projects/serverless/service_entity.rb index a1e0bf02d11..10360e575bb 100644 --- a/app/serializers/projects/serverless/service_entity.rb +++ b/app/serializers/projects/serverless/service_entity.rb @@ -44,28 +44,52 @@ module Projects end expose :url do |service| - service.dig('status', 'url') || "http://#{service.dig('status', 'domain')}" + knative_06_07_url(service) || knative_05_url(service) end expose :description do |service| + knative_07_description(service) || knative_05_06_description(service) + end + + expose :image do |service| service.dig( 'spec', 'runLatest', 'configuration', - 'revisionTemplate', + 'build', + 'template', + 'name') + end + + private + + def knative_07_description(service) + service.dig( + 'spec', + 'template', 'metadata', 'annotations', - 'Description') + 'Description' + ) end - expose :image do |service| + def knative_05_url(service) + "http://#{service.dig('status', 'domain')}" + end + + def knative_06_07_url(service) + service.dig('status', 'url') + end + + def knative_05_06_description(service) service.dig( 'spec', 'runLatest', 'configuration', - 'build', - 'template', - 'name') + 'revisionTemplate', + 'metadata', + 'annotations', + 'Description') end end end diff --git a/app/views/clusters/clusters/_advanced_settings.html.haml b/app/views/clusters/clusters/_advanced_settings.html.haml index 8005dcbf65f..493d7a00854 100644 --- a/app/views/clusters/clusters/_advanced_settings.html.haml +++ b/app/views/clusters/clusters/_advanced_settings.html.haml @@ -1,3 +1,9 @@ +- group_id = @cluster.group.id if @cluster.group_type? + +- if @cluster.project_type? + - group_id = @cluster.project.group.id if @cluster.project.group + - user_id = @cluster.project.namespace.owner_id unless group_id + - if can?(current_user, :admin_cluster, @cluster) - unless @cluster.provided_by_user? .append-bottom-20 @@ -7,6 +13,21 @@ - link_gke = link_to(s_('ClusterIntegration|Google Kubernetes Engine'), @cluster.gke_cluster_url, target: '_blank', rel: 'noopener noreferrer') = s_('ClusterIntegration|Manage your Kubernetes cluster by visiting %{link_gke}').html_safe % { link_gke: link_gke } + = form_for @cluster, url: clusterable.cluster_path(@cluster), as: :cluster, html: { class: 'cluster_management_form' } do |field| + + %h5 + = s_('ClusterIntegration|Cluster management project (alpha)') + + .form-group + .form-text.text-muted + = project_select_tag('cluster[management_project_id]', class: 'hidden-filter-value', toggle_class: 'js-project-search js-project-filter js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit', + placeholder: _('Select project'), idAttribute: 'id', data: { order_by: 'last_activity_at', idattribute: 'id', simple_filter: true, allow_clear: true, include_groups: false, include_projects_in_subgroups: true, group_id: group_id, user_id: user_id }, value: @cluster.management_project_id) + .text-muted + = s_('ClusterIntegration|A cluster management project can be used to run deployment jobs with Kubernetes <code>cluster-admin</code> privileges.').html_safe + = link_to _('More information'), help_page_path('user/clusters/management_project.md'), target: '_blank' + .form-group + = field.submit _('Save changes'), class: 'btn btn-success qa-save-domain' + .sub-section.form-group %h4.text-danger = s_('ClusterIntegration|Remove Kubernetes cluster integration') diff --git a/changelogs/unreleased/30695-remove-add-btn-in-approval-rule.yml b/changelogs/unreleased/30695-remove-add-btn-in-approval-rule.yml new file mode 100644 index 00000000000..5ef0c9ef67a --- /dev/null +++ b/changelogs/unreleased/30695-remove-add-btn-in-approval-rule.yml @@ -0,0 +1,5 @@ +--- +title: Can directly add approvers to approval rule +merge_request: 18965 +author: +type: changed diff --git a/changelogs/unreleased/31919-graphql-MR-sidebar-mutations.yml b/changelogs/unreleased/31919-graphql-MR-sidebar-mutations.yml new file mode 100644 index 00000000000..e8d47abd169 --- /dev/null +++ b/changelogs/unreleased/31919-graphql-MR-sidebar-mutations.yml @@ -0,0 +1,5 @@ +--- +title: 'GraphQL: Add Merge Request milestone mutation' +merge_request: 19257 +author: +type: added diff --git a/changelogs/unreleased/34372-serverless-function-description-does-not-show-up-for-newly-created-.yml b/changelogs/unreleased/34372-serverless-function-description-does-not-show-up-for-newly-created-.yml new file mode 100644 index 00000000000..0a7bfd5fb4f --- /dev/null +++ b/changelogs/unreleased/34372-serverless-function-description-does-not-show-up-for-newly-created-.yml @@ -0,0 +1,5 @@ +--- +title: Fix serverless function descriptions not showing on Knative 0.7 +merge_request: 18973 +author: +type: fixed diff --git a/changelogs/unreleased/34519-move-planels-in-dashboard-save-to-the-vuex-store.yml b/changelogs/unreleased/34519-move-planels-in-dashboard-save-to-the-vuex-store.yml new file mode 100644 index 00000000000..669a0c8bd93 --- /dev/null +++ b/changelogs/unreleased/34519-move-planels-in-dashboard-save-to-the-vuex-store.yml @@ -0,0 +1,5 @@ +--- +title: Save dashboard changes by the user into the vuex store +merge_request: 18862 +author: +type: changed diff --git a/changelogs/unreleased/fe-cluster-management-project.yml b/changelogs/unreleased/fe-cluster-management-project.yml new file mode 100644 index 00000000000..43b4ddc8724 --- /dev/null +++ b/changelogs/unreleased/fe-cluster-management-project.yml @@ -0,0 +1,5 @@ +--- +title: Add ability to select a Cluster management project +merge_request: 18928 +author: +type: added diff --git a/changelogs/unreleased/remove-dind-for-ds.yml b/changelogs/unreleased/remove-dind-for-ds.yml new file mode 100644 index 00000000000..b60c983d91e --- /dev/null +++ b/changelogs/unreleased/remove-dind-for-ds.yml @@ -0,0 +1,5 @@ +--- +title: Dependency Scanning template that doesn't rely on Docker-in-Docker +merge_request: +author: +type: other diff --git a/config/routes/project.rb b/config/routes/project.rb index 24b4f310350..d628e1ea650 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -433,6 +433,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do Gitlab.ee do get :logs + get '/pods/(:pod_name)/containers/(:container_name)/logs', to: 'environments#k8s_pod_logs', as: :k8s_pod_logs end end diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 1b663acd5fb..bfda8ff1194 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -473,6 +473,14 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph | `cherryPickOnCurrentMergeRequest` | Boolean! | Whether or not a user can perform `cherry_pick_on_current_merge_request` on this resource | | `revertOnCurrentMergeRequest` | Boolean! | Whether or not a user can perform `revert_on_current_merge_request` on this resource | +### MergeRequestSetMilestonePayload + +| Name | Type | Description | +| --- | ---- | ---------- | +| `clientMutationId` | String | A unique identifier for the client performing the mutation. | +| `errors` | String! => Array | Reasons why the mutation failed. | +| `mergeRequest` | MergeRequest | The merge request after mutation | + ### MergeRequestSetWipPayload | Name | Type | Description | @@ -492,6 +500,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph | Name | Type | Description | | --- | ---- | ---------- | +| `id` | ID! | ID of the milestone | | `description` | String | Description of the milestone | | `title` | String! | Title of the milestone | | `state` | String! | State of the milestone | diff --git a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml index c8930bc6263..53ba9792bd0 100644 --- a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml @@ -4,6 +4,12 @@ # List of the variables: https://gitlab.com/gitlab-org/security-products/dependency-scanning#settings # How to set: https://docs.gitlab.com/ee/ci/yaml/#variables +variables: + DS_ANALYZER_IMAGE_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers" + DS_DEFAULT_ANALYZERS: "gemnasium, retire.js, gemnasium-python, gemnasium-maven, bundler-audit" + DS_MAJOR_VERSION: 2 + DS_DISABLE_DIND: "false" + dependency_scanning: stage: test image: docker:stable @@ -61,3 +67,63 @@ dependency_scanning: except: variables: - $DEPENDENCY_SCANNING_DISABLED + - $DS_DISABLE_DIND == 'true' + +.analyzer: + extends: dependency_scanning + services: [] + except: + variables: + - $DS_DISABLE_DIND == 'false' + script: + - /analyzer run + +gemnasium-dependency_scanning: + extends: .analyzer + image: + name: "$DS_ANALYZER_IMAGE_PREFIX/gemnasium:$DS_MAJOR_VERSION" + only: + variables: + - $GITLAB_FEATURES =~ /\bdependency_scanning\b/ && + $DS_DEFAULT_ANALYZERS =~ /gemnasium/ && + $CI_PROJECT_REPOSITORY_LANGUAGES =~ /ruby|javascript|php/ + +gemnasium-maven-dependency_scanning: + extends: .analyzer + image: + name: "$DS_ANALYZER_IMAGE_PREFIX/gemnasium-maven:$DS_MAJOR_VERSION" + only: + variables: + - $GITLAB_FEATURES =~ /\bdependency_scanning\b/ && + $DS_DEFAULT_ANALYZERS =~ /gemnasium-maven/ && + $CI_PROJECT_REPOSITORY_LANGUAGES =~ /\bjava\b/ + +gemnasium-python-dependency_scanning: + extends: .analyzer + image: + name: "$DS_ANALYZER_IMAGE_PREFIX/gemnasium-python:$DS_MAJOR_VERSION" + only: + variables: + - $GITLAB_FEATURES =~ /\bdependency_scanning\b/ && + $DS_DEFAULT_ANALYZERS =~ /gemnasium-python/ && + $CI_PROJECT_REPOSITORY_LANGUAGES =~ /python/ + +bundler-audit-dependency_scanning: + extends: .analyzer + image: + name: "$DS_ANALYZER_IMAGE_PREFIX/bundler-audit:$DS_MAJOR_VERSION" + only: + variables: + - $GITLAB_FEATURES =~ /\bdependency_scanning\b/ && + $DS_DEFAULT_ANALYZERS =~ /bundler-audit/ && + $CI_PROJECT_REPOSITORY_LANGUAGES =~ /ruby/ + +retire-js-dependency_scanning: + extends: .analyzer + image: + name: "$DS_ANALYZER_IMAGE_PREFIX/retire.js:$DS_MAJOR_VERSION" + only: + variables: + - $GITLAB_FEATURES =~ /\bdependency_scanning\b/ && + $DS_DEFAULT_ANALYZERS =~ /retire.js/ && + $CI_PROJECT_REPOSITORY_LANGUAGES =~ /javascript/ diff --git a/lib/gitlab/database_importers/self_monitoring/project/create_service.rb b/lib/gitlab/database_importers/self_monitoring/project/create_service.rb index dfef158cc1d..4677e984305 100644 --- a/lib/gitlab/database_importers/self_monitoring/project/create_service.rb +++ b/lib/gitlab/database_importers/self_monitoring/project/create_service.rb @@ -176,19 +176,11 @@ module Gitlab end def prometheus_enabled? - Gitlab.config.prometheus.enable if Gitlab.config.prometheus - rescue Settingslogic::MissingSetting - log_error('prometheus.enable is not present in config/gitlab.yml') - - false + ::Gitlab::Prometheus::Internal.prometheus_enabled? end def prometheus_listen_address - Gitlab.config.prometheus.listen_address.to_s if Gitlab.config.prometheus - rescue Settingslogic::MissingSetting - log_error('Prometheus listen_address is not present in config/gitlab.yml') - - nil + ::Gitlab::Prometheus::Internal.listen_address end def instance_admins @@ -231,23 +223,7 @@ module Gitlab end def internal_prometheus_listen_address_uri - if prometheus_listen_address.starts_with?('0.0.0.0:') - # 0.0.0.0:9090 - port = ':' + prometheus_listen_address.split(':').second - 'http://localhost' + port - - elsif prometheus_listen_address.starts_with?(':') - # :9090 - 'http://localhost' + prometheus_listen_address - - elsif prometheus_listen_address.starts_with?('http') - # https://localhost:9090 - prometheus_listen_address - - else - # localhost:9090 - 'http://' + prometheus_listen_address - end + ::Gitlab::Prometheus::Internal.uri end def prometheus_service_attributes diff --git a/lib/gitlab/etag_caching/router.rb b/lib/gitlab/etag_caching/router.rb index 3d14a8dde8d..efddda0ec65 100644 --- a/lib/gitlab/etag_caching/router.rb +++ b/lib/gitlab/etag_caching/router.rb @@ -3,8 +3,6 @@ module Gitlab module EtagCaching class Router - prepend_if_ee('EE::Gitlab::EtagCaching::Router') # rubocop: disable Cop/InjectEnterpriseEditionModule - Route = Struct.new(:regexp, :name) # We enable an ETag for every request matching the regex. # To match a regex the path needs to match the following: @@ -80,3 +78,5 @@ module Gitlab end end end + +Gitlab::EtagCaching::Router.prepend_if_ee('EE::Gitlab::EtagCaching::Router') diff --git a/lib/gitlab/prometheus/internal.rb b/lib/gitlab/prometheus/internal.rb new file mode 100644 index 00000000000..d59352119ba --- /dev/null +++ b/lib/gitlab/prometheus/internal.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Gitlab + module Prometheus + class Internal + def self.uri + return if listen_address.blank? + + if listen_address.starts_with?('0.0.0.0:') + # 0.0.0.0:9090 + port = ':' + listen_address.split(':').second + 'http://localhost' + port + + elsif listen_address.starts_with?(':') + # :9090 + 'http://localhost' + listen_address + + elsif listen_address.starts_with?('http') + # https://localhost:9090 + listen_address + + else + # localhost:9090 + 'http://' + listen_address + end + end + + def self.listen_address + Gitlab.config.prometheus.listen_address.to_s if Gitlab.config.prometheus + rescue Settingslogic::MissingSetting + Gitlab::AppLogger.error('Prometheus listen_address is not present in config/gitlab.yml') + + nil + end + + def self.prometheus_enabled? + Gitlab.config.prometheus.enable if Gitlab.config.prometheus + rescue Settingslogic::MissingSetting + Gitlab::AppLogger.error('prometheus.enable is not present in config/gitlab.yml') + + false + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 93cbf73979e..f95f329442c 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3399,6 +3399,9 @@ msgstr "" msgid "ClusterIntegration|%{title} updated successfully." msgstr "" +msgid "ClusterIntegration|A cluster management project can be used to run deployment jobs with Kubernetes <code>cluster-admin</code> privileges." +msgstr "" + msgid "ClusterIntegration|A service token scoped to %{code}kube-system%{end_code} with %{code}cluster-admin%{end_code} privileges." msgstr "" @@ -3507,6 +3510,9 @@ msgstr "" msgid "ClusterIntegration|Cluster health" msgstr "" +msgid "ClusterIntegration|Cluster management project (alpha)" +msgstr "" + msgid "ClusterIntegration|Cluster name is required." msgstr "" @@ -15587,6 +15593,9 @@ msgstr "" msgid "Something went wrong while fetching latest comments." msgstr "" +msgid "Something went wrong while fetching projects" +msgstr "" + msgid "Something went wrong while fetching related merge requests." msgstr "" diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb index e42d538fdf8..a0fe957f97e 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true module QA - context 'Create' do + # Failure issue: https://gitlab.com/gitlab-org/gitlab/issues/34551 + context 'Create', :quarantine do describe 'File templates' do include Runtime::Fixtures diff --git a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb index 0a89f0c9d41..957674353d6 100644 --- a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true module QA - context 'Create' do + # Failure issue: https://gitlab.com/gitlab-org/gitlab/issues/34551 + context 'Create', :quarantine do describe 'Web IDE file templates' do include Runtime::Fixtures diff --git a/spec/controllers/projects/serverless/functions_controller_spec.rb b/spec/controllers/projects/serverless/functions_controller_spec.rb index eccc8e1d5de..73fb0fad646 100644 --- a/spec/controllers/projects/serverless/functions_controller_spec.rb +++ b/spec/controllers/projects/serverless/functions_controller_spec.rb @@ -13,6 +13,10 @@ describe Projects::Serverless::FunctionsController do let(:environment) { create(:environment, project: project) } let!(:deployment) { create(:deployment, :success, environment: environment, cluster: cluster) } let(:knative_services_finder) { environment.knative_services_finder } + let(:function_description) { 'A serverless function' } + let(:knative_stub_options) do + { namespace: namespace.namespace, name: cluster.project.name, description: function_description } + end let(:namespace) do create(:cluster_kubernetes_namespace, @@ -114,40 +118,33 @@ describe Projects::Serverless::FunctionsController do expect(response).to have_gitlab_http_status(200) expect(json_response).to include( - "name" => project.name, - "url" => "http://#{project.name}.#{namespace.namespace}.example.com", - "podcount" => 1 + 'name' => project.name, + 'url' => "http://#{project.name}.#{namespace.namespace}.example.com", + 'description' => function_description, + 'podcount' => 1 ) end end - context 'on Knative 0.5' do + context 'on Knative 0.5.0' do + before do + prepare_knative_stubs(knative_05_service(knative_stub_options)) + end + + include_examples 'GET #show with valid data' + end + + context 'on Knative 0.6.0' do before do - stub_kubeclient_service_pods - stub_reactive_cache(knative_services_finder, - { - services: kube_knative_services_body( - legacy_knative: true, - namespace: namespace.namespace, - name: cluster.project.name - )["items"], - pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"] - }, - *knative_services_finder.cache_args) + prepare_knative_stubs(knative_06_service(knative_stub_options)) end include_examples 'GET #show with valid data' end - context 'on Knative 0.6 or 0.7' do + context 'on Knative 0.7.0' do before do - stub_kubeclient_service_pods - stub_reactive_cache(knative_services_finder, - { - services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"], - pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"] - }, - *knative_services_finder.cache_args) + prepare_knative_stubs(knative_07_service(knative_stub_options)) end include_examples 'GET #show with valid data' @@ -172,11 +169,12 @@ describe Projects::Serverless::FunctionsController do expect(response).to have_gitlab_http_status(200) expect(json_response).to match({ - "knative_installed" => "checking", - "functions" => [ + 'knative_installed' => 'checking', + 'functions' => [ a_hash_including( - "name" => project.name, - "url" => "http://#{project.name}.#{namespace.namespace}.example.com" + 'name' => project.name, + 'url' => "http://#{project.name}.#{namespace.namespace}.example.com", + 'description' => function_description ) ] }) @@ -189,36 +187,38 @@ describe Projects::Serverless::FunctionsController do end end - context 'on Knative 0.5' do + context 'on Knative 0.5.0' do before do - stub_kubeclient_service_pods - stub_reactive_cache(knative_services_finder, - { - services: kube_knative_services_body( - legacy_knative: true, - namespace: namespace.namespace, - name: cluster.project.name - )["items"], - pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"] - }, - *knative_services_finder.cache_args) + prepare_knative_stubs(knative_05_service(knative_stub_options)) end include_examples 'GET #index with data' end - context 'on Knative 0.6 or 0.7' do + context 'on Knative 0.6.0' do before do - stub_kubeclient_service_pods - stub_reactive_cache(knative_services_finder, - { - services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"], - pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"] - }, - *knative_services_finder.cache_args) + prepare_knative_stubs(knative_06_service(knative_stub_options)) end include_examples 'GET #index with data' end + + context 'on Knative 0.7.0' do + before do + prepare_knative_stubs(knative_07_service(knative_stub_options)) + end + + include_examples 'GET #index with data' + end + end + + def prepare_knative_stubs(knative_service) + stub_kubeclient_service_pods + stub_reactive_cache(knative_services_finder, + { + services: [knative_service], + pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"] + }, + *knative_services_finder.cache_args) end end diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js index 62ba0d36982..cef50bf553c 100644 --- a/spec/frontend/api_spec.js +++ b/spec/frontend/api_spec.js @@ -467,6 +467,26 @@ describe('Api', () => { }); }); + describe('user projects', () => { + it('fetches all projects that belong to a particular user', done => { + const query = 'dummy query'; + const options = { unused: 'option' }; + const userId = '123456'; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/users/${userId}/projects`; + mock.onGet(expectedUrl).reply(200, [ + { + name: 'test', + }, + ]); + + Api.userProjects(userId, query, options, response => { + expect(response.length).toBe(1); + expect(response[0].name).toBe('test'); + done(); + }); + }); + }); + describe('commitPipelines', () => { it('fetches pipelines for a given commit', done => { const projectId = 'example/foobar'; diff --git a/spec/frontend/clusters/clusters_bundle_spec.js b/spec/frontend/clusters/clusters_bundle_spec.js index 517d8781600..317d3f3012b 100644 --- a/spec/frontend/clusters/clusters_bundle_spec.js +++ b/spec/frontend/clusters/clusters_bundle_spec.js @@ -10,8 +10,10 @@ import axios from '~/lib/utils/axios_utils'; import { loadHTMLFixture } from 'helpers/fixtures'; import { setTestTimeout } from 'helpers/timeout'; import $ from 'jquery'; +import initProjectSelectDropdown from '~/project_select'; jest.mock('~/lib/utils/poll'); +jest.mock('~/project_select'); const { INSTALLING, INSTALLABLE, INSTALLED, UNINSTALLING } = APPLICATION_STATUS; @@ -44,6 +46,7 @@ describe('Clusters', () => { afterEach(() => { cluster.destroy(); mock.restore(); + jest.clearAllMocks(); }); describe('class constructor', () => { @@ -55,6 +58,10 @@ describe('Clusters', () => { it('should call initPolling on construct', () => { expect(cluster.initPolling).toHaveBeenCalled(); }); + + it('should call initProjectSelectDropdown on construct', () => { + expect(initProjectSelectDropdown).toHaveBeenCalled(); + }); }); describe('toggle', () => { diff --git a/spec/frontend/fixtures/static/environments_logs.html b/spec/frontend/fixtures/static/environments_logs.html index 4e242b77d1f..88bb0a3ed41 100644 --- a/spec/frontend/fixtures/static/environments_logs.html +++ b/spec/frontend/fixtures/static/environments_logs.html @@ -2,7 +2,8 @@ class="js-kubernetes-logs" data-current-environment-name="production" data-environments-path="/root/my-project/environments.json" - data-logs-endpoint="/root/my-project/environments/1/logs.json" + data-project-full-path="root/my-project" + data-environment-id=1 > <div class="build-page-pod-logs"> <div class="build-trace-container prepend-top-default"> diff --git a/spec/frontend/monitoring/embed/embed_spec.js b/spec/frontend/monitoring/embed/embed_spec.js index 5de1a7c4c3b..3e22b0858e6 100644 --- a/spec/frontend/monitoring/embed/embed_spec.js +++ b/spec/frontend/monitoring/embed/embed_spec.js @@ -61,8 +61,8 @@ describe('Embed', () => { describe('metrics are available', () => { beforeEach(() => { - store.state.monitoringDashboard.groups = groups; - store.state.monitoringDashboard.groups[0].metrics = metricsData; + store.state.monitoringDashboard.dashboard.panel_groups = groups; + store.state.monitoringDashboard.dashboard.panel_groups[0].metrics = metricsData; store.state.monitoringDashboard.metricsWithData = metricsWithData; mountComponent(); diff --git a/spec/frontend/monitoring/embed/mock_data.js b/spec/frontend/monitoring/embed/mock_data.js index df4acb82e95..1685021fd4b 100644 --- a/spec/frontend/monitoring/embed/mock_data.js +++ b/spec/frontend/monitoring/embed/mock_data.js @@ -81,7 +81,9 @@ export const metricsData = [ export const initialState = { monitoringDashboard: {}, - groups: [], + dashboard: { + panel_groups: [], + }, metricsWithData: [], useDashboardEndpoint: true, }; diff --git a/spec/graphql/mutations/merge_requests/set_milestone_spec.rb b/spec/graphql/mutations/merge_requests/set_milestone_spec.rb new file mode 100644 index 00000000000..c2792a4bc25 --- /dev/null +++ b/spec/graphql/mutations/merge_requests/set_milestone_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Mutations::MergeRequests::SetMilestone do + let(:merge_request) { create(:merge_request) } + let(:user) { create(:user) } + subject(:mutation) { described_class.new(object: nil, context: { current_user: user }) } + + describe '#resolve' do + let(:milestone) { create(:milestone, project: merge_request.project) } + let(:mutated_merge_request) { subject[:merge_request] } + subject { mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, milestone: milestone) } + + it 'raises an error if the resource is not accessible to the user' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + + context 'when the user can update the merge request' do + before do + merge_request.project.add_developer(user) + end + + it 'returns the merge request with the milestone' do + expect(mutated_merge_request).to eq(merge_request) + expect(mutated_merge_request.milestone).to eq(milestone) + expect(subject[:errors]).to be_empty + end + + it 'returns errors merge request could not be updated' do + # Make the merge request invalid + merge_request.allow_broken = true + merge_request.update!(source_project: nil) + + expect(subject[:errors]).not_to be_empty + end + + context 'when passing milestone_id as nil' do + let(:milestone) { nil } + + it 'removes the milestone' do + merge_request.update!(milestone: create(:milestone, project: merge_request.project)) + + expect(mutated_merge_request.milestone).to eq(nil) + end + + it 'does not do anything if the MR already does not have a milestone' do + expect(mutated_merge_request.milestone).to eq(nil) + end + end + end + end +end diff --git a/spec/javascripts/monitoring/charts/time_series_spec.js b/spec/javascripts/monitoring/charts/time_series_spec.js index 31ea9ede9de..42feaca616b 100644 --- a/spec/javascripts/monitoring/charts/time_series_spec.js +++ b/spec/javascripts/monitoring/charts/time_series_spec.js @@ -23,7 +23,7 @@ describe('Time series component', () => { store = createStore(); store.commit(`monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, MonitoringMock.data); store.commit(`monitoringDashboard/${types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS}`, deploymentData); - [mockGraphData] = store.state.monitoringDashboard.groups[0].metrics; + [, mockGraphData] = store.state.monitoringDashboard.dashboard.panel_groups[0].metrics; makeTimeSeriesChart = (graphData, type) => shallowMount(TimeSeries, { diff --git a/spec/javascripts/monitoring/components/dashboard_spec.js b/spec/javascripts/monitoring/components/dashboard_spec.js index be7ab24185d..4070de4cc35 100644 --- a/spec/javascripts/monitoring/components/dashboard_spec.js +++ b/spec/javascripts/monitoring/components/dashboard_spec.js @@ -442,6 +442,28 @@ describe('Dashboard', () => { expect(findEnabledDraggables()).toEqual(findDraggables()); }); + it('metrics can be swapped', done => { + const firstDraggable = findDraggables().at(0); + const mockMetrics = [...metricsGroupsAPIResponse.data[0].metrics]; + const value = () => firstDraggable.props('value'); + + expect(value().length).toBe(mockMetrics.length); + value().forEach((metric, i) => { + expect(metric.title).toBe(mockMetrics[i].title); + }); + + // swap two elements and `input` them + [mockMetrics[0], mockMetrics[1]] = [mockMetrics[1], mockMetrics[0]]; + firstDraggable.vm.$emit('input', mockMetrics); + + firstDraggable.vm.$nextTick(() => { + value().forEach((metric, i) => { + expect(metric.title).toBe(mockMetrics[i].title); + }); + done(); + }); + }); + it('shows a remove button, which removes a panel', done => { expect(findFirstDraggableRemoveButton().isEmpty()).toBe(false); @@ -449,8 +471,6 @@ describe('Dashboard', () => { findFirstDraggableRemoveButton().trigger('click'); wrapper.vm.$nextTick(() => { - // At present graphs will not be removed in backend - // See https://gitlab.com/gitlab-org/gitlab/issues/27835 expect(findDraggablePanels().length).toEqual(expectedPanelCount - 1); done(); }); @@ -686,7 +706,9 @@ describe('Dashboard', () => { `monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, MonitoringMock.data, ); - [mockGraphData] = component.$store.state.monitoringDashboard.groups[0].metrics; + [ + mockGraphData, + ] = component.$store.state.monitoringDashboard.dashboard.panel_groups[0].metrics; }); describe('csvText', () => { diff --git a/spec/javascripts/monitoring/store/actions_spec.js b/spec/javascripts/monitoring/store/actions_spec.js index 1bd74f59282..4d602e3b0a2 100644 --- a/spec/javascripts/monitoring/store/actions_spec.js +++ b/spec/javascripts/monitoring/store/actions_spec.js @@ -291,9 +291,9 @@ describe('Monitoring store actions', () => { it('dispatches fetchPrometheusMetric for each panel query', done => { const params = {}; const state = storeState(); - state.groups = metricsDashboardResponse.dashboard.panel_groups; + state.dashboard.panel_groups = metricsDashboardResponse.dashboard.panel_groups; - const metric = state.groups[0].panels[0].metrics[0]; + const metric = state.dashboard.panel_groups[0].panels[0].metrics[0]; fetchPrometheusMetrics({ state, commit, dispatch }, params) .then(() => { diff --git a/spec/javascripts/monitoring/store/mutations_spec.js b/spec/javascripts/monitoring/store/mutations_spec.js index bdddd83358c..49aed1b85e6 100644 --- a/spec/javascripts/monitoring/store/mutations_spec.js +++ b/spec/javascripts/monitoring/store/mutations_spec.js @@ -20,16 +20,26 @@ describe('Monitoring mutations', () => { let groups; beforeEach(() => { - stateCopy.groups = []; + stateCopy.dashboard.panel_groups = []; groups = metricsGroupsAPIResponse.data; }); + it('adds a key to the group', () => { + mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, groups); + + expect(stateCopy.dashboard.panel_groups[0].key).toBe('kubernetes-0'); + expect(stateCopy.dashboard.panel_groups[1].key).toBe('nginx-1'); + }); + it('normalizes values', () => { mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, groups); const expectedTimestamp = '2017-05-25T08:22:34.925Z'; - const expectedValue = 0.0010794445585559514; - const [timestamp, value] = stateCopy.groups[0].metrics[0].queries[0].result[0].values[0]; + const expectedValue = 8.0390625; + const [ + timestamp, + value, + ] = stateCopy.dashboard.panel_groups[0].metrics[0].queries[0].result[0].values[0]; expect(timestamp).toEqual(expectedTimestamp); expect(value).toEqual(expectedValue); @@ -38,25 +48,25 @@ describe('Monitoring mutations', () => { it('contains two groups that contains, one of which has two queries sorted by priority', () => { mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, groups); - expect(stateCopy.groups).toBeDefined(); - expect(stateCopy.groups.length).toEqual(2); - expect(stateCopy.groups[0].metrics.length).toEqual(2); + expect(stateCopy.dashboard.panel_groups).toBeDefined(); + expect(stateCopy.dashboard.panel_groups.length).toEqual(2); + expect(stateCopy.dashboard.panel_groups[0].metrics.length).toEqual(2); }); it('assigns queries a metric id', () => { mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, groups); - expect(stateCopy.groups[1].metrics[0].queries[0].metricId).toEqual('100'); + expect(stateCopy.dashboard.panel_groups[1].metrics[0].queries[0].metricId).toEqual('100'); }); it('removes the data if all the values from a query are not defined', () => { mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, groups); - expect(stateCopy.groups[1].metrics[0].queries[0].result.length).toEqual(0); + expect(stateCopy.dashboard.panel_groups[1].metrics[0].queries[0].result.length).toEqual(0); }); it('assigns metric id of null if metric has no id', () => { - stateCopy.groups = []; + stateCopy.dashboard.panel_groups = []; const noId = groups.map(group => ({ ...group, ...{ @@ -70,7 +80,7 @@ describe('Monitoring mutations', () => { mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, noId); - stateCopy.groups.forEach(group => { + stateCopy.dashboard.panel_groups.forEach(group => { group.metrics.forEach(metric => { expect(metric.queries.every(query => query.metricId === null)).toBe(true); }); @@ -87,13 +97,13 @@ describe('Monitoring mutations', () => { it('aliases group panels to metrics for backwards compatibility', () => { mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, dashboardGroups); - expect(stateCopy.groups[0].metrics[0]).toBeDefined(); + expect(stateCopy.dashboard.panel_groups[0].metrics[0]).toBeDefined(); }); it('aliases panel metrics to queries for backwards compatibility', () => { mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, dashboardGroups); - expect(stateCopy.groups[0].metrics[0].queries).toBeDefined(); + expect(stateCopy.dashboard.panel_groups[0].metrics[0].queries).toBeDefined(); }); }); }); diff --git a/spec/lib/gitlab/prometheus/internal_spec.rb b/spec/lib/gitlab/prometheus/internal_spec.rb new file mode 100644 index 00000000000..884bdcb4e9b --- /dev/null +++ b/spec/lib/gitlab/prometheus/internal_spec.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Prometheus::Internal do + let(:listen_address) { 'localhost:9090' } + + let(:prometheus_settings) do + { + enable: true, + listen_address: listen_address + } + end + + before do + stub_config(prometheus: prometheus_settings) + end + + describe '.uri' do + shared_examples 'returns valid uri' do |uri_string| + it do + expect(described_class.uri).to eq(uri_string) + expect { Addressable::URI.parse(described_class.uri) }.not_to raise_error + end + end + + it_behaves_like 'returns valid uri', 'http://localhost:9090' + + context 'with non default prometheus address' do + let(:listen_address) { 'https://localhost:9090' } + + it_behaves_like 'returns valid uri', 'https://localhost:9090' + + context 'with :9090 symbol' do + let(:listen_address) { :':9090' } + + it_behaves_like 'returns valid uri', 'http://localhost:9090' + end + + context 'with 0.0.0.0:9090' do + let(:listen_address) { '0.0.0.0:9090' } + + it_behaves_like 'returns valid uri', 'http://localhost:9090' + end + end + + context 'when listen_address is nil' do + let(:listen_address) { nil } + + it 'does not fail' do + expect(described_class.uri).to eq(nil) + end + end + + context 'when prometheus listen address is blank in gitlab.yml' do + let(:listen_address) { '' } + + it 'does not configure prometheus' do + expect(described_class.uri).to eq(nil) + end + end + end + + describe 'prometheus_enabled?' do + it 'returns correct value' do + expect(described_class.prometheus_enabled?).to eq(true) + end + + context 'when prometheus setting is disabled in gitlab.yml' do + let(:prometheus_settings) do + { + enable: false, + listen_address: listen_address + } + end + + it 'returns correct value' do + expect(described_class.prometheus_enabled?).to eq(false) + end + end + + context 'when prometheus setting is not present in gitlab.yml' do + before do + allow(Gitlab.config).to receive(:prometheus).and_raise(Settingslogic::MissingSetting) + end + + it 'does not fail' do + expect(described_class.prometheus_enabled?).to eq(false) + end + end + end + + describe '.listen_address' do + it 'returns correct value' do + expect(described_class.listen_address).to eq(listen_address) + end + + context 'when prometheus setting is not present in gitlab.yml' do + before do + allow(Gitlab.config).to receive(:prometheus).and_raise(Settingslogic::MissingSetting) + end + + it 'does not fail' do + expect(described_class.listen_address).to eq(nil) + end + end + end +end diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_milestone_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_milestone_spec.rb new file mode 100644 index 00000000000..bd558edf9c5 --- /dev/null +++ b/spec/requests/api/graphql/mutations/merge_requests/set_milestone_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Setting milestone of a merge request' do + include GraphqlHelpers + + let(:current_user) { create(:user) } + let(:merge_request) { create(:merge_request) } + let(:project) { merge_request.project } + let(:milestone) { create(:milestone, project: project) } + let(:input) { { milestone_id: GitlabSchema.id_from_object(milestone).to_s } } + + let(:mutation) do + variables = { + project_path: project.full_path, + iid: merge_request.iid.to_s + } + graphql_mutation(:merge_request_set_milestone, variables.merge(input), + <<-QL.strip_heredoc + clientMutationId + errors + mergeRequest { + id + milestone { + id + } + } + QL + ) + end + + def mutation_response + graphql_mutation_response(:merge_request_set_milestone) + end + + before do + project.add_developer(current_user) + end + + it 'returns an error if the user is not allowed to update the merge request' do + post_graphql_mutation(mutation, current_user: create(:user)) + + expect(graphql_errors).not_to be_empty + end + + it 'sets the merge request milestone' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['mergeRequest']['milestone']['id']).to eq(milestone.to_global_id.to_s) + end + + context 'when passing milestone_id nil as input' do + let(:input) { { milestone_id: nil } } + + it 'removes the merge request milestone' do + merge_request.update!(milestone: milestone) + + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['mergeRequest']['milestone']).to be_nil + end + end +end diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb index e74dbca4f93..199156c53d7 100644 --- a/spec/support/helpers/kubernetes_helpers.rb +++ b/spec/support/helpers/kubernetes_helpers.rb @@ -319,10 +319,10 @@ module KubernetesHelpers } end - def kube_knative_services_body(legacy_knative: false, **options) + def kube_knative_services_body(**options) { "kind" => "List", - "items" => [legacy_knative ? knative_05_service(options) : kube_service(options)] + "items" => [knative_07_service(options)] } end @@ -398,77 +398,171 @@ module KubernetesHelpers } end - def kube_service(name: "kubetest", namespace: "default", domain: "example.com") - { - "metadata" => { - "creationTimestamp" => "2018-11-21T06:16:33Z", - "name" => name, - "namespace" => namespace, - "selfLink" => "/apis/serving.knative.dev/v1alpha1/namespaces/#{namespace}/services/#{name}" - }, + # noinspection RubyStringKeysInHashInspection + def knative_06_service(name: 'kubetest', namespace: 'default', domain: 'example.com', description: 'a knative service', environment: 'production') + { "apiVersion" => "serving.knative.dev/v1alpha1", + "kind" => "Service", + "metadata" => + { "annotations" => + { "serving.knative.dev/creator" => "system:serviceaccount:#{namespace}:#{namespace}-service-account", + "serving.knative.dev/lastModifier" => "system:serviceaccount:#{namespace}:#{namespace}-service-account" }, + "creationTimestamp" => "2019-10-22T21:19:20Z", + "generation" => 1, + "labels" => { "service" => name }, + "name" => name, + "namespace" => namespace, + "resourceVersion" => "6042", + "selfLink" => "/apis/serving.knative.dev/v1alpha1/namespaces/#{namespace}/services/#{name}", + "uid" => "9c7f63d0-f511-11e9-8815-42010a80002f" }, "spec" => { - "generation" => 2 + "runLatest" => { + "configuration" => { + "revisionTemplate" => { + "metadata" => { + "annotations" => { "Description" => description }, + "creationTimestamp" => "2019-10-22T21:19:20Z", + "labels" => { "service" => name } + }, + "spec" => { + "container" => { + "env" => [{ "name" => "timestamp", "value" => "2019-10-22 21:19:20" }], + "image" => "image_name", + "name" => "", + "resources" => {} + }, + "timeoutSeconds" => 300 + } + } + } + } }, "status" => { - "url" => "http://#{name}.#{namespace}.#{domain}", "address" => { - "url" => "#{name}.#{namespace}.svc.cluster.local" + "hostname" => "#{name}.#{namespace}.svc.cluster.local", + "url" => "http://#{name}.#{namespace}.svc.cluster.local" }, - "latestCreatedRevisionName" => "#{name}-00002", - "latestReadyRevisionName" => "#{name}-00002", - "observedGeneration" => 2 - } - } - end - - def knative_05_service(name: "kubetest", namespace: "default", domain: "example.com") - { - "metadata" => { - "creationTimestamp" => "2018-11-21T06:16:33Z", - "name" => name, - "namespace" => namespace, - "selfLink" => "/apis/serving.knative.dev/v1alpha1/namespaces/#{namespace}/services/#{name}" - }, - "spec" => { - "generation" => 2 - }, - "status" => { + "conditions" => + [{ "lastTransitionTime" => "2019-10-22T21:20:25Z", "status" => "True", "type" => "ConfigurationsReady" }, + { "lastTransitionTime" => "2019-10-22T21:20:25Z", "status" => "True", "type" => "Ready" }, + { "lastTransitionTime" => "2019-10-22T21:20:25Z", "status" => "True", "type" => "RoutesReady" }], "domain" => "#{name}.#{namespace}.#{domain}", "domainInternal" => "#{name}.#{namespace}.svc.cluster.local", - "latestCreatedRevisionName" => "#{name}-00002", - "latestReadyRevisionName" => "#{name}-00002", - "observedGeneration" => 2 - } - } - end - - def kube_service_full(name: "kubetest", namespace: "kube-ns", domain: "example.com") - { - "metadata" => { - "creationTimestamp" => "2018-11-21T06:16:33Z", - "name" => name, - "namespace" => namespace, - "selfLink" => "/apis/serving.knative.dev/v1alpha1/namespaces/#{namespace}/services/#{name}", - "annotation" => { - "description" => "This is a test description" - } + "latestCreatedRevisionName" => "#{name}-bskx6", + "latestReadyRevisionName" => "#{name}-bskx6", + "observedGeneration" => 1, + "traffic" => [{ "latestRevision" => true, "percent" => 100, "revisionName" => "#{name}-bskx6" }], + "url" => "http://#{name}.#{namespace}.#{domain}" }, + "environment_scope" => environment, + "cluster_id" => 9, + "podcount" => 0 } + end + + # noinspection RubyStringKeysInHashInspection + def knative_07_service(name: 'kubetest', namespace: 'default', domain: 'example.com', description: 'a knative service', environment: 'production') + { "apiVersion" => "serving.knative.dev/v1alpha1", + "kind" => "Service", + "metadata" => + { "annotations" => + { "serving.knative.dev/creator" => "system:serviceaccount:#{namespace}:#{namespace}-service-account", + "serving.knative.dev/lastModifier" => "system:serviceaccount:#{namespace}:#{namespace}-service-account" }, + "creationTimestamp" => "2019-10-22T21:19:13Z", + "generation" => 1, + "labels" => { "service" => name }, + "name" => name, + "namespace" => namespace, + "resourceVersion" => "289726", + "selfLink" => "/apis/serving.knative.dev/v1alpha1/namespaces/#{namespace}/services/#{name}", + "uid" => "988349fa-f511-11e9-9ea1-42010a80005e" }, "spec" => { - "generation" => 2, - "build" => { - "template" => "go-1.10.3" + "template" => { + "metadata" => { + "annotations" => { "Description" => description }, + "creationTimestamp" => "2019-10-22T21:19:12Z", + "labels" => { "service" => name } + }, + "spec" => { + "containers" => [{ + "env" => + [{ "name" => "timestamp", "value" => "2019-10-22 21:19:12" }], + "image" => "image_name", + "name" => "user-container", + "resources" => {} + }], + "timeoutSeconds" => 300 + } + }, + "traffic" => [{ "latestRevision" => true, "percent" => 100 }] + }, + "status" => + { "address" => { "url" => "http://#{name}.#{namespace}.svc.cluster.local" }, + "conditions" => + [{ "lastTransitionTime" => "2019-10-22T21:20:15Z", "status" => "True", "type" => "ConfigurationsReady" }, + { "lastTransitionTime" => "2019-10-22T21:20:15Z", "status" => "True", "type" => "Ready" }, + { "lastTransitionTime" => "2019-10-22T21:20:15Z", "status" => "True", "type" => "RoutesReady" }], + "latestCreatedRevisionName" => "#{name}-92tsj", + "latestReadyRevisionName" => "#{name}-92tsj", + "observedGeneration" => 1, + "traffic" => [{ "latestRevision" => true, "percent" => 100, "revisionName" => "#{name}-92tsj" }], + "url" => "http://#{name}.#{namespace}.#{domain}" }, + "environment_scope" => environment, + "cluster_id" => 5, + "podcount" => 0 } + end + + # noinspection RubyStringKeysInHashInspection + def knative_05_service(name: 'kubetest', namespace: 'default', domain: 'example.com', description: 'a knative service', environment: 'production') + { "apiVersion" => "serving.knative.dev/v1alpha1", + "kind" => "Service", + "metadata" => + { "annotations" => + { "serving.knative.dev/creator" => "system:serviceaccount:#{namespace}:#{namespace}-service-account", + "serving.knative.dev/lastModifier" => "system:serviceaccount:#{namespace}:#{namespace}-service-account" }, + "creationTimestamp" => "2019-10-22T21:19:19Z", + "generation" => 1, + "labels" => { "service" => name }, + "name" => name, + "namespace" => namespace, + "resourceVersion" => "330390", + "selfLink" => "/apis/serving.knative.dev/v1alpha1/namespaces/#{namespace}/services/#{name}", + "uid" => "9c710da6-f511-11e9-9ba0-42010a800161" }, + "spec" => { + "runLatest" => { + "configuration" => { + "revisionTemplate" => { + "metadata" => { + "annotations" => { "Description" => description }, + "creationTimestamp" => "2019-10-22T21:19:19Z", + "labels" => { "service" => name } + }, + "spec" => { + "container" => { + "env" => [{ "name" => "timestamp", "value" => "2019-10-22 21:19:19" }], + "image" => "image_name", + "name" => "", + "resources" => { "requests" => { "cpu" => "400m" } } + }, + "timeoutSeconds" => 300 + } + } + } } }, - "status" => { - "url" => "http://#{name}.#{namespace}.#{domain}", - "address" => { - "url" => "#{name}.#{namespace}.svc.cluster.local" - }, - "latestCreatedRevisionName" => "#{name}-00002", - "latestReadyRevisionName" => "#{name}-00002", - "observedGeneration" => 2 - } - } + "status" => + { "address" => { "hostname" => "#{name}.#{namespace}.svc.cluster.local" }, + "conditions" => + [{ "lastTransitionTime" => "2019-10-22T21:20:24Z", "status" => "True", "type" => "ConfigurationsReady" }, + { "lastTransitionTime" => "2019-10-22T21:20:24Z", "status" => "True", "type" => "Ready" }, + { "lastTransitionTime" => "2019-10-22T21:20:24Z", "status" => "True", "type" => "RoutesReady" }], + "domain" => "#{name}.#{namespace}.#{domain}", + "domainInternal" => "#{name}.#{namespace}.svc.cluster.local", + "latestCreatedRevisionName" => "#{name}-58qgr", + "latestReadyRevisionName" => "#{name}-58qgr", + "observedGeneration" => 1, + "traffic" => [{ "percent" => 100, "revisionName" => "#{name}-58qgr" }] }, + "environment_scope" => environment, + "cluster_id" => 8, + "podcount" => 0 } end def kube_terminals(service, pod) |