Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-03-18 03:09:43 +00:00
parent 2e31c85a97
commit b4b9b3854e
44 changed files with 1009 additions and 291 deletions

View file

@ -5,15 +5,17 @@ import {
GlSprintf,
GlAlert,
GlDropdown,
GlDropdownDivider,
GlDropdownItem,
GlFormGroup,
GlSearchBoxByClick,
GlInfiniteScroll,
} from '@gitlab/ui';
import { s__ } from '~/locale';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
import LogControlButtons from './log_control_buttons.vue';
import { timeRanges, defaultTimeRange } from '~/monitoring/constants';
import { timeRanges, defaultTimeRange } from '~/vue_shared/constants';
import { timeRangeFromUrl } from '~/monitoring/utils';
import { formatDate } from '../utils';
@ -22,6 +24,7 @@ export default {
GlSprintf,
GlAlert,
GlDropdown,
GlDropdownDivider,
GlDropdownItem,
GlFormGroup,
GlSearchBoxByClick,
@ -90,6 +93,16 @@ export default {
shouldShowElasticStackCallout() {
return !this.isElasticStackCalloutDismissed && this.disableAdvancedControls;
},
podDropdownText() {
if (this.pods.current) {
return this.pods.current;
} else if (this.advancedFeaturesEnabled) {
// "All pods" is a valid option when advanced querying is available
return s__('Environments|All pods');
}
return s__('Environments|No pod selected');
},
},
mounted() {
this.setInitData({
@ -178,11 +191,17 @@ export default {
>
<gl-dropdown
id="pods-dropdown"
:text="pods.current || s__('Environments|No pods to display')"
:text="podDropdownText"
:disabled="environments.isLoading"
class="d-flex gl-h-32 js-pods-dropdown"
toggle-class="dropdown-menu-toggle"
>
<template v-if="advancedFeaturesEnabled">
<gl-dropdown-item key="all-pods" @click="showPodLogs(null)">
{{ s__('Environments|All pods') }}
</gl-dropdown-item>
<gl-dropdown-divider />
</template>
<gl-dropdown-item
v-for="podName in pods.options"
:key="podName"

View file

@ -82,7 +82,6 @@ export const setTimeRange = ({ dispatch, commit }, timeRange) => {
export const showEnvironment = ({ dispatch, commit }, environmentName) => {
commit(types.SET_PROJECT_ENVIRONMENT, environmentName);
commit(types.SET_CURRENT_POD_NAME, null);
dispatch('fetchLogs');
};
@ -107,16 +106,16 @@ export const fetchEnvironments = ({ commit, dispatch }, environmentsPath) => {
};
export const fetchLogs = ({ commit, state }) => {
commit(types.REQUEST_PODS_DATA);
commit(types.REQUEST_LOGS_DATA);
return requestLogsUntilData(state)
.then(({ data }) => {
const { pod_name, pods, logs, cursor } = data;
commit(types.RECEIVE_LOGS_DATA_SUCCESS, { logs, cursor });
commit(types.SET_CURRENT_POD_NAME, pod_name);
commit(types.RECEIVE_PODS_DATA_SUCCESS, pods);
commit(types.RECEIVE_LOGS_DATA_SUCCESS, { logs, cursor });
})
.catch(() => {
commit(types.RECEIVE_PODS_DATA_ERROR);

View file

@ -1,7 +1,7 @@
import { formatDate } from '../utils';
const mapTrace = ({ timestamp = null, message = '' }) =>
[timestamp ? formatDate(timestamp) : '', message].join(' | ');
const mapTrace = ({ timestamp = null, pod = '', message = '' }) =>
[timestamp ? formatDate(timestamp) : '', pod, message].join(' | ');
export const trace = state => state.logs.lines.map(mapTrace).join('\n');

View file

@ -14,6 +14,5 @@ export const REQUEST_LOGS_DATA_PREPEND = 'REQUEST_LOGS_DATA_PREPEND';
export const RECEIVE_LOGS_DATA_PREPEND_SUCCESS = 'RECEIVE_LOGS_DATA_PREPEND_SUCCESS';
export const RECEIVE_LOGS_DATA_PREPEND_ERROR = 'RECEIVE_LOGS_DATA_PREPEND_ERROR';
export const REQUEST_PODS_DATA = 'REQUEST_PODS_DATA';
export const RECEIVE_PODS_DATA_SUCCESS = 'RECEIVE_PODS_DATA_SUCCESS';
export const RECEIVE_PODS_DATA_ERROR = 'RECEIVE_PODS_DATA_ERROR';

View file

@ -1,8 +1,9 @@
import * as types from './mutation_types';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
const mapLine = ({ timestamp, message }) => ({
const mapLine = ({ timestamp, pod, message }) => ({
timestamp,
pod,
message,
});
@ -21,6 +22,10 @@ export default {
// Environments Data
[types.SET_PROJECT_ENVIRONMENT](state, environmentName) {
state.environments.current = environmentName;
// Clear current pod options
state.pods.current = null;
state.pods.options = [];
},
[types.REQUEST_ENVIRONMENTS_DATA](state) {
state.environments.options = [];
@ -81,9 +86,6 @@ export default {
[types.SET_CURRENT_POD_NAME](state, podName) {
state.pods.current = podName;
},
[types.REQUEST_PODS_DATA](state) {
state.pods.options = [];
},
[types.RECEIVE_PODS_DATA_SUCCESS](state, podOptions) {
state.pods.options = podOptions;
},

View file

@ -1,4 +1,4 @@
import { timeRanges, defaultTimeRange } from '~/monitoring/constants';
import { timeRanges, defaultTimeRange } from '~/vue_shared/constants';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
export default () => ({

View file

@ -31,7 +31,8 @@ import DashboardsDropdown from './dashboards_dropdown.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import { getAddMetricTrackingOptions, timeRangeToUrl, timeRangeFromUrl } from '../utils';
import { defaultTimeRange, timeRanges, metricStates } from '../constants';
import { metricStates } from '../constants';
import { defaultTimeRange, timeRanges } from '~/vue_shared/constants';
export default {
components: {

View file

@ -3,7 +3,8 @@ import { mapActions, mapState, mapGetters } from 'vuex';
import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
import { timeRangeFromUrl, removeTimeRangeParams } from '../utils';
import { sidebarAnimationDuration, defaultTimeRange } from '../constants';
import { sidebarAnimationDuration } from '../constants';
import { defaultTimeRange } from '~/vue_shared/constants';
let sidebarMutationObserver;

View file

@ -1,5 +1,3 @@
import { __ } from '~/locale';
export const PROMETHEUS_TIMEOUT = 120000; // TWO_MINUTES
/**
@ -89,37 +87,3 @@ export const dateFormats = {
timeOfDay: 'h:MM TT',
default: 'dd mmm yyyy, h:MMTT',
};
export const timeRanges = [
{
label: __('30 minutes'),
duration: { seconds: 60 * 30 },
},
{
label: __('3 hours'),
duration: { seconds: 60 * 60 * 3 },
},
{
label: __('8 hours'),
duration: { seconds: 60 * 60 * 8 },
default: true,
},
{
label: __('1 day'),
duration: { seconds: 60 * 60 * 24 * 1 },
},
{
label: __('3 days'),
duration: { seconds: 60 * 60 * 24 * 3 },
},
{
label: __('1 week'),
duration: { seconds: 60 * 60 * 24 * 7 * 1 },
},
{
label: __('1 month'),
duration: { seconds: 60 * 60 * 24 * 30 },
},
];
export const defaultTimeRange = timeRanges.find(tr => tr.default);

View file

@ -43,6 +43,11 @@ export default {
required: false,
default: () => defaultTimeRanges,
},
customEnabled: {
type: Boolean,
required: false,
default: true,
},
},
data() {
return {
@ -166,6 +171,7 @@ export default {
>
<div class="d-flex justify-content-between gl-p-2">
<gl-form-group
v-if="customEnabled"
:label="__('Custom range')"
label-for="custom-from-time"
label-class="gl-pb-1"

View file

@ -0,0 +1,56 @@
import { __ } from '~/locale';
const INTERVALS = {
minute: 'minute',
hour: 'hour',
day: 'day',
};
export const timeRanges = [
{
label: __('30 minutes'),
duration: { seconds: 60 * 30 },
name: 'thirtyMinutes',
interval: INTERVALS.minute,
},
{
label: __('3 hours'),
duration: { seconds: 60 * 60 * 3 },
name: 'threeHours',
interval: INTERVALS.hour,
},
{
label: __('8 hours'),
duration: { seconds: 60 * 60 * 8 },
name: 'eightHours',
default: true,
interval: INTERVALS.hour,
},
{
label: __('1 day'),
duration: { seconds: 60 * 60 * 24 * 1 },
name: 'oneDay',
interval: INTERVALS.hour,
},
{
label: __('3 days'),
duration: { seconds: 60 * 60 * 24 * 3 },
name: 'threeDays',
interval: INTERVALS.hour,
},
{
label: __('1 week'),
duration: { seconds: 60 * 60 * 24 * 7 * 1 },
name: 'oneWeek',
interval: INTERVALS.day,
},
{
label: __('1 month'),
duration: { seconds: 60 * 60 * 24 * 30 },
name: 'oneMonth',
interval: INTERVALS.day,
},
];
export const defaultTimeRange = timeRanges.find(tr => tr.default);
export const getTimeWindow = timeWindowName => timeRanges.find(tr => tr.name === timeWindowName);

View file

@ -55,24 +55,12 @@ module PodLogs
return error(_('Cluster does not exist')) if cluster.nil?
return error(_('Namespace is empty')) if namespace.blank?
result[:pod_name] = params['pod_name'].presence
result[:container_name] = params['container_name'].presence
success(result)
end
def check_param_lengths(_result)
pod_name = params['pod_name'].presence
container_name = params['container_name'].presence
if pod_name&.length.to_i > K8S_NAME_MAX_LENGTH
return error(_('pod_name cannot be larger than %{max_length}'\
' chars' % { max_length: K8S_NAME_MAX_LENGTH }))
elsif container_name&.length.to_i > K8S_NAME_MAX_LENGTH
return error(_('container_name cannot be larger than'\
' %{max_length} chars' % { max_length: K8S_NAME_MAX_LENGTH }))
end
success(pod_name: pod_name, container_name: container_name)
end
def get_raw_pods(result)
result[:raw_pods] = cluster.kubeclient.get_pods(namespace: namespace)
@ -85,40 +73,6 @@ module PodLogs
success(result)
end
def check_pod_name(result)
# If pod_name is not received as parameter, get the pod logs of the first
# pod of this namespace.
result[:pod_name] ||= result[:pods].first
unless result[:pod_name]
return error(_('No pods available'))
end
unless result[:pods].include?(result[:pod_name])
return error(_('Pod does not exist'))
end
success(result)
end
def check_container_name(result)
pod_details = result[:raw_pods].first { |p| p.metadata.name == result[:pod_name] }
containers = pod_details.spec.containers.map(&:name)
# select first container if not specified
result[:container_name] ||= containers.first
unless result[:container_name]
return error(_('No containers available'))
end
unless containers.include?(result[:container_name])
return error(_('Container does not exist'))
end
success(result)
end
def pod_logs(result)
raise NotImplementedError
end

View file

@ -3,11 +3,8 @@
module PodLogs
class ElasticsearchService < PodLogs::BaseService
steps :check_arguments,
:check_param_lengths,
:get_raw_pods,
:get_pod_names,
:check_pod_name,
:check_container_name,
:check_times,
:check_search,
:check_cursor,
@ -53,7 +50,7 @@ module PodLogs
response = ::Gitlab::Elasticsearch::Logs.new(client).pod_logs(
namespace,
result[:pod_name],
pod_name: result[:pod_name],
container_name: result[:container_name],
search: result[:search],
start_time: result[:start],

View file

@ -8,7 +8,6 @@ module PodLogs
EncodingHelperError = Class.new(StandardError)
steps :check_arguments,
:check_param_lengths,
:get_raw_pods,
:get_pod_names,
:check_pod_name,
@ -22,6 +21,50 @@ module PodLogs
private
def check_pod_name(result)
# If pod_name is not received as parameter, get the pod logs of the first
# pod of this namespace.
result[:pod_name] ||= result[:pods].first
unless result[:pod_name]
return error(_('No pods available'))
end
unless result[:pod_name].length.to_i <= K8S_NAME_MAX_LENGTH
return error(_('pod_name cannot be larger than %{max_length}'\
' chars' % { max_length: K8S_NAME_MAX_LENGTH }))
end
unless result[:pods].include?(result[:pod_name])
return error(_('Pod does not exist'))
end
success(result)
end
def check_container_name(result)
pod_details = result[:raw_pods].first { |p| p.metadata.name == result[:pod_name] }
containers = pod_details.spec.containers.map(&:name)
# select first container if not specified
result[:container_name] ||= containers.first
unless result[:container_name]
return error(_('No containers available'))
end
unless result[:container_name].length.to_i <= K8S_NAME_MAX_LENGTH
return error(_('container_name cannot be larger than'\
' %{max_length} chars' % { max_length: K8S_NAME_MAX_LENGTH }))
end
unless containers.include?(result[:container_name])
return error(_('Container does not exist'))
end
success(result)
end
def pod_logs(result)
result[:logs] = cluster.kubeclient.get_pod_log(
result[:pod_name],
@ -62,7 +105,8 @@ module PodLogs
values = line.split(' ', 2)
{
timestamp: values[0],
message: values[1]
message: values[1],
pod: result[:pod_name]
}
end

View file

@ -0,0 +1,5 @@
---
title: Add all pods view to logs explorer
merge_request: 26883
author:
type: added

View file

@ -0,0 +1,5 @@
---
title: Adds crossplane as CI/CD Managed App
merge_request: 27374
author:
type: added

View file

@ -6000,6 +6000,76 @@ type Project {
"""
requestAccessEnabled: Boolean
"""
Find a single requirement. Available only when feature flag `requirements_management` is enabled.
"""
requirement(
"""
IID of the requirement, e.g., "1"
"""
iid: ID
"""
List of IIDs of requirements, e.g., [1, 2]
"""
iids: [ID!]
"""
List requirements by sort order
"""
sort: Sort
"""
Filter requirements by state
"""
state: RequirementState
): Requirement
"""
Find requirements. Available only when feature flag `requirements_management` is enabled.
"""
requirements(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
IID of the requirement, e.g., "1"
"""
iid: ID
"""
List of IIDs of requirements, e.g., [1, 2]
"""
iids: [ID!]
"""
Returns the last _n_ elements from the list.
"""
last: Int
"""
List requirements by sort order
"""
sort: Sort
"""
Filter requirements by state
"""
state: RequirementState
): RequirementConnection
"""
Detailed version of a Sentry error on the project
"""
@ -6664,6 +6734,41 @@ type Requirement {
userPermissions: RequirementPermissions!
}
"""
The connection type for Requirement.
"""
type RequirementConnection {
"""
A list of edges.
"""
edges: [RequirementEdge]
"""
A list of nodes.
"""
nodes: [Requirement]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type RequirementEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: Requirement
}
"""
Check permissions for the current user on a requirement
"""
@ -7463,6 +7568,31 @@ type SnippetPermissions {
updateSnippet: Boolean!
}
"""
Common sort values
"""
enum Sort {
"""
Created at ascending order
"""
created_asc
"""
Created at descending order
"""
created_desc
"""
Updated at ascending order
"""
updated_asc
"""
Updated at descending order
"""
updated_desc
}
type Submodule implements Entry {
"""
Flat path of the entry

View file

@ -18032,6 +18032,168 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "requirement",
"description": "Find a single requirement. Available only when feature flag `requirements_management` is enabled.",
"args": [
{
"name": "iid",
"description": "IID of the requirement, e.g., \"1\"",
"type": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
},
"defaultValue": null
},
{
"name": "iids",
"description": "List of IIDs of requirements, e.g., [1, 2]",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "sort",
"description": "List requirements by sort order",
"type": {
"kind": "ENUM",
"name": "Sort",
"ofType": null
},
"defaultValue": null
},
{
"name": "state",
"description": "Filter requirements by state",
"type": {
"kind": "ENUM",
"name": "RequirementState",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "Requirement",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "requirements",
"description": "Find requirements. Available only when feature flag `requirements_management` is enabled.",
"args": [
{
"name": "iid",
"description": "IID of the requirement, e.g., \"1\"",
"type": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
},
"defaultValue": null
},
{
"name": "iids",
"description": "List of IIDs of requirements, e.g., [1, 2]",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "sort",
"description": "List requirements by sort order",
"type": {
"kind": "ENUM",
"name": "Sort",
"ofType": null
},
"defaultValue": null
},
{
"name": "state",
"description": "Filter requirements by state",
"type": {
"kind": "ENUM",
"name": "RequirementState",
"ofType": null
},
"defaultValue": null
},
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "RequirementConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "sentryDetailedError",
"description": "Detailed version of a Sentry error on the project",
@ -20106,6 +20268,118 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "RequirementConnection",
"description": "The connection type for Requirement.",
"fields": [
{
"name": "edges",
"description": "A list of edges.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "RequirementEdge",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodes",
"description": "A list of nodes.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Requirement",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pageInfo",
"description": "Information to aid in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "PageInfo",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "RequirementEdge",
"description": "An edge in a connection.",
"fields": [
{
"name": "cursor",
"description": "A cursor for use in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "node",
"description": "The item at the end of the edge.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Requirement",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "RequirementPermissions",
@ -22643,6 +22917,41 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "Sort",
"description": "Common sort values",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "updated_desc",
"description": "Updated at descending order",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updated_asc",
"description": "Updated at ascending order",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "created_desc",
"description": "Created at descending order",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "created_asc",
"description": "Created at ascending order",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "SCALAR",
"name": "String",

View file

@ -898,6 +898,7 @@ Information about pagination in a connection.
| `removeSourceBranchAfterMerge` | Boolean | Indicates if `Delete source branch` option should be enabled by default for all new merge requests of the project |
| `repository` | Repository | Git repository of the project |
| `requestAccessEnabled` | Boolean | Indicates if users can request member access to the project |
| `requirement` | Requirement | Find a single requirement. Available only when feature flag `requirements_management` is enabled. |
| `sentryDetailedError` | SentryDetailedError | Detailed version of a Sentry error on the project |
| `sentryErrors` | SentryErrorCollection | Paginated collection of Sentry errors on the project |
| `serviceDeskAddress` | String | E-mail address of the service desk. |

View file

@ -81,7 +81,7 @@ already reserved for category labels).
The descriptions on the [labels page](https://gitlab.com/groups/gitlab-org/-/labels)
explain what falls under each type label.
The GitLab handbook documents [when something is a bug and when it is a feature request.](https://about.gitlab.com/handbook/product/product-management/process/feature-or-bug.html)
The GitLab handbook documents [when something is a bug and when it is a feature request](https://about.gitlab.com/handbook/product/product-management/process/feature-or-bug.html).
### Facet labels

View file

@ -32,12 +32,12 @@ The `BulkInsertSafe` concern has two functions:
- It performs checks against your model class to ensure that it does not use ActiveRecord
APIs that are not safe to use with respect to bulk insertions (more on that below).
- It adds a new class method `bulk_insert!`, which you can use to insert many records at once.
- It adds new class methods `bulk_insert!` and `bulk_upsert!`, which you can use to insert many records at once.
## Insert records via `bulk_insert!`
## Insert records with `bulk_insert!` and `bulk_upsert!`
If the target class passes the checks performed by `BulkInsertSafe`, you can proceed to use
the `bulk_insert!` class method as follows:
If the target class passes the checks performed by `BulkInsertSafe`, you can insert an array of
ActiveRecord model objects as follows:
```ruby
records = [MyModel.new, ...]
@ -45,6 +45,28 @@ records = [MyModel.new, ...]
MyModel.bulk_insert!(records)
```
Note that calls to `bulk_insert!` will always attempt to insert _new records_. If instead
you would like to replace existing records with new values, while still inserting those
that do not already exist, then you can use `bulk_upsert!`:
```ruby
records = [MyModel.new, existing_model, ...]
MyModel.bulk_upsert!(records, unique_by: [:name])
```
In this example, `unique_by` specifies the columns by which records are considered to be
unique and as such will be updated if they existed prior to insertion. For example, if
`existing_model` has a `name` attribute, and if a record with the same `name` value already
exists, its fields will be updated with those of `existing_model`.
The `unique_by` parameter can also be passed as a `Symbol`, in which case it specifies
a database index by which a column is considered unique:
```ruby
MyModel.bulk_insert!(records, unique_by: :index_on_name)
```
### Record validation
The `bulk_insert!` method guarantees that `records` will be inserted transactionally, and
@ -74,6 +96,23 @@ Since this will also affect the number of `INSERT`s that occur, make sure you me
performance impact this might have on your code. There is a trade-off between the number of
`INSERT` statements the database has to process and the size and cost of each `INSERT`.
### Handling duplicate records
NOTE: **Note:**
This parameter applies only to `bulk_insert!`. If you intend to update existing
records, use `bulk_upsert!` instead.
It may happen that some records you are trying to insert already exist, which would result in
primary key conflicts. There are two ways to address this problem: failing fast by raising an
error or skipping duplicate records. The default behavior of `bulk_insert!` is to fail fast
and raise an `ActiveRecord::RecordNotUnique` error.
If this is undesirable, you can instead skip duplicate records with the `skip_duplicates` flag:
```ruby
MyModel.bulk_insert!(records, skip_duplicates: true)
```
### Requirements for safe bulk insertions
Large parts of ActiveRecord's persistence API are built around the notion of callbacks. Many
@ -145,11 +184,12 @@ simply be treated as if you had invoked `save` from outside the block.
There are a few restrictions to how these APIs can be used:
- Bulk inserts only work for new records; `UPDATE`s or "upserts" are not supported yet.
- `ON CONFLICT` behavior cannot currently be configured; an error will be raised on primary key conflicts.
- `BulkInsertableAssociations` furthermore has the following restrictions:
- only compatible with `has_many` relations.
- does not support `has_many through: ...` relations.
- Writing [`jsonb`](https://www.postgresql.org/docs/current/datatype-json.html) content is
[not currently supported](https://gitlab.com/gitlab-org/gitlab/-/issues/210560).
Moreover, input data should either be limited to around 1000 records at most,
or already batched prior to calling bulk insert. The `INSERT` statement will run in a single

View file

@ -144,10 +144,20 @@ It's also important to ensure that any background migrations have been fully com
before upgrading to a new major version. To see the current size of the `background_migration` queue,
[Check for background migrations before upgrading](../update/README.md#checking-for-background-migrations-before-upgrading).
From version 12 onwards, an additional step is required. More significant migrations may occur during major release upgrades. To ensure these are successful, increment to the first minor version (`x.0.x`) during the major version jump. Then proceed with upgrading to a newer release.
### Version 12 onwards: Extra step for major upgrades
From version 12 onwards, an additional step is required. More significant migrations
may occur during major release upgrades.
To ensure these are successful:
1. Increment to the first minor version (`x.0.x`) during the major version jump.
1. Proceed with upgrading to a newer release.
For example: `11.11.x` -> `12.0.x` -> `12.8.x`
### Example upgrade paths
Please see the table below for some examples:
| Latest stable version | Your version | Recommended upgrade path | Note |
@ -155,8 +165,10 @@ Please see the table below for some examples:
| 9.4.5 | 8.13.4 | `8.13.4` -> `8.17.7` -> `9.4.5` | `8.17.7` is the last version in version `8` |
| 10.1.4 | 8.13.4 | `8.13.4 -> 8.17.7 -> 9.5.10 -> 10.1.4` | `8.17.7` is the last version in version `8`, `9.5.10` is the last version in version `9` |
| 11.3.4 | 8.13.4 | `8.13.4` -> `8.17.7` -> `9.5.10` -> `10.8.7` -> `11.3.4` | `8.17.7` is the last version in version `8`, `9.5.10` is the last version in version `9`, `10.8.7` is the last version in version `10` |
| 12.5.8 | 11.3.4 | `11.3.4` -> `11.11.8` -> `12.0.12` -> `12.5.8` | `11.11.8` is the last version in version `11`. `12.0.x` [is a required step.](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23211#note_272842444) |
| 12.8.5 | 9.2.6 | `9.2.6` -> `9.5.10` -> `10.8.7` -> `11.11.8` -> `12.0.12` -> `12.8.5` | Four intermediate versions required: the final 9.5, 10.8, 11.11 releases, plus 12.0 |
| 12.5.8 | 11.3.4 | `11.3.4` -> `11.11.8` -> `12.0.12` -> `12.5.8` | `11.11.8` is the last version in version `11`. `12.0.x` [is a required step](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23211#note_272842444). |
| 12.8.5 | 9.2.6 | `9.2.6` -> `9.5.10` -> `10.8.7` -> `11.11.8` -> `12.0.12` -> `12.8.5` | Four intermediate versions are required: the final 9.5, 10.8, 11.11 releases, plus 12.0. |
## More information
More information about the release procedures can be found in our
[release documentation](https://gitlab.com/gitlab-org/release/docs). You may also want to read our

View file

@ -548,6 +548,7 @@ Supported applications:
- [Sentry](#install-sentry-using-gitlab-ci)
- [GitLab Runner](#install-gitlab-runner-using-gitlab-ci)
- [Cilium](#install-cilium-using-gitlab-ci)
- [Vault](#install-vault-using-gitlab-ci)
- [JupyterHub](#install-jupyterhub-using-gitlab-ci)
- [Elastic Stack](#install-elastic-stack-using-gitlab-ci)
- [Crossplane](#install-crossplane-using-gitlab-ci)
@ -813,6 +814,95 @@ agent:
enabled: false
```
### Install Vault using GitLab CI
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/9982) in GitLab 12.9.
[Hashicorp Vault](https://vaultproject.io/) is a secrets management solution which
can be used to safely manage and store passwords, credentials, certificates and more. A Vault
installation could be leveraged to provide a single secure data store for credentials
used in your applications, GitLab CI jobs, and more. It could also serve as a way of
providing SSL/TLS certificates to systems and deployments in your infrastructure. Leveraging
Vault as a single source for all these credentials allows greater security by having
a single source of access, control, and auditability around all your sensitive
credentials and certificates.
To install Vault, enable it in the `.gitlab/managed-apps/config.yaml` file:
```yaml
vault:
installed: true
```
By default you will get a basic Vault setup with no high availability nor any scalable
storage backend. This is enough for simple testing and small scale deployments, though has limits
to how much it can scale, and as it is a single instance deployment, you will experience downtime
when upgrading the Vault application.
To optimally use Vault in a production environment, it's ideal to have a good understanding
of the internals of Vault and how to configure it. This can be done by reading the
[the Vault documentation](https://www.vaultproject.io/docs/internals/) as well as
the Vault Helm chart [values.yaml file](https://github.com/hashicorp/vault-helm/blob/v0.3.3/values.yaml).
At a minimum you will likely set up:
- A [seal](https://www.vaultproject.io/docs/configuration/seal/) for extra encryption
of the master key.
- A [storage backend](https://www.vaultproject.io/docs/configuration/storage/) that is
suitable for environment and storage security requirements.
- [HA Mode](https://www.vaultproject.io/docs/concepts/ha/).
- [The Vault UI](https://www.vaultproject.io/docs/configuration/ui/).
The following is an example values file (`.gitlab/managed-apps/vault/values.yaml`)
that configures Google Key Management Service for auto-unseal, using a Google Cloud Storage backend, enabling
the Vault UI, and enabling HA with 3 pod replicas. The `storage` and `seal` stanzas
below are examples and should be replaced with settings specific to your environment.
```yaml
# Enable the Vault WebUI
ui:
enabled: true
server:
# Disable the built in data storage volume as it's not safe for Hight Availablity mode
dataStorage:
enabled: false
# Enable High Availability Mode
ha:
enabled: true
# Configure Vault to listen on port 8200 for normal traffic and port 8201 for inter-cluster traffic
config: |
listener "tcp" {
tls_disable = 1
address = "[::]:8200"
cluster_address = "[::]:8201"
}
# Configure Vault to store its data in a GCS Bucket backend
storage "gcs" {
path = "gcs://my-vault-storage/vault-bucket"
ha_enabled = "true"
}
# Configure Vault to automatically unseal storage using a GKMS key
seal "gcpckms" {
project = "vault-helm-dev-246514"
region = "global"
key_ring = "vault-helm-unseal-kr"
crypto_key = "vault-helm-unseal-key"
}
```
Once you have successfully installed Vault, you will need to [initialize the Vault](https://learn.hashicorp.com/vault/getting-started/deploy#initializing-the-vault)
and obtain the initial root token. You will need access to your Kubernetes cluster that Vault has been deployed into in order to do this.
To initialise the Vault, get a shell to one of the Vault pods running inside Kubernetes (typically this is done by using the `kubectl` command line tool).
Once you have a shell into the pod, run the `vault operator init` command:
```shell
kubectl -n gitlab-managed-apps exec -it vault-0 sh
/ $ vault operator init
```
This should give you your unseal keys and initial root token. Make sure to note these down
and keep these safe as you will need them to unseal the Vault throughout its lifecycle.
### Install JupyterHub using GitLab CI
> [Introduced](https://gitlab.com/gitlab-org/cluster-integration/cluster-applications/-/merge_requests/40) in GitLab 12.8.

View file

@ -1,6 +1,6 @@
apply:
stage: deploy
image: "registry.gitlab.com/gitlab-org/cluster-integration/cluster-applications:v0.11.0"
image: "registry.gitlab.com/gitlab-org/cluster-integration/cluster-applications:v0.12.0"
environment:
name: production
variables:
@ -16,6 +16,7 @@ apply:
PROMETHEUS_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/prometheus/values.yaml
ELASTIC_STACK_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/elastic-stack/values.yaml
VAULT_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/vault/values.yaml
CROSSPLANE_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/crossplane/values.yaml
script:
- gitlab-managed-apps /usr/local/share/gitlab-managed-apps/helmfile.yaml
only:

View file

@ -12,7 +12,7 @@ module Gitlab
@client = client
end
def pod_logs(namespace, pod_name, container_name: nil, search: nil, start_time: nil, end_time: nil, cursor: nil)
def pod_logs(namespace, pod_name: nil, container_name: nil, search: nil, start_time: nil, end_time: nil, cursor: nil)
query = { bool: { must: [] } }.tap do |q|
filter_pod_name(q, pod_name)
filter_namespace(q, namespace)
@ -38,7 +38,7 @@ module Gitlab
{ "offset": { order: :desc } }
],
# only return these fields in the response
_source: ["@timestamp", "message"],
_source: ["@timestamp", "message", "kubernetes.pod.name"],
# fixed limit for now, we should support paginated queries
size: ::Gitlab::Elasticsearch::Logs::LOGS_LIMIT
}
@ -51,6 +51,9 @@ module Gitlab
end
def filter_pod_name(query, pod_name)
# We can filter by "all pods" with a null pod_name
return if pod_name.nil?
query[:bool][:must] << {
match_phrase: {
"kubernetes.pod.name" => {
@ -113,7 +116,8 @@ module Gitlab
results = results.map do |hit|
{
timestamp: hit["_source"]["@timestamp"],
message: hit["_source"]["message"]
message: hit["_source"]["message"],
pod: hit["_source"]["kubernetes"]["pod"]["name"]
}
end

View file

@ -711,9 +711,6 @@ msgstr ""
msgid "20-29 contributions"
msgstr ""
msgid "24 hours"
msgstr ""
msgid "2FA"
msgstr ""
@ -726,9 +723,6 @@ msgstr ""
msgid "3 hours"
msgstr ""
msgid "30 days"
msgstr ""
msgid "30 minutes"
msgstr ""
@ -750,9 +744,6 @@ msgstr ""
msgid "404|Please contact your GitLab administrator if you think this is a mistake."
msgstr ""
msgid "7 days"
msgstr ""
msgid "8 hours"
msgstr ""
@ -7660,6 +7651,9 @@ msgstr ""
msgid "EnvironmentsDashboard|This dashboard displays a maximum of 7 projects and 3 environments per project. %{readMoreLink}"
msgstr ""
msgid "Environments|All pods"
msgstr ""
msgid "Environments|An error occurred while canceling the auto stop, please try again"
msgstr ""
@ -7741,7 +7735,7 @@ msgstr ""
msgid "Environments|No deployments yet"
msgstr ""
msgid "Environments|No pods to display"
msgid "Environments|No pod selected"
msgstr ""
msgid "Environments|Note that this action will stop the environment, but it will %{emphasisStart}not%{emphasisEnd} have an effect on any existing deployment due to no “stop environment action” being defined in the %{ciConfigLinkStart}.gitlab-ci.yml%{ciConfigLinkEnd} file."

View file

@ -19,7 +19,12 @@
"_score": null,
"_source": {
"message": "10.8.2.1 - - [25/Oct/2019:08:03:22 UTC] \"GET / HTTP/1.1\" 200 13",
"@timestamp": "2019-12-13T14:35:34.034Z"
"@timestamp": "2019-12-13T14:35:34.034Z",
"kubernetes": {
"pod": {
"name": "production-6866bc8974-m4sk4"
}
}
},
"sort": [
9999998,
@ -33,7 +38,12 @@
"_score": null,
"_source": {
"message": "10.8.2.1 - - [27/Oct/2019:23:49:54 UTC] \"GET / HTTP/1.1\" 200 13",
"@timestamp": "2019-12-13T14:35:35.034Z"
"@timestamp": "2019-12-13T14:35:35.034Z",
"kubernetes": {
"pod": {
"name": "production-6866bc8974-m4sk4"
}
}
},
"sort": [
9999949,
@ -47,7 +57,12 @@
"_score": null,
"_source": {
"message": "10.8.2.1 - - [04/Nov/2019:23:09:24 UTC] \"GET / HTTP/1.1\" 200 13",
"@timestamp": "2019-12-13T14:35:36.034Z"
"@timestamp": "2019-12-13T14:35:36.034Z",
"kubernetes": {
"pod": {
"name": "production-6866bc8974-m4sk4"
}
}
},
"sort": [
9999944,
@ -61,7 +76,12 @@
"_score": null,
"_source": {
"message": "- -\u003e /",
"@timestamp": "2019-12-13T14:35:37.034Z"
"@timestamp": "2019-12-13T14:35:37.034Z",
"kubernetes": {
"pod": {
"name": "production-6866bc8974-m4sk4"
}
}
},
"sort": [
9999934,

View file

@ -33,7 +33,8 @@
],
"_source": [
"@timestamp",
"message"
"message",
"kubernetes.pod.name"
],
"size": 500
}

View file

@ -40,7 +40,8 @@
],
"_source": [
"@timestamp",
"message"
"message",
"kubernetes.pod.name"
],
"size": 500
}

View file

@ -37,7 +37,8 @@
],
"_source": [
"@timestamp",
"message"
"message",
"kubernetes.pod.name"
],
"size": 500
}

View file

@ -42,7 +42,8 @@
],
"_source": [
"@timestamp",
"message"
"message",
"kubernetes.pod.name"
],
"size": 500
}

View file

@ -42,7 +42,8 @@
],
"_source": [
"@timestamp",
"message"
"message",
"kubernetes.pod.name"
],
"size": 500
}

View file

@ -42,7 +42,8 @@
],
"_source": [
"@timestamp",
"message"
"message",
"kubernetes.pod.name"
],
"size": 500
}

View file

@ -43,7 +43,8 @@
],
"_source": [
"@timestamp",
"message"
"message",
"kubernetes.pod.name"
],
"size": 500
}

View file

@ -300,9 +300,10 @@ describe('EnvironmentLogs', () => {
const items = findPodsDropdown().findAll(GlDropdownItem);
expect(findPodsDropdown().props('text')).toBe(mockPodName);
expect(items.length).toBe(mockPods.length);
expect(items.length).toBe(mockPods.length + 1);
expect(items.at(0).text()).toBe('All pods');
mockPods.forEach((pod, i) => {
const item = items.at(i);
const item = items.at(i + 1);
expect(item.text()).toBe(pod);
});
});
@ -345,7 +346,7 @@ describe('EnvironmentLogs', () => {
expect(dispatch).not.toHaveBeenCalledWith(`${module}/showPodLogs`, expect.anything());
items.at(index).vm.$emit('click');
items.at(index + 1).vm.$emit('click');
expect(dispatch).toHaveBeenCalledWith(`${module}/showPodLogs`, mockPods[index]);
});

View file

@ -32,15 +32,93 @@ export const mockPods = [
];
export const mockLogsResult = [
{ timestamp: '2019-12-13T13:43:18.2760123Z', message: 'Log 1' },
{ timestamp: '2019-12-13T13:43:18.2760123Z', message: 'Log 2' },
{ timestamp: '2019-12-13T13:43:26.8420123Z', message: 'Log 3' },
{
timestamp: '2019-12-13T13:43:18.2760123Z',
message: '10.36.0.1 - - [16/Oct/2019:06:29:48 UTC] "GET / HTTP/1.1" 200 13',
pod: 'foo',
},
{
timestamp: '2019-12-13T13:43:18.2760123Z',
message: '- -> /',
pod: 'bar',
},
{
timestamp: '2019-12-13T13:43:26.8420123Z',
message: '10.36.0.1 - - [16/Oct/2019:06:29:57 UTC] "GET / HTTP/1.1" 200 13',
pod: 'foo',
},
{
timestamp: '2019-12-13T13:43:26.8420123Z',
message: '- -> /',
pod: 'bar',
},
{
timestamp: '2019-12-13T13:43:28.3710123Z',
message: '10.36.0.1 - - [16/Oct/2019:06:29:58 UTC] "GET / HTTP/1.1" 200 13',
pod: 'foo',
},
{
timestamp: '2019-12-13T13:43:28.3710123Z',
message: '- -> /',
pod: 'bar',
},
{
timestamp: '2019-12-13T13:43:36.8860123Z',
message: '10.36.0.1 - - [16/Oct/2019:06:30:07 UTC] "GET / HTTP/1.1" 200 13',
pod: 'foo',
},
{
timestamp: '2019-12-13T13:43:36.8860123Z',
message: '- -> /',
pod: 'bar',
},
{
timestamp: '2019-12-13T13:43:38.4000123Z',
message: '10.36.0.1 - - [16/Oct/2019:06:30:08 UTC] "GET / HTTP/1.1" 200 13',
pod: 'foo',
},
{
timestamp: '2019-12-13T13:43:38.4000123Z',
message: '- -> /',
pod: 'bar',
},
{
timestamp: '2019-12-13T13:43:46.8420123Z',
message: '10.36.0.1 - - [16/Oct/2019:06:30:17 UTC] "GET / HTTP/1.1" 200 13',
pod: 'foo',
},
{
timestamp: '2019-12-13T13:43:46.8430123Z',
message: '- -> /',
pod: 'bar',
},
{
timestamp: '2019-12-13T13:43:48.3240123Z',
message: '10.36.0.1 - - [16/Oct/2019:06:30:18 UTC] "GET / HTTP/1.1" 200 13',
pod: 'foo',
},
{
timestamp: '2019-12-13T13:43:48.3250123Z',
message: '- -> /',
pod: 'bar',
},
];
export const mockTrace = [
'Dec 13 13:43:18.276Z | Log 1',
'Dec 13 13:43:18.276Z | Log 2',
'Dec 13 13:43:26.842Z | Log 3',
'Dec 13 13:43:18.276Z | foo | 10.36.0.1 - - [16/Oct/2019:06:29:48 UTC] "GET / HTTP/1.1" 200 13',
'Dec 13 13:43:18.276Z | bar | - -> /',
'Dec 13 13:43:26.842Z | foo | 10.36.0.1 - - [16/Oct/2019:06:29:57 UTC] "GET / HTTP/1.1" 200 13',
'Dec 13 13:43:26.842Z | bar | - -> /',
'Dec 13 13:43:28.371Z | foo | 10.36.0.1 - - [16/Oct/2019:06:29:58 UTC] "GET / HTTP/1.1" 200 13',
'Dec 13 13:43:28.371Z | bar | - -> /',
'Dec 13 13:43:36.886Z | foo | 10.36.0.1 - - [16/Oct/2019:06:30:07 UTC] "GET / HTTP/1.1" 200 13',
'Dec 13 13:43:36.886Z | bar | - -> /',
'Dec 13 13:43:38.400Z | foo | 10.36.0.1 - - [16/Oct/2019:06:30:08 UTC] "GET / HTTP/1.1" 200 13',
'Dec 13 13:43:38.400Z | bar | - -> /',
'Dec 13 13:43:46.842Z | foo | 10.36.0.1 - - [16/Oct/2019:06:30:17 UTC] "GET / HTTP/1.1" 200 13',
'Dec 13 13:43:46.843Z | bar | - -> /',
'Dec 13 13:43:48.324Z | foo | 10.36.0.1 - - [16/Oct/2019:06:30:18 UTC] "GET / HTTP/1.1" 200 13',
'Dec 13 13:43:48.325Z | bar | - -> /',
];
export const mockResponse = {

View file

@ -13,7 +13,7 @@ import {
fetchMoreLogsPrepend,
} from '~/logs/stores/actions';
import { defaultTimeRange } from '~/monitoring/constants';
import { defaultTimeRange } from '~/vue_shared/constants';
import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
@ -172,14 +172,13 @@ describe('Logs Store actions', () => {
describe('fetchLogs', () => {
beforeEach(() => {
expectedMutations = [
{ type: types.REQUEST_PODS_DATA },
{ type: types.REQUEST_LOGS_DATA },
{ type: types.SET_CURRENT_POD_NAME, payload: mockPodName },
{ type: types.RECEIVE_PODS_DATA_SUCCESS, payload: mockPods },
{
type: types.RECEIVE_LOGS_DATA_SUCCESS,
payload: { logs: mockLogsResult, cursor: mockNextCursor },
},
{ type: types.SET_CURRENT_POD_NAME, payload: mockPodName },
{ type: types.RECEIVE_PODS_DATA_SUCCESS, payload: mockPods },
];
expectedActions = [];
@ -364,7 +363,6 @@ describe('Logs Store actions', () => {
null,
state,
[
{ type: types.REQUEST_PODS_DATA },
{ type: types.REQUEST_LOGS_DATA },
{ type: types.RECEIVE_PODS_DATA_ERROR },
{ type: types.RECEIVE_LOGS_DATA_ERROR },

View file

@ -223,17 +223,6 @@ describe('Logs Store Mutations', () => {
});
});
describe('REQUEST_PODS_DATA', () => {
it('receives pods data', () => {
mutations[types.REQUEST_PODS_DATA](state);
expect(state.pods).toEqual(
expect.objectContaining({
options: [],
}),
);
});
});
describe('RECEIVE_PODS_DATA_SUCCESS', () => {
it('receives pods data success', () => {
mutations[types.RECEIVE_PODS_DATA_SUCCESS](state, mockPods);

View file

@ -78,6 +78,7 @@ exports[`Dashboard template matches the default snapshot 1`] = `
label-size="sm"
>
<date-time-picker-stub
customenabled="true"
options="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]"
value="[object Object]"
/>

View file

@ -7,7 +7,7 @@ import { mockProjectDir } from '../mock_data';
import Dashboard from '~/monitoring/components/dashboard.vue';
import { createStore } from '~/monitoring/stores';
import { defaultTimeRange } from '~/monitoring/constants';
import { defaultTimeRange } from '~/vue_shared/constants';
import { propsData } from '../init_utils';
jest.mock('~/flash');

View file

@ -5,10 +5,10 @@ require 'spec_helper'
describe Gitlab::Elasticsearch::Logs do
let(:client) { Elasticsearch::Transport::Client }
let(:es_message_1) { { timestamp: "2019-12-13T14:35:34.034Z", message: "10.8.2.1 - - [25/Oct/2019:08:03:22 UTC] \"GET / HTTP/1.1\" 200 13" } }
let(:es_message_2) { { timestamp: "2019-12-13T14:35:35.034Z", message: "10.8.2.1 - - [27/Oct/2019:23:49:54 UTC] \"GET / HTTP/1.1\" 200 13" } }
let(:es_message_3) { { timestamp: "2019-12-13T14:35:36.034Z", message: "10.8.2.1 - - [04/Nov/2019:23:09:24 UTC] \"GET / HTTP/1.1\" 200 13" } }
let(:es_message_4) { { timestamp: "2019-12-13T14:35:37.034Z", message: "- -\u003e /" } }
let(:es_message_1) { { timestamp: "2019-12-13T14:35:34.034Z", pod: "production-6866bc8974-m4sk4", message: "10.8.2.1 - - [25/Oct/2019:08:03:22 UTC] \"GET / HTTP/1.1\" 200 13" } }
let(:es_message_2) { { timestamp: "2019-12-13T14:35:35.034Z", pod: "production-6866bc8974-m4sk4", message: "10.8.2.1 - - [27/Oct/2019:23:49:54 UTC] \"GET / HTTP/1.1\" 200 13" } }
let(:es_message_3) { { timestamp: "2019-12-13T14:35:36.034Z", pod: "production-6866bc8974-m4sk4", message: "10.8.2.1 - - [04/Nov/2019:23:09:24 UTC] \"GET / HTTP/1.1\" 200 13" } }
let(:es_message_4) { { timestamp: "2019-12-13T14:35:37.034Z", pod: "production-6866bc8974-m4sk4", message: "- -\u003e /" } }
let(:es_response) { JSON.parse(fixture_file('lib/elasticsearch/logs_response.json')) }
@ -40,49 +40,49 @@ describe Gitlab::Elasticsearch::Logs do
it 'returns the logs as an array' do
expect(client).to receive(:search).with(body: a_hash_equal_to_json(body)).and_return(es_response)
result = subject.pod_logs(namespace, pod_name)
result = subject.pod_logs(namespace, pod_name: pod_name)
expect(result).to eq(logs: [es_message_4, es_message_3, es_message_2, es_message_1], cursor: cursor)
end
it 'can further filter the logs by container name' do
expect(client).to receive(:search).with(body: a_hash_equal_to_json(body_with_container)).and_return(es_response)
result = subject.pod_logs(namespace, pod_name, container_name: container_name)
result = subject.pod_logs(namespace, pod_name: pod_name, container_name: container_name)
expect(result).to eq(logs: [es_message_4, es_message_3, es_message_2, es_message_1], cursor: cursor)
end
it 'can further filter the logs by search' do
expect(client).to receive(:search).with(body: a_hash_equal_to_json(body_with_search)).and_return(es_response)
result = subject.pod_logs(namespace, pod_name, search: search)
result = subject.pod_logs(namespace, pod_name: pod_name, search: search)
expect(result).to eq(logs: [es_message_4, es_message_3, es_message_2, es_message_1], cursor: cursor)
end
it 'can further filter the logs by start_time and end_time' do
expect(client).to receive(:search).with(body: a_hash_equal_to_json(body_with_times)).and_return(es_response)
result = subject.pod_logs(namespace, pod_name, start_time: start_time, end_time: end_time)
result = subject.pod_logs(namespace, pod_name: pod_name, start_time: start_time, end_time: end_time)
expect(result).to eq(logs: [es_message_4, es_message_3, es_message_2, es_message_1], cursor: cursor)
end
it 'can further filter the logs by only start_time' do
expect(client).to receive(:search).with(body: a_hash_equal_to_json(body_with_start_time)).and_return(es_response)
result = subject.pod_logs(namespace, pod_name, start_time: start_time)
result = subject.pod_logs(namespace, pod_name: pod_name, start_time: start_time)
expect(result).to eq(logs: [es_message_4, es_message_3, es_message_2, es_message_1], cursor: cursor)
end
it 'can further filter the logs by only end_time' do
expect(client).to receive(:search).with(body: a_hash_equal_to_json(body_with_end_time)).and_return(es_response)
result = subject.pod_logs(namespace, pod_name, end_time: end_time)
result = subject.pod_logs(namespace, pod_name: pod_name, end_time: end_time)
expect(result).to eq(logs: [es_message_4, es_message_3, es_message_2, es_message_1], cursor: cursor)
end
it 'can search after a cursor' do
expect(client).to receive(:search).with(body: a_hash_equal_to_json(body_with_cursor)).and_return(es_response)
result = subject.pod_logs(namespace, pod_name, cursor: cursor)
result = subject.pod_logs(namespace, pod_name: pod_name, cursor: cursor)
expect(result).to eq(logs: [es_message_4, es_message_3, es_message_2, es_message_1], cursor: cursor)
end
end

View file

@ -78,9 +78,7 @@ describe ::PodLogs::BaseService do
expect(result[:message]).to eq('Namespace is empty')
end
end
end
describe '#check_param_lengths' do
context 'when pod_name and container_name are provided' do
let(:params) do
{
@ -90,43 +88,13 @@ describe ::PodLogs::BaseService do
end
it 'returns success' do
result = subject.send(:check_param_lengths, {})
result = subject.send(:check_arguments, {})
expect(result[:status]).to eq(:success)
expect(result[:pod_name]).to eq(pod_name)
expect(result[:container_name]).to eq(container_name)
end
end
context 'when pod_name is too long' do
let(:params) do
{
'pod_name' => "a very long string." * 15
}
end
it 'returns an error' do
result = subject.send(:check_param_lengths, {})
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('pod_name cannot be larger than 253 chars')
end
end
context 'when container_name is too long' do
let(:params) do
{
'container_name' => "a very long string." * 15
}
end
it 'returns an error' do
result = subject.send(:check_param_lengths, {})
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('container_name cannot be larger than 253 chars')
end
end
end
describe '#get_raw_pods' do
@ -150,80 +118,4 @@ describe ::PodLogs::BaseService do
expect(result[:pods]).to eq([pod_name])
end
end
describe '#check_pod_name' do
it 'returns success if pod_name was specified' do
result = subject.send(:check_pod_name, pod_name: pod_name, pods: [pod_name])
expect(result[:status]).to eq(:success)
expect(result[:pod_name]).to eq(pod_name)
end
it 'returns success if pod_name was not specified but there are pods' do
result = subject.send(:check_pod_name, pod_name: nil, pods: [pod_name])
expect(result[:status]).to eq(:success)
expect(result[:pod_name]).to eq(pod_name)
end
it 'returns error if pod_name was not specified and there are no pods' do
result = subject.send(:check_pod_name, pod_name: nil, pods: [])
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('No pods available')
end
it 'returns error if pod_name was specified but does not exist' do
result = subject.send(:check_pod_name, pod_name: 'another_pod', pods: [pod_name])
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('Pod does not exist')
end
end
describe '#check_container_name' do
it 'returns success if container_name was specified' do
result = subject.send(:check_container_name,
container_name: container_name,
pod_name: pod_name,
raw_pods: raw_pods
)
expect(result[:status]).to eq(:success)
expect(result[:container_name]).to eq(container_name)
end
it 'returns success if container_name was not specified and there are containers' do
result = subject.send(:check_container_name,
pod_name: pod_name,
raw_pods: raw_pods
)
expect(result[:status]).to eq(:success)
expect(result[:container_name]).to eq(container_name)
end
it 'returns error if container_name was not specified and there are no containers on the pod' do
raw_pods.first.spec.containers = []
result = subject.send(:check_container_name,
pod_name: pod_name,
raw_pods: raw_pods
)
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('No containers available')
end
it 'returns error if container_name was specified but does not exist' do
result = subject.send(:check_container_name,
container_name: 'foo',
pod_name: pod_name,
raw_pods: raw_pods
)
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('Container does not exist')
end
end
end

View file

@ -170,7 +170,7 @@ describe ::PodLogs::ElasticsearchService do
.and_return(Elasticsearch::Transport::Client.new)
allow_any_instance_of(::Gitlab::Elasticsearch::Logs)
.to receive(:pod_logs)
.with(namespace, pod_name, container_name: container_name, search: search, start_time: start_time, end_time: end_time, cursor: cursor)
.with(namespace, pod_name: pod_name, container_name: container_name, search: search, start_time: start_time, end_time: end_time, cursor: cursor)
.and_return({ logs: expected_logs, cursor: expected_cursor })
result = subject.send(:pod_logs, result_arg)

View file

@ -9,13 +9,18 @@ describe ::PodLogs::KubernetesService do
let(:namespace) { 'autodevops-deploy-9-production' }
let(:pod_name) { 'pod-1' }
let(:container_name) { 'container-1' }
let(:container_name) { 'container-0' }
let(:params) { {} }
let(:raw_logs) do
"2019-12-13T14:04:22.123456Z Log 1\n2019-12-13T14:04:23.123456Z Log 2\n" \
"2019-12-13T14:04:24.123456Z Log 3"
end
let(:raw_pods) do
JSON.parse([
kube_pod(name: pod_name)
].to_json, object_class: OpenStruct)
end
subject { described_class.new(cluster, namespace, params: params) }
@ -140,9 +145,9 @@ describe ::PodLogs::KubernetesService do
let(:expected_logs) do
[
{ message: "Log 1", timestamp: "2019-12-13T14:04:22.123456Z" },
{ message: "Log 2", timestamp: "2019-12-13T14:04:23.123456Z" },
{ message: "Log 3", timestamp: "2019-12-13T14:04:24.123456Z" }
{ message: "Log 1", pod: 'pod-1', timestamp: "2019-12-13T14:04:22.123456Z" },
{ message: "Log 2", pod: 'pod-1', timestamp: "2019-12-13T14:04:23.123456Z" },
{ message: "Log 3", pod: 'pod-1', timestamp: "2019-12-13T14:04:24.123456Z" }
]
end
@ -163,4 +168,98 @@ describe ::PodLogs::KubernetesService do
end
end
end
describe '#check_pod_name' do
it 'returns success if pod_name was specified' do
result = subject.send(:check_pod_name, pod_name: pod_name, pods: [pod_name])
expect(result[:status]).to eq(:success)
expect(result[:pod_name]).to eq(pod_name)
end
it 'returns success if pod_name was not specified but there are pods' do
result = subject.send(:check_pod_name, pod_name: nil, pods: [pod_name])
expect(result[:status]).to eq(:success)
expect(result[:pod_name]).to eq(pod_name)
end
it 'returns error if pod_name was not specified and there are no pods' do
result = subject.send(:check_pod_name, pod_name: nil, pods: [])
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('No pods available')
end
it 'returns error if pod_name was specified but does not exist' do
result = subject.send(:check_pod_name, pod_name: 'another_pod', pods: [pod_name])
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('Pod does not exist')
end
it 'returns error if pod_name is too long' do
result = subject.send(:check_pod_name, pod_name: "a very long string." * 15, pods: [pod_name])
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('pod_name cannot be larger than 253 chars')
end
end
describe '#check_container_name' do
it 'returns success if container_name was specified' do
result = subject.send(:check_container_name,
container_name: container_name,
pod_name: pod_name,
raw_pods: raw_pods
)
expect(result[:status]).to eq(:success)
expect(result[:container_name]).to eq(container_name)
end
it 'returns success if container_name was not specified and there are containers' do
result = subject.send(:check_container_name,
pod_name: pod_name,
raw_pods: raw_pods
)
expect(result[:status]).to eq(:success)
expect(result[:container_name]).to eq(container_name)
end
it 'returns error if container_name was not specified and there are no containers on the pod' do
raw_pods.first.spec.containers = []
result = subject.send(:check_container_name,
pod_name: pod_name,
raw_pods: raw_pods
)
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('No containers available')
end
it 'returns error if container_name was specified but does not exist' do
result = subject.send(:check_container_name,
container_name: 'foo',
pod_name: pod_name,
raw_pods: raw_pods
)
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('Container does not exist')
end
it 'returns error if container_name is too long' do
result = subject.send(:check_container_name,
container_name: "a very long string." * 15,
pod_name: pod_name,
raw_pods: raw_pods
)
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('container_name cannot be larger than 253 chars')
end
end
end