Commit f580a3ed authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 72a19c71 0b5bc45e
......@@ -8,10 +8,14 @@ import {
import { generateBadges } from 'ee_else_ce/members/utils';
import { glEmojiTag } from '~/emoji';
import { __ } from '~/locale';
import { isUserBusy } from '~/set_status_modal/utils';
import { AVATAR_SIZE } from '../../constants';
export default {
name: 'UserAvatar',
i18n: {
busy: __('Busy'),
},
avatarSize: AVATAR_SIZE,
orphanedUserLabel: __('Orphaned member'),
safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
......@@ -46,7 +50,10 @@ export default {
}).filter((badge) => badge.show);
},
statusEmoji() {
return this.user?.status?.emoji;
return this.user?.showStatus && this.user?.status?.emoji;
},
isUserBusy() {
return isUserBusy(this.user?.availability || '');
},
},
methods: {
......@@ -73,6 +80,11 @@ export default {
:entity-id="user.id"
>
<template #meta>
<div v-if="isUserBusy" class="gl-p-1">
<span class="gl-text-gray-500 gl-font-sm gl-font-weight-normal"
>({{ $options.i18n.busy }})</span
>
</div>
<div v-if="statusEmoji" class="gl-p-1">
<span
v-safe-html:[$options.safeHtmlConfig]="glEmojiTag(statusEmoji)"
......
......@@ -9,12 +9,14 @@ import axios from '~/lib/utils/axios_utils';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import { redirectTo } from '~/lib/utils/url_utility';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import getRefMixin from '../mixins/get_ref';
import blobInfoQuery from '../queries/blob_info.query.graphql';
import { DEFAULT_BLOB_INFO, TEXT_FILE_TYPE, LFS_STORAGE } from '../constants';
import BlobButtonGroup from './blob_button_group.vue';
import BlobEdit from './blob_edit.vue';
import ForkSuggestion from './fork_suggestion.vue';
import { loadViewer, viewerProps } from './blob_viewers';
import { loadViewer } from './blob_viewers';
export default {
i18n: {
......@@ -29,7 +31,7 @@ export default {
GlButton,
ForkSuggestion,
},
mixins: [getRefMixin],
mixins: [getRefMixin, glFeatureFlagMixin()],
inject: {
originalBranch: {
default: '',
......@@ -78,52 +80,7 @@ export default {
isBinary: false,
isLoadingLegacyViewer: false,
activeViewerType: SIMPLE_BLOB_VIEWER,
project: {
userPermissions: {
pushCode: false,
downloadCode: false,
createMergeRequestIn: false,
forkProject: false,
},
pathLocks: {
nodes: [],
},
repository: {
empty: true,
blobs: {
nodes: [
{
name: '',
size: '',
rawTextBlob: '',
type: '',
fileType: '',
tooLarge: false,
path: '',
editBlobPath: '',
ideEditPath: '',
forkAndEditPath: '',
ideForkAndEditPath: '',
storedExternally: false,
externalStorage: '',
environmentFormattedExternalUrl: '',
environmentExternalUrlForRouteMap: '',
canModifyBlob: false,
canCurrentUserPushToBranch: false,
archived: false,
rawPath: '',
externalStorageUrl: '',
replacePath: '',
pipelineEditorPath: '',
deletePath: '',
simpleViewer: {},
richViewer: null,
webPath: '',
},
],
},
},
},
project: DEFAULT_BLOB_INFO,
};
},
computed: {
......@@ -134,7 +91,7 @@ export default {
return this.$apollo.queries.project.loading;
},
isBinaryFileType() {
return this.isBinary || this.blobInfo.simpleViewer?.fileType !== 'text';
return this.isBinary || this.blobInfo.simpleViewer?.fileType !== TEXT_FILE_TYPE;
},
blobInfo() {
const nodes = this.project?.repository?.blobs?.nodes || [];
......@@ -153,11 +110,16 @@ export default {
},
blobViewer() {
const { fileType } = this.viewer;
return loadViewer(fileType, this.isUsingLfs);
return this.shouldLoadLegacyViewer ? null : loadViewer(fileType, this.isUsingLfs);
},
viewerProps() {
const { fileType } = this.viewer;
return viewerProps(fileType, this.blobInfo);
shouldLoadLegacyViewer() {
return this.viewer.fileType === TEXT_FILE_TYPE && !this.glFeatures.highlightJs;
},
legacyViewerLoaded() {
return (
(this.activeViewerType === SIMPLE_BLOB_VIEWER && this.legacySimpleViewer) ||
(this.activeViewerType === RICH_BLOB_VIEWER && this.legacyRichViewer)
);
},
canLock() {
const { pushCode, downloadCode } = this.project.userPermissions;
......@@ -186,20 +148,22 @@ export default {
: this.blobInfo.forkAndEditPath;
},
isUsingLfs() {
return this.blobInfo.storedExternally && this.blobInfo.externalStorage === 'lfs';
return this.blobInfo.storedExternally && this.blobInfo.externalStorage === LFS_STORAGE;
},
},
methods: {
loadLegacyViewer(type) {
if (this.legacyViewerLoaded(type)) {
loadLegacyViewer() {
if (this.legacyViewerLoaded) {
return;
}
const type = this.activeViewerType;
this.isLoadingLegacyViewer = true;
axios
.get(`${this.blobInfo.webPath}?format=json&viewer=${type}`)
.then(({ data: { html, binary } }) => {
if (type === 'simple') {
if (type === SIMPLE_BLOB_VIEWER) {
this.legacySimpleViewer = html;
} else {
this.legacyRichViewer = html;
......@@ -210,12 +174,6 @@ export default {
})
.catch(() => this.displayError());
},
legacyViewerLoaded(type) {
return (
(type === SIMPLE_BLOB_VIEWER && this.legacySimpleViewer) ||
(type === RICH_BLOB_VIEWER && this.legacyRichViewer)
);
},
displayError() {
createFlash({ message: __('An error occurred while loading the file. Please try again.') });
},
......@@ -223,7 +181,7 @@ export default {
this.activeViewerType = newViewer || SIMPLE_BLOB_VIEWER;
if (!this.blobViewer) {
this.loadLegacyViewer(this.activeViewerType);
this.loadLegacyViewer();
}
},
editBlob(target) {
......@@ -309,7 +267,7 @@ export default {
:hide-line-numbers="true"
:loading="isLoadingLegacyViewer"
/>
<component :is="blobViewer" v-else v-bind="viewerProps" class="blob-viewer" />
<component :is="blobViewer" v-else :blob="blobInfo" class="blob-viewer" />
</div>
</div>
</template>
......@@ -9,19 +9,17 @@ export default {
GlLink,
},
props: {
fileName: {
type: String,
blob: {
type: Object,
required: true,
},
filePath: {
type: String,
required: true,
},
fileSize: {
type: Number,
required: false,
default: 0,
},
},
data() {
return {
fileName: this.blob.name,
filePath: this.blob.rawPath,
fileSize: this.blob.rawSize || 0,
};
},
computed: {
downloadFileSize() {
......
<script>
export default {
props: {
url: {
type: String,
required: true,
},
alt: {
type: String,
blob: {
type: Object,
required: true,
},
},
data() {
return {
url: this.blob.rawPath,
alt: this.blob.name,
};
},
};
</script>
<template>
......
......@@ -17,34 +17,3 @@ export const loadViewer = (type, isUsingLfs) => {
return viewer;
};
export const viewerProps = (type, blob) => {
const props = {
text: {
content: blob.rawTextBlob,
autoDetect: true, // We'll eventually disable autoDetect and pass the language explicitly to reduce the footprint (https://gitlab.com/gitlab-org/gitlab/-/issues/348145)
},
download: {
fileName: blob.name,
filePath: blob.rawPath,
fileSize: blob.rawSize,
},
image: {
url: blob.rawPath,
alt: blob.name,
},
video: {
url: blob.rawPath,
},
pdf: {
url: blob.rawPath,
fileSize: blob.rawSize,
},
lfs: {
fileName: blob.name,
filePath: blob.rawPath,
},
};
return props[type] || props[blob.externalStorage];
};
......@@ -13,15 +13,17 @@ export default {
GlSprintf,
},
props: {
fileName: {
type: String,
required: true,
},
filePath: {
type: String,
blob: {
type: Object,
required: true,
},
},
data() {
return {
fileName: this.blob.name,
filePath: this.blob.rawPath,
};
},
};
</script>
......
......@@ -11,17 +11,17 @@ export default {
tooLargeButtonText: __('Download PDF'),
},
props: {
url: {
type: String,
required: true,
},
fileSize: {
type: Number,
blob: {
type: Object,
required: true,
},
},
data() {
return { totalPages: 0 };
return {
url: this.blob.rawPath,
fileSize: this.blob.rawSize,
totalPages: 0,
};
},
computed: {
tooLargeToDisplay() {
......
<script>
export default {
props: {
url: {
type: String,
blob: {
type: Object,
required: true,
},
},
data() {
return {
url: this.blob.rawPath,
};
},
};
</script>
<template>
......
......@@ -25,3 +25,54 @@ export const PDF_MAX_FILE_SIZE = 10000000; // 10 MB
export const PDF_MAX_PAGE_LIMIT = 50;
export const ROW_APPEAR_DELAY = 150;
export const DEFAULT_BLOB_INFO = {
userPermissions: {
pushCode: false,
downloadCode: false,
createMergeRequestIn: false,
forkProject: false,
},
pathLocks: {
nodes: [],
},
repository: {
empty: true,
blobs: {
nodes: [
{
name: '',
size: '',
rawTextBlob: '',
type: '',
fileType: '',
tooLarge: false,
path: '',
editBlobPath: '',
ideEditPath: '',
forkAndEditPath: '',
ideForkAndEditPath: '',
storedExternally: false,
externalStorage: '',
environmentFormattedExternalUrl: '',
environmentExternalUrlForRouteMap: '',
canModifyBlob: false,
canCurrentUserPushToBranch: false,
archived: false,
rawPath: '',
externalStorageUrl: '',
replacePath: '',
pipelineEditorPath: '',
deletePath: '',
simpleViewer: {},
richViewer: null,
webPath: '',
},
],
},
},
};
export const TEXT_FILE_TYPE = 'text';
export const LFS_STORAGE = 'lfs';
......@@ -4,6 +4,7 @@ import LineNumbers from '~/vue_shared/components/line_numbers.vue';
import { sanitize } from '~/lib/dompurify';
const LINE_SELECT_CLASS_NAME = 'hll';
const PLAIN_TEXT_LANGUAGE = 'plaintext';
export default {
components: {
......@@ -13,24 +14,21 @@ export default {
SafeHtml: GlSafeHtmlDirective,
},
props: {
content: {
type: String,
blob: {
type: Object,
required: true,
},
language: {
type: String,
required: false,
default: 'plaintext',
},
autoDetect: {
type: Boolean,
required: false,
default: false,
default: true, // We'll eventually disable autoDetect and pass the language explicitly to reduce the footprint (https://gitlab.com/gitlab-org/gitlab/-/issues/348145)
},
},
data() {
return {
languageDefinition: null,
content: this.blob.rawTextBlob,
language: this.blob.language || PLAIN_TEXT_LANGUAGE,
hljs: null,
};
},
......
# frozen_string_literal: true
class MemberUserEntity < UserEntity
unexpose :show_status
unexpose :path
unexpose :state
unexpose :status_tooltip_html
......
......@@ -508,5 +508,15 @@ module Gitlab
end
end
end
# DO NOT PLACE ANY INITIALIZERS AFTER THIS.
config.after_initialize do
# on_master_start yields immediately in unclustered environments and runs
# when the primary process is done initializing otherwise.
Gitlab::Cluster::LifecycleEvents.on_master_start do
Gitlab::Metrics::BootTimeTracker.instance.track_boot_time!
Gitlab::Console.welcome!
end
end
end
end
---
name: track_application_boot_time
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79139
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/351769
milestone: '14.8'
type: development
group: group::memory
default_enabled: true
# frozen_string_literal: true
# rubocop:disable Rails/Output
if Gitlab::Runtime.console?
# note that this will not print out when using `spring`
justify = 15
puts '-' * 80
puts " Ruby:".ljust(justify) + RUBY_DESCRIPTION
puts " GitLab:".ljust(justify) + "#{Gitlab::VERSION} (#{Gitlab.revision}) #{Gitlab.ee? ? 'EE' : 'FOSS'}"
puts " GitLab Shell:".ljust(justify) + "#{Gitlab::VersionInfo.parse(Gitlab::Shell.version)}"
if ApplicationRecord.database.exists?
puts " #{ApplicationRecord.database.human_adapter_name}:".ljust(justify) + ApplicationRecord.database.version
Gitlab.ee do
if Gitlab::Geo.connected? && Gitlab::Geo.enabled?
puts " Geo enabled:".ljust(justify) + 'yes'
puts " Geo server:".ljust(justify) + EE::GeoHelper.current_node_human_status
end
end
end
puts '-' * 80
# Stop irb from writing a history file by default.
module IrbNoHistory
def init_config(*)
......
......@@ -4,13 +4,15 @@ group: Configure
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Install the GitLab Agent Server (KAS) **(FREE SELF)**
# Install the GitLab Agent Server for Kubernetes (KAS) **(FREE SELF)**
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3834) in GitLab 13.10, the GitLab Agent Server (KAS) became available on GitLab.com under `wss://kas.gitlab.com`.
> [Moved](https://gitlab.com/groups/gitlab-org/-/epics/6290) from GitLab Premium to GitLab Free in 14.5.
> - [Moved](https://gitlab.com/groups/gitlab-org/-/epics/6290) from GitLab Premium to GitLab Free in 14.5.
The GitLab Agent Server (KAS) is a GitLab backend service dedicated to
managing the [GitLab Agent](../../user/clusters/agent/index.md).
The GitLab Agent Server for Kubernetes is a GitLab backend service dedicated to
managing the [GitLab Agent for Kubernetes](../../user/clusters/agent/index.md).
The KAS acronym refers to the former name, Kubernetes Agent Server.
The KAS is already installed and available in GitLab.com under `wss://kas.gitlab.com`.
This document describes how to install a KAS for GitLab self-managed instances.
......@@ -94,8 +96,8 @@ For GitLab instances installed through Omnibus packages:
## Troubleshooting
If you face any issues with KAS, you can read the service logs
with the following command:
If you have issues while using the GitLab Agent Server for Kubernetes, view the
service logs by running the following command:
```shell
kubectl logs -f -l=app=kas -n <YOUR-GITLAB-NAMESPACE>
......@@ -103,8 +105,7 @@ kubectl logs -f -l=app=kas -n <YOUR-GITLAB-NAMESPACE>
In Omnibus GitLab, find the logs in `/var/log/gitlab/gitlab-kas/`.
See also the [user documentation](../../user/clusters/agent/index.md#troubleshooting)
for troubleshooting problems with individual agents.
You can also [troubleshoot issues with individual Agents](../../user/clusters/agent/troubleshooting.md).
### KAS logs - GitOps: failed to get project information
......
......@@ -48,6 +48,7 @@ The following metrics are available:
| `gitlab_database_transaction_seconds` | Histogram | 12.1 | Time spent in database transactions, in seconds | |
| `gitlab_method_call_duration_seconds` | Histogram | 10.2 | Method calls real duration | `controller`, `action`, `module`, `method` |
| `gitlab_page_out_of_bounds` | Counter | 12.8 | Counter for the PageLimiter pagination limit being hit | `controller`, `action`, `bot` |
| `gitlab_rails_boot_time_seconds` | Gauge | 14.8 | Time elapsed for Rails primary process to finish startup | |
| `gitlab_rails_queue_duration_seconds` | Histogram | 9.4 | Measures latency between GitLab Workhorse forwarding a request to Rails | |
| `gitlab_sql_duration_seconds` | Histogram | 10.2 | SQL execution time, excluding `SCHEMA` operations and `BEGIN` / `COMMIT` | |
| `gitlab_sql_<role>_duration_seconds` | Histogram | 13.10 | SQL execution time, excluding `SCHEMA` operations and `BEGIN` / `COMMIT`, grouped by database roles (primary/replica) | |
......
......@@ -205,191 +205,6 @@ For self-managed GitLab instances, go to `https://gitlab.example.com/-/graphql-e
Find out how to [migrate to the GitLab Agent for Kubernetes](../../infrastructure/clusters/migrate_to_gitlab_agent.md) from the certificate-based integration depending on the features you use.
## Troubleshooting
## Related topics
If you face any issues while using the Agent, read the
service logs with the following command:
```shell
kubectl logs -f -l=app=gitlab-kubernetes-agent -n gitlab-kubernetes-agent
```
GitLab administrators can additionally view the [GitLab Agent Server logs](../../../administration/clusters/kas.md#troubleshooting).
### Agent logs
#### Transport: Error while dialing failed to WebSocket dial
```json
{
"level": "warn",
"time": "2020-11-04T10:14:39.368Z",
"msg": "GetConfiguration failed",
"error": "rpc error: code = Unavailable desc = connection error: desc = \"transport: Error while dialing failed to WebSocket dial: failed to send handshake request: Get \\\"https://gitlab-kas:443/-/kubernetes-agent\\\": dial tcp: lookup gitlab-kas on 10.60.0.10:53: no such host\""
}
```
This error is shown if there are some connectivity issues between the address
specified as `kas-address`, and your Agent pod. To fix it, make sure that you
specified the `kas-address` correctly.
```json
{
"level": "error",
"time": "2021-06-25T21:15:45.335Z",
"msg": "Reverse tunnel",
"mod_name": "reverse_tunnel",
"error": "Connect(): rpc error: code = Unavailable desc = connection error: desc= \"transport: Error while dialing failed to WebSocket dial: expected handshake response status code 101 but got 301\""
}
```
This error occurs if the `kas-address` doesn't include a trailing slash. To fix it, make sure that the
`wss` or `ws` URL ends with a trailing slash, such as `wss://GitLab.host.tld:443/-/kubernetes-agent/`
or `ws://GitLab.host.tld:80/-/kubernetes-agent/`.
#### ValidationError(Deployment.metadata)
```json
{
"level": "info",
"time": "2020-10-30T08:56:54.329Z",
"msg": "Synced",
"project_id": "root/kas-manifest001",
"resource_key": "apps/Deployment/kas-test001/nginx-deployment",
"sync_result": "error validating data: [ValidationError(Deployment.metadata): unknown field \"replicas\" in io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta, ValidationError(Deployment.metadata): unknown field \"selector\" in io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta, ValidationError(Deployment.metadata): unknown field \"template\" in io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta]"
}
```
This error is shown if a manifest file is malformed, and Kubernetes can't
create specified objects. Make sure that your manifest files are valid. You
may try using them to create objects in Kubernetes directly for more troubleshooting.
#### Error while dialing failed to WebSocket dial: failed to send handshake request
```json
{
"level": "warn",
"time": "2020-10-30T09:50:51.173Z",
"msg": "GetConfiguration failed",
"error": "rpc error: code = Unavailable desc = connection error: desc = \"transport: Error while dialing failed to WebSocket dial: failed to send handshake request: Get \\\"https://GitLabhost.tld:443/-/kubernetes-agent\\\": net/http: HTTP/1.x transport connection broken: malformed HTTP response \\\"\\\\x00\\\\x00\\\\x06\\\\x04\\\\x00\\\\x00\\\\x00\\\\x00\\\\x00\\\\x00\\\\x05\\\\x00\\\\x00@\\\\x00\\\"\""
}
```
This error is shown if you configured `wss` as `kas-address` on the agent side,
but KAS on the server side is not available via `wss`. To fix it, make sure the
same schemes are configured on both sides.
It's not possible to set the `grpc` scheme due to the issue
[It is not possible to configure KAS to work with `grpc` without directly editing GitLab KAS deployment](https://gitlab.com/gitlab-org/gitlab/-/issues/276888). To use `grpc` while the
issue is in progress, directly edit the deployment with the
`kubectl edit deployment gitlab-kas` command, and change `--listen-websocket=true` to `--listen-websocket=false`. After running that command, you should be able to use
`grpc://gitlab-kas.<YOUR-NAMESPACE>:8150`.
#### Decompressor is not installed for grpc-encoding
```json
{
"level": "warn",
"time": "2020-11-05T05:25:46.916Z",
"msg": "GetConfiguration.Recv failed",
"error": "rpc error: code = Unimplemented desc = grpc: Decompressor is not installed for grpc-encoding \"gzip\""
}
```
This error is shown if the version of the agent is newer that the version of KAS.
To fix it, make sure that both `agentk` and KAS use the same versions.
#### Certificate signed by unknown authority
```json
{
"level": "error",
"time": "2021-02-25T07:22:37.158Z",
"msg": "Reverse tunnel",
"mod_name": "reverse_tunnel",
"error": "Connect(): rpc error: code = Unavailable desc = connection error: desc = \"transport: Error while dialing failed to WebSocket dial: failed to send handshake request: Get \\\"https://GitLabhost.tld:443/-/kubernetes-agent/\\\": x509: certificate signed by unknown authority\""
}
```
This error is shown if your GitLab instance is using a certificate signed by an internal CA that
is unknown to the agent. One approach to fixing it is to present the CA certificate file to the agent
via a Kubernetes `configmap` and mount the file in the agent `/etc/ssl/certs` directory from where it
will be picked up automatically.
For example, if your internal CA certificate is `myCA.pem`:
```plaintext
kubectl -n gitlab-kubernetes-agent create configmap ca-pemstore --from-file=myCA.pem
```
Then in `resources.yml`:
```yaml
spec:
serviceAccountName: gitlab-kubernetes-agent
containers:
- name: agent
image: "registry.gitlab.com/gitlab-org/cluster-integration/gitlab-agent/agentk:<version>"
args:
- --token-file=/config/token
- --kas-address
- wss://kas.host.tld:443 # replace this line with the line below if using Omnibus GitLab or GitLab.com.
# - wss://gitlab.host.tld:443/-/kubernetes-agent/
# - wss://kas.gitlab.com # for GitLab.com users, use this KAS.
# - grpc://host.docker.internal:8150 # use this attribute when connecting from Docker.
volumeMounts:
- name: token-volume
mountPath: /config
- name: ca-pemstore-volume
mountPath: /etc/ssl/certs/myCA.pem
subPath: myCA.pem
volumes:
- name: token-volume
secret:
secretName: gitlab-kubernetes-agent-token
- name: ca-pemstore-volume
configMap:
name: ca-pemstore
items:
- key: myCA.pem
path: myCA.pem
```
Alternatively, you can mount the certificate file at a different location and include it using the
`--ca-cert-file` agent parameter:
```yaml
containers:
- name: agent
image: "registry.gitlab.com/gitlab-org/cluster-integration/gitlab-agent/agentk:<version>"
args:
- --ca-cert-file=/tmp/myCA.pem
- --token-file=/config/token
- --kas-address
- wss://kas.host.tld:443 # replace this line with the line below if using Omnibus GitLab or GitLab.com.
# - wss://gitlab.host.tld:443/-/kubernetes-agent/
# - wss://kas.gitlab.com # for GitLab.com users, use this KAS.
# - grpc://host.docker.internal:8150 # use this attribute when connecting from Docker.
volumeMounts:
- name: token-volume
mountPath: /config
- name: ca-pemstore-volume
mountPath: /tmp/myCA.pem
subPath: myCA.pem
```
#### Project not found
```json
{
"level ":"error ",
"time ":"2022-01-05T15:18:11.331Z",
"msg ":"GetObjectsToSynchronize.Recv failed ",
"mod_name ":"gitops ",
"error ":"rpc error: code = NotFound desc = project not found ",
}
```
This error is shown if the manifest project is not public. To fix it,
[make sure your manifest project is public](repository.md#synchronize-manifest-projects) or your manifest files
are stored in the Agent's configuration repository.
- [Troubleshooting](troubleshooting.md)
---
stage: Configure
group: Configure
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Troubleshooting the GitLab Agent for Kubernetes
When you are using the GitLab Agent for Kubernetes, you might experience issues you need to troubleshoot.
You can start by viewing the service logs:
```shell
kubectl logs -f -l=app=gitlab-kubernetes-agent -n gitlab-kubernetes-agent
```
If you are a GitLab administrator, you can also view the [GitLab Agent Server logs](../../../administration/clusters/kas.md#troubleshooting).
## Transport: Error while dialing failed to WebSocket dial
```json
{
"level": "warn",
"time": "2020-11-04T10:14:39.368Z",
"msg": "GetConfiguration failed",
"error": "rpc error: code = Unavailable desc = connection error: desc = \"transport: Error while dialing failed to WebSocket dial: failed to send handshake request: Get \\\"https://gitlab-kas:443/-/kubernetes-agent\\\": dial tcp: lookup gitlab-kas on 10.60.0.10:53: no such host\""
}
```
This error is shown if there are some connectivity issues between the address
specified as `kas-address`, and your Agent pod. To fix it, make sure that you
specified the `kas-address` correctly.
```json
{
"level": "error",
"time": "2021-06-25T21:15:45.335Z",
"msg": "Reverse tunnel",
"mod_name": "reverse_tunnel",
"error": "Connect(): rpc error: code = Unavailable desc = connection error: desc= \"transport: Error while dialing failed to WebSocket dial: expected handshake response status code 101 but got 301\""
}
```
This error occurs if the `kas-address` doesn't include a trailing slash. To fix it, make sure that the
`wss` or `ws` URL ends with a trailing slash, such as `wss://GitLab.host.tld:443/-/kubernetes-agent/`
or `ws://GitLab.host.tld:80/-/kubernetes-agent/`.
## ValidationError(Deployment.metadata)
```json
{
"level": "info",
"time": "2020-10-30T08:56:54.329Z",
"msg": "Synced",
"project_id": "root/kas-manifest001",
"resource_key": "apps/Deployment/kas-test001/nginx-deployment",
"sync_result": "error validating data: [ValidationError(Deployment.metadata): unknown field \"replicas\" in io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta, ValidationError(Deployment.metadata): unknown field \"selector\" in io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta, ValidationError(Deployment.metadata): unknown field \"template\" in io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta]"
}
```
This error is shown if a manifest file is malformed, and Kubernetes can't
create specified objects. Make sure that your manifest files are valid. You
may try using them to create objects in Kubernetes directly for more troubleshooting.
## Error while dialing failed to WebSocket dial: failed to send handshake request
```json
{
"level": "warn",
"time": "2020-10-30T09:50:51.173Z",
"msg": "GetConfiguration failed",
"error": "rpc error: code = Unavailable desc = connection error: desc = \"transport: Error while dialing failed to WebSocket dial: failed to send handshake request: Get \\\"https://GitLabhost.tld:443/-/kubernetes-agent\\\": net/http: HTTP/1.x transport connection broken: malformed HTTP response \\\"\\\\x00\\\\x00\\\\x06\\\\x04\\\\x00\\\\x00\\\\x00\\\\x00\\\\x00\\\\x00\\\\x05\\\\x00\\\\x00@\\\\x00\\\"\""
}
```
This error is shown if you configured `wss` as `kas-address` on the agent side,
but KAS on the server side is not available via `wss`. To fix it, make sure the
same schemes are configured on both sides.
It's not possible to set the `grpc` scheme due to the issue
[It is not possible to configure KAS to work with `grpc` without directly editing GitLab KAS deployment](https://gitlab.com/gitlab-org/gitlab/-/issues/276888). To use `grpc` while the
issue is in progress, directly edit the deployment with the
`kubectl edit deployment gitlab-kas` command, and change `--listen-websocket=true` to `--listen-websocket=false`. After running that command, you should be able to use
`grpc://gitlab-kas.<YOUR-NAMESPACE>:8150`.
## Decompressor is not installed for grpc-encoding
```json
{
"level": "warn",
"time": "2020-11-05T05:25:46.916Z",
"msg": "GetConfiguration.Recv failed",
"error": "rpc error: code = Unimplemented desc = grpc: Decompressor is not installed for grpc-encoding \"gzip\""
}
```
This error is shown if the version of the agent is newer that the version of KAS.
To fix it, make sure that both `agentk` and KAS use the same versions.
## Certificate signed by unknown authority
```json
{
"level": "error",
"time": "2021-02-25T07:22:37.158Z",
"msg": "Reverse tunnel",
"mod_name": "reverse_tunnel",
"error": "Connect(): rpc error: code = Unavailable desc = connection error: desc = \"transport: Error while dialing failed to WebSocket dial: failed to send handshake request: Get \\\"https://GitLabhost.tld:443/-/kubernetes-agent/\\\": x509: certificate signed by unknown authority\""
}
```
This error is shown if your GitLab instance is using a certificate signed by an internal CA that
is unknown to the agent. One approach to fixing it is to present the CA certificate file to the agent
via a Kubernetes `configmap` and mount the file in the agent `/etc/ssl/certs` directory from where it
will be picked up automatically.
For example, if your internal CA certificate is `myCA.pem`:
```plaintext
kubectl -n gitlab-kubernetes-agent create configmap ca-pemstore --from-file=myCA.pem
```
Then in `resources.yml`:
```yaml
spec:
serviceAccountName: gitlab-kubernetes-agent
containers:
- name: agent
image: "registry.gitlab.com/gitlab-org/cluster-integration/gitlab-agent/agentk:<version>"
args:
- --token-file=/config/token
- --kas-address
- wss://kas.host.tld:443 # replace this line with the line below if using Omnibus GitLab or GitLab.com.
# - wss://gitlab.host.tld:443/-/kubernetes-agent/
# - wss://kas.gitlab.com # for GitLab.com users, use this KAS.
# - grpc://host.docker.internal:8150 # use this attribute when connecting from Docker.
volumeMounts:
- name: token-volume
mountPath: /config
- name: ca-pemstore-volume
mountPath: /etc/ssl/certs/myCA.pem
subPath: myCA.pem
volumes:
- name: token-volume
secret:
secretName: gitlab-kubernetes-agent-token
- name: ca-pemstore-volume
configMap:
name: ca-pemstore
items:
- key: myCA.pem
path: myCA.pem
```
Alternatively, you can mount the certificate file at a different location and include it using the
`--ca-cert-file` agent parameter:
```yaml
containers:
- name: agent
image: "registry.gitlab.com/gitlab-org/cluster-integration/gitlab-agent/agentk:<version>"
args:
- --ca-cert-file=/tmp/myCA.pem
- --token-file=/config/token
- --kas-address
- wss://kas.host.tld:443 # replace this line with the line below if using Omnibus GitLab or GitLab.com.
# - wss://gitlab.host.tld:443/-/kubernetes-agent/
# - wss://kas.gitlab.com # for GitLab.com users, use this KAS.
# - grpc://host.docker.internal:8150 # use this attribute when connecting from Docker.
volumeMounts:
- name: token-volume
mountPath: /config
- name: ca-pemstore-volume
mountPath: /tmp/myCA.pem
subPath: myCA.pem
```
## Project not found
```json
{
"level ":"error ",
"time ":"2022-01-05T15:18:11.331Z",
"msg ":"GetObjectsToSynchronize.Recv failed ",
"mod_name ":"gitops ",
"error ":"rpc error: code = NotFound desc = project not found ",
}
```
This error is shown if the manifest project is not public. To fix it,
[make sure your manifest project is public](repository.md#synchronize-manifest-projects) or your manifest files
are stored in the Agent's configuration repository.
......@@ -6,36 +6,33 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Infrastructure as Code with Terraform and GitLab **(FREE)**
## Motivation
With Terraform in GitLab, you can use GitLab authentication and authorization with
your GitOps and Infrastructure-as-Code (IaC) workflows.
Use these features if you want to collaborate on Terraform code within GitLab or would like to use GitLab as a Terraform state storage that incorporates best practices out of the box.
The Terraform integration features in GitLab enable your GitOps / Infrastructure-as-Code (IaC)
workflows to tie into GitLab authentication and authorization. These features focus on
lowering the barrier to entry for teams to adopt Terraform, collaborate effectively in
GitLab, and support Terraform best practices.
## Quick Start
## Integrate your project with Terraform
> SAST test was [introduced](https://gitlab.com/groups/gitlab-org/-/epics/6655) in GitLab 14.6.
Use the following `.gitlab-ci.yml` to set up a basic Terraform project integration
for GitLab versions 14.0 and later:
In GitLab 14.0 and later, to integrate your project with Terraform, add the following
to your `.gitlab-ci.yml` file:
```yaml
include:
- template: Terraform.latest.gitlab-ci.yml
variables:
# If not using GitLab's HTTP backend, remove this line and specify TF_HTTP_* variables
# If you do not use the GitLab HTTP backend, remove this line and specify TF_HTTP_* variables
TF_STATE_NAME: default
TF_CACHE_KEY: default
# If your terraform files are in a subdirectory, set TF_ROOT accordingly
# TF_ROOT: terraform/production
```
This template includes the following parameters that you can override:
The `Terraform.latest.gitlab-ci.yml` template:
- Uses the latest [GitLab Terraform image](https://gitlab.com/gitlab-org/terraform-images).
- Uses the [GitLab-managed Terraform State](#gitlab-managed-terraform-state) as
- Uses the [GitLab-managed Terraform state](#gitlab-managed-terraform-state) as
the Terraform state storage backend.
- Creates [four pipeline stages](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Terraform.latest.gitlab-ci.yml):
`test`, `validate`, `build`, and `deploy`. These stages
......@@ -44,10 +41,12 @@ This template includes the following parameters that you can override:
- Runs the [Terraform SAST scanner](../../application_security/iac_scanning/index.md#configure-iac-scanning-manually),
that you can disable by creating a `SAST_DISABLED` environment variable and setting it to `1`.
The latest template described above might contain breaking changes between major GitLab releases. For users requiring more stable setups, we
recommend using the stable templates:
You can override the values in the default template by updating your `.gitlab-ci.yml` file.
The latest template might contain breaking changes between major GitLab releases.
For a more stable template, we recommend:
- [A ready to use version](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Terraform.gitlab-ci.yml)
- [A ready-to-use version](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Terraform.gitlab-ci.yml)
- [A base template for customized setups](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Terraform/Base.gitlab-ci.yml)
This video from January 2021 walks you through all the GitLab Terraform integration features:
......@@ -59,7 +58,7 @@ This video from January 2021 walks you through all the GitLab Terraform integrat
<iframe src="https://www.youtube.com/embed/iGXjUrkkzDI" frameborder="0" allowfullscreen="true"> </iframe>
</figure>
## GitLab Managed Terraform state
## GitLab-managed Terraform state
[Terraform remote backends](https://www.terraform.io/docs/language/settings/backends/index.html)
enable you to store the state file in a remote, shared store. GitLab uses the
......@@ -67,7 +66,7 @@ enable you to store the state file in a remote, shared store. GitLab uses the
to securely store the state files in local storage (the default) or
[the remote store of your choice](../../../administration/terraform_state.md).
The GitLab managed Terraform state backend can store your Terraform state easily and
The GitLab-managed Terraform state backend can store your Terraform state easily and
securely. It spares you from setting up additional remote resources like
Amazon S3 or Google Cloud Storage. Its features include:
......@@ -75,7 +74,7 @@ Amazon S3 or Google Cloud Storage. Its features include:
- Locking and unlocking state.
- Remote Terraform plan and apply execution.
Read more on setting up and [using GitLab Managed Terraform states](terraform_state.md).
Read more about setting up and [using GitLab-managed Terraform states](terraform_state.md).
## Terraform module registry
......@@ -104,7 +103,7 @@ to manage various aspects of GitLab using Terraform. The provider is an open sou
owned by GitLab, where everyone can contribute.
The [documentation of the provider](https://registry.terraform.io/providers/gitlabhq/gitlab/latest/docs)
is available as part of the official Terraform provider documentations.
is available as part of the official Terraform provider documentation.
## Create a new cluster through IaC (DEPRECATED)
......
......@@ -23,7 +23,7 @@ recommend encrypting plan output or modifying the project visibility settings.
## Configure Terraform report artifacts
GitLab ships with a [pre-built CI template](index.md#quick-start) that uses GitLab Managed Terraform state and integrates Terraform changes into merge requests. We recommend customizing the pre-built image and relying on the `gitlab-terraform` helper provided within for a quick setup.
GitLab ships with a [pre-built CI template](index.md#integrate-your-project-with-terraform) that uses GitLab Managed Terraform state and integrates Terraform changes into merge requests. We recommend customizing the pre-built image and relying on the `gitlab-terraform` helper provided within for a quick setup.
To manually configure a GitLab Terraform Report artifact:
......
......@@ -4,7 +4,7 @@ group: Configure
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# GitLab managed Terraform State **(FREE)**
# GitLab-managed Terraform state **(FREE)**
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2673) in GitLab 13.0.
......@@ -19,7 +19,7 @@ Using local storage (the default) on clustered deployments of GitLab will result
a split state across nodes, making subsequent executions of Terraform inconsistent.
You are highly advised to use a remote storage resource in that case.
The GitLab managed Terraform state backend can store your Terraform state easily and
The GitLab-managed Terraform state backend can store your Terraform state easily and
securely, and spares you from setting up additional remote resources like
Amazon S3 or Google Cloud Storage. Its features include:
......@@ -216,7 +216,7 @@ recommends encrypting plan output or modifying the project visibility settings.
See [this reference project](https://gitlab.com/gitlab-org/configure/examples/gitlab-terraform-aws) using GitLab and Terraform to deploy a basic AWS EC2 in a custom VPC.
## Using a GitLab managed Terraform state backend as a remote data source
## Using a GitLab-managed Terraform state backend as a remote data source
You can use a GitLab-managed Terraform state as a
[Terraform data source](https://www.terraform.io/docs/language/state/remote-state-data.html).
......@@ -260,13 +260,13 @@ using `data.terraform_remote_state.example.outputs.<OUTPUT-NAME>`.
You need at least the Developer role in the target project
to read the Terraform state.
## Migrating to GitLab Managed Terraform state
## Migrating to GitLab-managed Terraform state
Terraform supports copying the state when the backend is changed or
reconfigured. This can be useful if you need to migrate from another backend to
GitLab managed Terraform state. Using a local terminal is recommended to run the commands needed for migrating to GitLab Managed Terraform state.
GitLab-managed Terraform state. Using a local terminal is recommended to run the commands needed for migrating to GitLab-managed Terraform state.
The following example demonstrates how to change the state name, the same workflow is needed to migrate to GitLab Managed Terraform state from a different state storage backend.
The following example demonstrates how to change the state name, the same workflow is needed to migrate to GitLab-managed Terraform state from a different state storage backend.
### Setting up the initial backend
......
import Vue from 'vue';
import VueRouter from 'vue-router';
import VueApollo from 'vue-apollo';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import BlobButtonGroup from '~/repository/components/blob_button_group.vue';
......@@ -19,6 +21,7 @@ import {
jest.mock('~/lib/utils/common_utils');
Vue.use(VueRouter);
const router = new VueRouter();
const mockAxios = new MockAdapter(axios);
let wrapper;
let mockResolver;
......@@ -79,6 +82,7 @@ describe('Blob content viewer component', () => {
afterEach(() => {
wrapper.destroy();
mockAxios.reset();
});
describe('BlobHeader action slot', () => {
......
# frozen_string_literal: true
require 'fast_spec_helper'
RSpec.describe Gitlab::Console do
describe '.welcome!' do
context 'when running in the Rails console' do
before do
allow(Gitlab::Runtime).to receive(:console?).and_return(true)
allow(Gitlab::Geo).to receive(:enabled?).and_return(true)
allow(Gitlab::Metrics::BootTimeTracker.instance).to receive(:startup_time).and_return(42)
end
it 'prints a welcome message' do
expect($stdout).to receive(:puts).ordered.with(include("--"))
expect($stdout).to receive(:puts).ordered.with(include("Ruby:"))
expect($stdout).to receive(:puts).ordered.with(include("GitLab:"))
expect($stdout).to receive(:puts).ordered.with(include("GitLab Shell:"))
expect($stdout).to receive(:puts).ordered.with(include("PostgreSQL:"))
expect($stdout).to receive(:puts).ordered.with(include("Geo enabled:"))
expect($stdout).to receive(:puts).ordered.with(include("Geo server:"))
expect($stdout).to receive(:puts).ordered.with(include("--"))
expect($stdout).not_to receive(:puts).ordered
described_class.welcome!
end
end
end
end
# frozen_string_literal: true
# rubocop:disable Rails/Output
module Gitlab
module Console
class << self
def welcome!
return unless Gitlab::Runtime.console?
# note that this will not print out when using `spring`
justify = 15
puts '-' * 80
puts " Ruby:".ljust(justify) + RUBY_DESCRIPTION
puts " GitLab:".ljust(justify) + "#{Gitlab::VERSION} (#{Gitlab.revision}) #{Gitlab.ee? ? 'EE' : 'FOSS'}"
puts " GitLab Shell:".ljust(justify) + "#{Gitlab::VersionInfo.parse(Gitlab::Shell.version)}"
if ApplicationRecord.database.exists?
puts " #{ApplicationRecord.database.human_adapter_name}:".ljust(justify) + ApplicationRecord.database.version
Gitlab.ee do
if Gitlab::Geo.connected? && Gitlab::Geo.enabled?
puts " Geo enabled:".ljust(justify) + 'yes'
puts " Geo server:".ljust(justify) + EE::GeoHelper.current_node_human_status
end
end
end
if RUBY_PLATFORM.include?('darwin')
# Sorry, macOS users. The current implementation requires procfs.
puts '-' * 80
else
boot_time_seconds = Gitlab::Metrics::BootTimeTracker.instance.startup_time
booted_in = "[ booted in %.2fs ]" % [boot_time_seconds]
puts '-' * (80 - booted_in.length) + booted_in
end
end
end
end
end
# rubocop:enable Rails/Output
# frozen_string_literal: true
module Gitlab
module Metrics
class BootTimeTracker
include Singleton
SUPPORTED_RUNTIMES = [:puma, :sidekiq, :console].freeze
def startup_time
@startup_time || 0
end
def track_boot_time!(logger: Gitlab::AppJsonLogger)
return if @startup_time
runtime = Gitlab::Runtime.safe_identify
return unless SUPPORTED_RUNTIMES.include?(runtime) && Feature.enabled?(:track_application_boot_time, default_enabled: :yaml)
@startup_time = Gitlab::Metrics::System.process_runtime_elapsed_seconds
Gitlab::Metrics.gauge(
:gitlab_rails_boot_time_seconds, 'Time elapsed for Rails primary process to finish startup'
).set({}, @startup_time)
logger.info(message: 'Application boot finished', runtime: runtime.to_s, duration_s: @startup_time)
end
def reset!
@startup_time = nil
end
end
end
end
......@@ -7,6 +7,9 @@ module Gitlab
# This module relies on the /proc filesystem being available. If /proc is
# not available the methods of this module will be stubbed.
module System
extend self
PROC_STAT_PATH = '/proc/self/stat'
PROC_STATUS_PATH = '/proc/self/status'
PROC_SMAPS_ROLLUP_PATH = '/proc/self/smaps_rollup'
PROC_LIMITS_PATH = '/proc/self/limits'
......@@ -17,7 +20,7 @@ module Gitlab
RSS_PATTERN = /VmRSS:\s+(?<value>\d+)/.freeze
MAX_OPEN_FILES_PATTERN = /Max open files\s*(?<value>\d+)/.freeze
def self.summary
def summary
proportional_mem = memory_usage_uss_pss
{
version: RUBY_DESCRIPTION,
......@@ -32,43 +35,43 @@ module Gitlab
end
# Returns the current process' RSS (resident set size) in bytes.
def self.memory_usage_rss
def memory_usage_rss
sum_matches(PROC_STATUS_PATH, rss: RSS_PATTERN)[:rss].kilobytes
end
# Returns the current process' USS/PSS (unique/proportional set size) in bytes.
def self.memory_usage_uss_pss
def memory_usage_uss_pss
sum_matches(PROC_SMAPS_ROLLUP_PATH, uss: PRIVATE_PAGES_PATTERN, pss: PSS_PATTERN)
.transform_values(&:kilobytes)
end
def self.file_descriptor_count
def file_descriptor_count
Dir.glob(PROC_FD_GLOB).length
end
def self.max_open_file_descriptors
def max_open_file_descriptors
sum_matches(PROC_LIMITS_PATH, max_fds: MAX_OPEN_FILES_PATTERN)[:max_fds]
end
def self.cpu_time
def cpu_time
Process.clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID, :float_second)
end
# Returns the current real time in a given precision.
#
# Returns the time as a Float for precision = :float_second.
def self.real_time(precision = :float_second)
def real_time(precision = :float_second)
Process.clock_gettime(Process::CLOCK_REALTIME, precision)
end
# Returns the current monotonic clock time as seconds with microseconds precision.
#
# Returns the time as a Float.
def self.monotonic_time
def monotonic_time
Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_second)
end
def self.thread_cpu_time
def thread_cpu_time
# Not all OS kernels are supporting `Process::CLOCK_THREAD_CPUTIME_ID`
# Refer: https://gitlab.com/gitlab-org/gitlab/issues/30567#note_221765627
return unless defined?(Process::CLOCK_THREAD_CPUTIME_ID)
......@@ -76,33 +79,67 @@ module Gitlab
Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID, :float_second)
end
def self.thread_cpu_duration(start_time)
def thread_cpu_duration(start_time)
end_time = thread_cpu_time
return unless start_time && end_time
end_time - start_time
end
# Returns the total time the current process has been running in seconds.
def process_runtime_elapsed_seconds
# Entry 22 (1-indexed) contains the process `starttime`, see:
# https://man7.org/linux/man-pages/man5/proc.5.html
#
# This value is a fixed timestamp in clock ticks.
# To obtain an elapsed time in seconds, we divide by the number
# of ticks per second and subtract from the system uptime.
start_time_ticks = proc_stat_entries[21].to_f
clock_ticks_per_second = Etc.sysconf(Etc::SC_CLK_TCK)
uptime - (start_time_ticks / clock_ticks_per_second)
end
private
# Given a path to a file in /proc and a hash of (metric, pattern) pairs,
# sums up all values found for those patterns under the respective metric.
def self.sum_matches(proc_file, **patterns)
def sum_matches(proc_file, **patterns)
results = patterns.transform_values { 0 }
begin
File.foreach(proc_file) do |line|
safe_yield_procfile(proc_file) do |io|
io.each_line do |line|
patterns.each do |metric, pattern|
match = line.match(pattern)
value = match&.named_captures&.fetch('value', 0)
results[metric] += value.to_i
end
end
rescue Errno::ENOENT
# This means the procfile we're reading from did not exist;
# this is safe to ignore, since we initialize each metric to 0
end
results
end
def proc_stat_entries
safe_yield_procfile(PROC_STAT_PATH) do |io|
io.read.split(' ')
end || []
end
def safe_yield_procfile(path, &block)
File.open(path, &block)
rescue Errno::ENOENT
# This means the procfile we're reading from did not exist;
# most likely we're on Darwin.
end
# Equivalent to reading /proc/uptime on Linux 2.6+.
#
# Returns 0 if not supported, e.g. on Darwin.
def uptime
Process.clock_gettime(Process::CLOCK_BOOTTIME)
rescue NameError
0
end
end
end
end
......@@ -31,6 +31,12 @@ module Gitlab
end
end
def safe_identify
identify
rescue UnknownProcessError, AmbiguousProcessError
nil
end
def puma?
!!defined?(::Puma)
end
......
{
"type": "object",
"required": ["id", "name", "username", "avatar_url", "web_url", "blocked", "two_factor_enabled"],
"required": ["id", "name", "username", "avatar_url", "web_url", "blocked", "two_factor_enabled", "show_status"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
......@@ -17,6 +17,7 @@
"emoji": { "type": "string" }
},
"additionalProperties": false
}
},
"show_status": { "type": "boolean" }
}
}
import { GlAvatarLink, GlBadge } from '@gitlab/ui';
import { within } from '@testing-library/dom';
import { mount, createWrapper } from '@vue/test-utils';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import UserAvatar from '~/members/components/avatars/user_avatar.vue';
import { AVAILABILITY_STATUS } from '~/set_status_modal/utils';
import { member as memberMock, member2faEnabled, orphanedMember } from '../../mock_data';
describe('UserAvatar', () => {
......@@ -10,7 +11,7 @@ describe('UserAvatar', () => {
const { user } = memberMock;
const createComponent = (propsData = {}, provide = {}) => {
wrapper = mount(UserAvatar, {
wrapper = mountExtended(UserAvatar, {
propsData: {
member: memberMock,
isCurrentUser: false,
......@@ -23,9 +24,6 @@ describe('UserAvatar', () => {
});
};
const getByText = (text, options) =>
createWrapper(within(wrapper.element).findByText(text, options));
const findStatusEmoji = (emoji) => wrapper.find(`gl-emoji[data-name="${emoji}"]`);
afterEach(() => {
......@@ -48,13 +46,13 @@ describe('UserAvatar', () => {
it("renders user's name", () => {
createComponent();
expect(getByText(user.name).exists()).toBe(true);
expect(wrapper.findByText(user.name).exists()).toBe(true);
});
it("renders user's username", () => {
createComponent();
expect(getByText(`@${user.username}`).exists()).toBe(true);
expect(wrapper.findByText(`@${user.username}`).exists()).toBe(true);
});
it("renders user's avatar", () => {
......@@ -67,7 +65,7 @@ describe('UserAvatar', () => {
it('displays an orphaned user', () => {
createComponent({ member: orphanedMember });
expect(getByText('Orphaned member').exists()).toBe(true);
expect(wrapper.findByText('Orphaned member').exists()).toBe(true);
});
});
......@@ -85,13 +83,13 @@ describe('UserAvatar', () => {
it('renders the "It\'s you" badge when member is current user', () => {
createComponent({ isCurrentUser: true });
expect(getByText("It's you").exists()).toBe(true);
expect(wrapper.findByText("It's you").exists()).toBe(true);
});
it('does not render 2FA badge when `canManageMembers` is `false`', () => {
createComponent({ member: member2faEnabled }, { canManageMembers: false });
expect(within(wrapper.element).queryByText('2FA')).toBe(null);
expect(wrapper.findByText('2FA').exists()).toBe(false);
});
});
......@@ -112,6 +110,23 @@ describe('UserAvatar', () => {
expect(findStatusEmoji(emoji).exists()).toBe(true);
});
describe('when `user.showStatus` is `false', () => {
it('does not display status emoji', () => {
createComponent({
member: {
...memberMock,
user: {
...memberMock.user,
showStatus: false,
status: { emoji, messageHtml: 'On vacation' },
},
},
});
expect(findStatusEmoji(emoji).exists()).toBe(false);
});
});
});
describe('when not set', () => {
......@@ -122,4 +137,30 @@ describe('UserAvatar', () => {
});
});
});
describe('user availability', () => {
describe('when `user.availability` is `null`', () => {
it("does not show `(Busy)` next to user's name", () => {
createComponent();
expect(wrapper.findByText('(Busy)').exists()).toBe(false);
});
});
describe(`when user.availability is ${AVAILABILITY_STATUS.BUSY}`, () => {
it("shows `(Busy)` next to user's name", () => {
createComponent({
member: {
...memberMock,
user: {
...memberMock.user,
availability: AVAILABILITY_STATUS.BUSY,
},
},
});
expect(wrapper.findByText('(Busy)').exists()).toBe(true);
});
});
});
});
......@@ -25,6 +25,8 @@ export const member = {
twoFactorEnabled: false,
oncallSchedules: [{ name: 'schedule 1' }],
escalationPolicies: [{ name: 'policy 1' }],
availability: null,
showStatus: true,
},
id: 238,
createdAt: '2020-07-17T16:22:46.923Z',
......
......@@ -3,7 +3,6 @@ import { mount, shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
......@@ -13,7 +12,7 @@ import BlobButtonGroup from '~/repository/components/blob_button_group.vue';
import BlobContentViewer from '~/repository/components/blob_content_viewer.vue';
import BlobEdit from '~/repository/components/blob_edit.vue';
import ForkSuggestion from '~/repository/components/fork_suggestion.vue';
import { loadViewer, viewerProps } from '~/repository/components/blob_viewers';
import { loadViewer } from '~/repository/components/blob_viewers';
import DownloadViewer from '~/repository/components/blob_viewers/download_viewer.vue';
import EmptyViewer from '~/repository/components/blob_viewers/empty_viewer.vue';
import SourceViewer from '~/vue_shared/components/source_viewer.vue';
......@@ -51,6 +50,7 @@ const createComponent = async (mockData = {}, mountFn = shallowMount) => {
createMergeRequestIn = userPermissionsMock.createMergeRequestIn,
isBinary,
inject = {},
highlightJs = true,
} = mockData;
const project = {
......@@ -78,7 +78,12 @@ const createComponent = async (mockData = {}, mountFn = shallowMount) => {
apolloProvider: fakeApollo,
propsData: propsMock,
mixins: [{ data: () => ({ ref: refMock }) }],
provide: { ...inject },
provide: {
...inject,
glFeatures: {
highlightJs,
},
},
}),
);
......@@ -99,7 +104,6 @@ describe('Blob content viewer component', () => {
const findForkSuggestion = () => wrapper.findComponent(ForkSuggestion);
beforeEach(() => {
gon.features = { highlightJs: true };
isLoggedIn.mockReturnValue(true);
});
......@@ -137,6 +141,15 @@ describe('Blob content viewer component', () => {
});
describe('legacy viewers', () => {
it('loads a legacy viewer when a the fileType is text and the highlightJs feature is turned off', async () => {
await createComponent({
blob: { ...simpleViewerMock, fileType: 'text', highlightJs: false },
});
expect(mockAxios.history.get).toHaveLength(1);
expect(mockAxios.history.get[0].url).toEqual('some_file.js?format=json&viewer=simple');
});
it('loads a legacy viewer when a viewer component is not available', async () => {
await createComponent({ blob: { ...simpleViewerMock, fileType: 'unknown' } });
......@@ -202,7 +215,6 @@ describe('Blob content viewer component', () => {
describe('Blob viewer', () => {
afterEach(() => {
loadViewer.mockRestore();
viewerProps.mockRestore();
});
it('does not render a BlobContent component if a Blob viewer is available', async () => {
......@@ -213,33 +225,29 @@ describe('Blob content viewer component', () => {
});
it.each`
viewer | loadViewerReturnValue | viewerPropsReturnValue
${'empty'} | ${EmptyViewer} | ${{}}
${'download'} | ${DownloadViewer} | ${{ filePath: '/some/file/path', fileName: 'test.js', fileSize: 100 }}
${'text'} | ${SourceViewer} | ${{ content: 'test', autoDetect: true }}
`(
'renders viewer component for $viewer files',
async ({ viewer, loadViewerReturnValue, viewerPropsReturnValue }) => {
loadViewer.mockReturnValue(loadViewerReturnValue);
viewerProps.mockReturnValue(viewerPropsReturnValue);
createComponent({
blob: {
...simpleViewerMock,
fileType: 'null',
simpleViewer: {
...simpleViewerMock.simpleViewer,
fileType: viewer,
},
viewer | loadViewerReturnValue
${'empty'} | ${EmptyViewer}
${'download'} | ${DownloadViewer}
${'text'} | ${SourceViewer}
`('renders viewer component for $viewer files', async ({ viewer, loadViewerReturnValue }) => {
loadViewer.mockReturnValue(loadViewerReturnValue);
createComponent({
blob: {
...simpleViewerMock,
fileType: 'null',
simpleViewer: {
...simpleViewerMock.simpleViewer,
fileType: viewer,
},
});
},
});
await waitForPromises();
await waitForPromises();
expect(loadViewer).toHaveBeenCalledWith(viewer, false);
expect(wrapper.findComponent(loadViewerReturnValue).exists()).toBe(true);
},
);
expect(loadViewer).toHaveBeenCalledWith(viewer, false);
expect(wrapper.findComponent(loadViewerReturnValue).exists()).toBe(true);
});
});
describe('BlobHeader action slot', () => {
......
......@@ -6,42 +6,33 @@ import DownloadViewer from '~/repository/components/blob_viewers/download_viewer
describe('Text Viewer', () => {
let wrapper;
const DEFAULT_PROPS = {
fileName: 'file_name.js',
filePath: '/some/file/path',
fileSize: 2269674,
const DEFAULT_BLOB_DATA = {
name: 'file_name.js',
rawPath: '/some/file/path',
rawSize: 2269674,
};
const createComponent = (props = {}) => {
const createComponent = (blobData = {}) => {
wrapper = shallowMount(DownloadViewer, {
propsData: {
...DEFAULT_PROPS,
...props,
blob: {
...DEFAULT_BLOB_DATA,
...blobData,
},
},
});
};
it('renders component', () => {
createComponent();
const { fileName, filePath, fileSize } = DEFAULT_PROPS;
expect(wrapper.props()).toMatchObject({
fileName,
filePath,
fileSize,
});
});
it('renders download human readable file size text', () => {
createComponent();
const downloadText = `Download (${numberToHumanSize(DEFAULT_PROPS.fileSize)})`;
const downloadText = `Download (${numberToHumanSize(DEFAULT_BLOB_DATA.rawSize)})`;
expect(wrapper.text()).toBe(downloadText);
});
it('renders download text', () => {
createComponent({
fileSize: 0,
rawSize: 0,
});
expect(wrapper.text()).toBe('Download');
......@@ -49,13 +40,13 @@ describe('Text Viewer', () => {
it('renders download link', () => {
createComponent();
const { filePath, fileName } = DEFAULT_PROPS;
const { rawPath, name } = DEFAULT_BLOB_DATA;
expect(wrapper.findComponent(GlLink).attributes()).toMatchObject({
rel: 'nofollow',
target: '_blank',
href: filePath,
download: fileName,
href: rawPath,
download: name,
});
});
......
......@@ -4,13 +4,13 @@ import ImageViewer from '~/repository/components/blob_viewers/image_viewer.vue';
describe('Image Viewer', () => {
let wrapper;
const propsData = {
url: 'some/image.png',
alt: 'image.png',
const DEFAULT_BLOB_DATA = {
rawPath: 'some/image.png',
name: 'image.png',
};
const createComponent = () => {
wrapper = shallowMount(ImageViewer, { propsData });
wrapper = shallowMount(ImageViewer, { propsData: { blob: DEFAULT_BLOB_DATA } });
};
const findImage = () => wrapper.find('[data-testid="image"]');
......@@ -19,7 +19,7 @@ describe('Image Viewer', () => {
createComponent();
expect(findImage().exists()).toBe(true);
expect(findImage().attributes('src')).toBe(propsData.url);
expect(findImage().attributes('alt')).toBe(propsData.alt);
expect(findImage().attributes('src')).toBe(DEFAULT_BLOB_DATA.rawPath);
expect(findImage().attributes('alt')).toBe(DEFAULT_BLOB_DATA.name);
});
});
......@@ -5,14 +5,14 @@ import LfsViewer from '~/repository/components/blob_viewers/lfs_viewer.vue';
describe('LFS Viewer', () => {
let wrapper;
const DEFAULT_PROPS = {
fileName: 'file_name.js',
filePath: '/some/file/path',
const DEFAULT_BLOB_DATA = {
name: 'file_name.js',
rawPath: '/some/file/path',
};
const createComponent = () => {
wrapper = shallowMount(LfsViewer, {
propsData: { ...DEFAULT_PROPS },
propsData: { blob: { ...DEFAULT_BLOB_DATA } },
stubs: { GlSprintf },
});
};
......@@ -30,12 +30,12 @@ describe('LFS Viewer', () => {
});
it('renders download link', () => {
const { filePath, fileName } = DEFAULT_PROPS;
const { rawPath, name } = DEFAULT_BLOB_DATA;
expect(findLink().attributes()).toMatchObject({
target: '_blank',
href: filePath,
download: fileName,
href: rawPath,
download: name,
});
});
});
......@@ -6,10 +6,12 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
describe('PDF Viewer', () => {
let wrapper;
const defaultPropsData = { url: 'some/pdf_blob.pdf' };
const DEFAULT_BLOB_DATA = { rawPath: 'some/pdf_blob.pdf' };
const createComponent = (fileSize = 999) => {
wrapper = shallowMountExtended(Component, { propsData: { ...defaultPropsData, fileSize } });
const createComponent = (rawSize = 999) => {
wrapper = shallowMountExtended(Component, {
propsData: { blob: { ...DEFAULT_BLOB_DATA, rawSize } },
});
};
const findPDFViewer = () => wrapper.findComponent(PdfViewer);
......@@ -20,7 +22,7 @@ describe('PDF Viewer', () => {
createComponent();
expect(findPDFViewer().exists()).toBe(true);
expect(findPDFViewer().props('pdf')).toBe(defaultPropsData.url);
expect(findPDFViewer().props('pdf')).toBe(DEFAULT_BLOB_DATA.rawPath);
});
describe('Too large', () => {
......
......@@ -4,10 +4,10 @@ import VideoViewer from '~/repository/components/blob_viewers/video_viewer.vue';
describe('Video Viewer', () => {
let wrapper;
const propsData = { url: 'some/video.mp4' };
const DEFAULT_BLOB_DATA = { rawPath: 'some/video.mp4' };
const createComponent = () => {
wrapper = shallowMountExtended(VideoViewer, { propsData });
wrapper = shallowMountExtended(VideoViewer, { propsData: { blob: { ...DEFAULT_BLOB_DATA } } });
};
const findVideo = () => wrapper.findByTestId('video');
......@@ -16,7 +16,7 @@ describe('Video Viewer', () => {
createComponent();
expect(findVideo().exists()).toBe(true);
expect(findVideo().attributes('src')).toBe(propsData.url);
expect(findVideo().attributes('src')).toBe(DEFAULT_BLOB_DATA.rawPath);
expect(findVideo().attributes('controls')).not.toBeUndefined();
});
});
......@@ -12,17 +12,18 @@ const router = new VueRouter();
describe('Source Viewer component', () => {
let wrapper;
const language = 'javascript';
const content = `// Some source code`;
const DEFAULT_BLOB_DATA = { language, rawTextBlob: content };
const highlightedContent = `<span data-testid='test-highlighted' id='LC1'>${content}</span><span id='LC2'></span>`;
const language = 'javascript';
hljs.highlight.mockImplementation(() => ({ value: highlightedContent }));
hljs.highlightAuto.mockImplementation(() => ({ value: highlightedContent }));
const createComponent = async (props = {}) => {
const createComponent = async (props = { autoDetect: false }) => {
wrapper = shallowMountExtended(SourceViewer, {
router,
propsData: { content, language, ...props },
propsData: { blob: { ...DEFAULT_BLOB_DATA }, ...props },
});
await waitForPromises();
};
......
# frozen_string_literal: true
require 'fast_spec_helper'
RSpec.describe Gitlab::Console do
describe '.welcome!' do
context 'when running in the Rails console' do
before do
allow(Gitlab::Runtime).to receive(:console?).and_return(true)
allow(Gitlab::Metrics::BootTimeTracker.instance).to receive(:startup_time).and_return(42)
end
shared_examples 'console messages' do
it 'prints system info' do
expect($stdout).to receive(:puts).ordered.with(include("--"))
expect($stdout).to receive(:puts).ordered.with(include("Ruby:"))
expect($stdout).to receive(:puts).ordered.with(include("GitLab:"))
expect($stdout).to receive(:puts).ordered.with(include("GitLab Shell:"))
expect($stdout).to receive(:puts).ordered.with(include("PostgreSQL:"))
expect($stdout).to receive(:puts).ordered.with(include("--"))
expect($stdout).not_to receive(:puts).ordered
described_class.welcome!
end
end
# This is to add line coverage, not to actually verify behavior on macOS.
context 'on darwin' do
before do
stub_const('RUBY_PLATFORM', 'x86_64-darwin-19')
end
it_behaves_like 'console messages'
end
it_behaves_like 'console messages'
end
context 'when not running in the Rails console' do
before do
allow(Gitlab::Runtime).to receive(:console?).and_return(false)
end
it 'does not print anything' do
expect($stdout).not_to receive(:puts)
described_class.welcome!
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Metrics::BootTimeTracker do
let(:logger) { double('logger') }
let(:gauge) { double('gauge') }
subject(:tracker) { described_class.instance }
before do
described_class.instance.reset!
allow(logger).to receive(:info)
allow(gauge).to receive(:set)
allow(Gitlab::Metrics).to receive(:gauge).and_return(gauge)
end
describe '#track_boot_time!' do
described_class::SUPPORTED_RUNTIMES.each do |runtime|
context "when called on #{runtime} for the first time" do
before do
expect(Gitlab::Runtime).to receive(:safe_identify).and_return(runtime)
end
it 'set the startup_time' do
tracker.track_boot_time!(logger: logger)
expect(tracker.startup_time).to be > 0
end
it 'records the current process runtime' do
expect(Gitlab::Metrics::System).to receive(:process_runtime_elapsed_seconds).once
tracker.track_boot_time!(logger: logger)
end
it 'logs the application boot time' do
expect(Gitlab::Metrics::System).to receive(:process_runtime_elapsed_seconds).and_return(42)
expect(logger).to receive(:info).with(message: 'Application boot finished', runtime: runtime.to_s, duration_s: 42)
tracker.track_boot_time!(logger: logger)
end
it 'tracks boot time in a prometheus gauge' do
expect(Gitlab::Metrics::System).to receive(:process_runtime_elapsed_seconds).and_return(42)
expect(gauge).to receive(:set).with({}, 42)
tracker.track_boot_time!(logger: logger)
end
context 'on subsequent calls' do
it 'does nothing' do
tracker.track_boot_time!(logger: logger)
expect(Gitlab::Metrics::System).not_to receive(:process_runtime_elapsed_seconds)
expect(logger).not_to receive(:info)
expect(gauge).not_to receive(:set)
tracker.track_boot_time!(logger: logger)
end
end
end
end
context 'when called on other runtimes' do
it 'does nothing' do
tracker.track_boot_time!(logger: logger)
expect(Gitlab::Metrics::System).not_to receive(:process_runtime_elapsed_seconds)
expect(logger).not_to receive(:info)
expect(gauge).not_to receive(:set)
tracker.track_boot_time!(logger: logger)
end
end
# TODO: When https://gitlab.com/gitlab-org/gitlab/-/issues/351769 is closed,
# revert to using fast_spec_helper again.
context 'when feature flag is off' do
it 'does nothing' do
stub_feature_flags(track_application_boot_time: false)
expect(Gitlab::Metrics::System).not_to receive(:process_runtime_elapsed_seconds)
expect(logger).not_to receive(:info)
expect(gauge).not_to receive(:set)
tracker.track_boot_time!(logger: logger)
end
end
end
describe '#startup_time' do
it 'returns 0 when boot time not tracked' do
expect(tracker.startup_time).to eq(0)
end
end
end
......@@ -4,6 +4,13 @@ require 'spec_helper'
RSpec.describe Gitlab::Metrics::System do
context 'when /proc files exist' do
# Modified column 22 to be 1000 (starttime ticks)
let(:proc_stat) do
<<~SNIP
2095 (ruby) R 0 2095 2095 34818 2095 4194560 211267 7897 2 0 287 51 10 1 20 0 5 0 1000 566210560 80885 18446744073709551615 94736211292160 94736211292813 140720919612064 0 0 0 0 0 1107394127 0 0 0 17 3 0 0 0 0 0 94736211303768 94736211304544 94736226689024 140720919619473 140720919619513 140720919619513 140720919621604 0
SNIP
end
# Fixtures pulled from:
# Linux carbon 5.3.0-7648-generic #41~1586789791~19.10~9593806-Ubuntu SMP Mon Apr 13 17:50:40 UTC x86_64 x86_64 x86_64 GNU/Linux
let(:proc_status) do
......@@ -97,6 +104,29 @@ RSpec.describe Gitlab::Metrics::System do
end
end
describe '.process_runtime_elapsed_seconds' do
it 'returns the seconds elapsed since the process was started' do
# sets process starttime ticks to 1000
mock_existing_proc_file('/proc/self/stat', proc_stat)
# system clock ticks/sec
expect(Etc).to receive(:sysconf).with(Etc::SC_CLK_TCK).and_return(100)
# system uptime in seconds
expect(::Process).to receive(:clock_gettime).and_return(15)
# uptime - (starttime_ticks / ticks_per_sec)
expect(described_class.process_runtime_elapsed_seconds).to eq(5)
end
context 'when inputs are not available' do
it 'returns 0' do
mock_missing_proc_file
expect(::Process).to receive(:clock_gettime).and_raise(NameError)
expect(described_class.process_runtime_elapsed_seconds).to eq(0)
end
end
end
describe '.summary' do
it 'contains a selection of the available fields' do
stub_const('RUBY_DESCRIPTION', 'ruby-3.0-patch1')
......@@ -223,10 +253,10 @@ RSpec.describe Gitlab::Metrics::System do
end
def mock_existing_proc_file(path, content)
allow(File).to receive(:foreach).with(path) { |_path, &block| content.each_line(&block) }
allow(File).to receive(:open).with(path) { |_path, &block| block.call(StringIO.new(content)) }
end
def mock_missing_proc_file
allow(File).to receive(:foreach).and_raise(Errno::ENOENT)
allow(File).to receive(:open).and_raise(Errno::ENOENT)
end
end
......@@ -26,8 +26,16 @@ RSpec.describe Gitlab::Runtime do
end
context "when unknown" do
it "raises an exception when trying to identify" do
expect { subject.identify }.to raise_error(subject::UnknownProcessError)
describe '.identify' do
it "raises an exception when trying to identify" do
expect { subject.identify }.to raise_error(subject::UnknownProcessError)
end
end
describe '.safe_identify' do
it "returns nil" do
expect(subject.safe_identify).to be_nil
end
end
end
......@@ -37,8 +45,16 @@ RSpec.describe Gitlab::Runtime do
stub_const('::Rails::Console', double)
end
it "raises an exception when trying to identify" do
expect { subject.identify }.to raise_error(subject::AmbiguousProcessError)
describe '.identify' do
it "raises an exception when trying to identify" do
expect { subject.identify }.to raise_error(subject::AmbiguousProcessError)
end
end
describe '.safe_identify' do
it "returns nil" do
expect(subject.safe_identify).to be_nil
end
end
end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment