Commit ccc5f473 authored by Tiago Botelho's avatar Tiago Botelho Committed by Rémy Coutable

Adds Kubernetes Pod Logs to GitLab

parent d1057461
...@@ -107,6 +107,7 @@ export default { ...@@ -107,6 +107,7 @@ export default {
:deploy-board-data="model.deployBoardData" :deploy-board-data="model.deployBoardData"
:is-loading="model.isLoadingDeployBoard" :is-loading="model.isLoadingDeployBoard"
:is-empty="model.isEmptyDeployBoard" :is-empty="model.isEmptyDeployBoard"
:logs-path="model.logs_path"
/> />
</div> </div>
</div> </div>
......
...@@ -6,9 +6,12 @@ import { visitUrl } from './lib/utils/url_utility'; ...@@ -6,9 +6,12 @@ import { visitUrl } from './lib/utils/url_utility';
import bp from './breakpoints'; import bp from './breakpoints';
import { numberToHumanSize } from './lib/utils/number_utils'; import { numberToHumanSize } from './lib/utils/number_utils';
import { setCiStatusFavicon } from './lib/utils/common_utils'; import { setCiStatusFavicon } from './lib/utils/common_utils';
import { isScrolledToBottom, scrollDown } from './lib/utils/scroll_utils';
import LogOutputBehaviours from './lib/utils/logoutput_behaviours';
export default class Job { export default class Job extends LogOutputBehaviours {
constructor(options) { constructor(options) {
super();
this.timeout = null; this.timeout = null;
this.state = null; this.state = null;
this.fetchingStatusFavicon = false; this.fetchingStatusFavicon = false;
...@@ -29,10 +32,6 @@ export default class Job { ...@@ -29,10 +32,6 @@ export default class Job {
this.$buildTraceOutput = $('.js-build-output'); this.$buildTraceOutput = $('.js-build-output');
this.$topBar = $('.js-top-bar'); this.$topBar = $('.js-top-bar');
// Scroll controllers
this.$scrollTopBtn = $('.js-scroll-up');
this.$scrollBottomBtn = $('.js-scroll-down');
clearTimeout(this.timeout); clearTimeout(this.timeout);
this.initSidebar(); this.initSidebar();
...@@ -48,23 +47,14 @@ export default class Job { ...@@ -48,23 +47,14 @@ export default class Job {
.off('click', '.stage-item') .off('click', '.stage-item')
.on('click', '.stage-item', this.updateDropdown); .on('click', '.stage-item', this.updateDropdown);
// add event listeners to the scroll buttons
this.$scrollTopBtn
.off('click')
.on('click', this.scrollToTop.bind(this));
this.$scrollBottomBtn
.off('click')
.on('click', this.scrollToBottom.bind(this));
this.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100); this.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100);
this.$window this.$window
.off('scroll') .off('scroll')
.on('scroll', () => { .on('scroll', () => {
if (!this.isScrolledToBottom()) { if (!isScrolledToBottom()) {
this.toggleScrollAnimation(false); this.toggleScrollAnimation(false);
} else if (this.isScrolledToBottom() && !this.isLogComplete) { } else if (isScrolledToBottom() && !this.isLogComplete) {
this.toggleScrollAnimation(true); this.toggleScrollAnimation(true);
} }
this.scrollThrottled(); this.scrollThrottled();
...@@ -90,60 +80,8 @@ export default class Job { ...@@ -90,60 +80,8 @@ export default class Job {
StickyFill.add(this.$topBar); StickyFill.add(this.$topBar);
} }
// eslint-disable-next-line class-methods-use-this
canScroll() {
return $(document).height() > $(window).height();
}
toggleScroll() {
const $document = $(document);
const currentPosition = $document.scrollTop();
const scrollHeight = $document.height();
const windowHeight = $(window).height();
if (this.canScroll()) {
if (currentPosition > 0 &&
(scrollHeight - currentPosition !== windowHeight)) {
// User is in the middle of the log
this.toggleDisableButton(this.$scrollTopBtn, false);
this.toggleDisableButton(this.$scrollBottomBtn, false);
} else if (currentPosition === 0) {
// User is at Top of Log
this.toggleDisableButton(this.$scrollTopBtn, true);
this.toggleDisableButton(this.$scrollBottomBtn, false);
} else if (this.isScrolledToBottom()) {
// User is at the bottom of the build log.
this.toggleDisableButton(this.$scrollTopBtn, false);
this.toggleDisableButton(this.$scrollBottomBtn, true);
}
} else {
this.toggleDisableButton(this.$scrollTopBtn, true);
this.toggleDisableButton(this.$scrollBottomBtn, true);
}
}
// eslint-disable-next-line class-methods-use-this
isScrolledToBottom() {
const $document = $(document);
const currentPosition = $document.scrollTop();
const scrollHeight = $document.height();
const windowHeight = $(window).height();
return scrollHeight - currentPosition === windowHeight;
}
// eslint-disable-next-line class-methods-use-this
scrollDown() {
const $document = $(document);
$document.scrollTop($document.height());
}
scrollToBottom() { scrollToBottom() {
this.scrollDown(); scrollDown();
this.hasBeenScrolled = true; this.hasBeenScrolled = true;
this.toggleScroll(); this.toggleScroll();
} }
...@@ -154,12 +92,6 @@ export default class Job { ...@@ -154,12 +92,6 @@ export default class Job {
this.toggleScroll(); this.toggleScroll();
} }
// eslint-disable-next-line class-methods-use-this
toggleDisableButton($button, disable) {
if (disable && $button.prop('disabled')) return;
$button.prop('disabled', disable);
}
toggleScrollAnimation(toggle) { toggleScrollAnimation(toggle) {
this.$scrollBottomBtn.toggleClass('animate', toggle); this.$scrollBottomBtn.toggleClass('animate', toggle);
} }
...@@ -191,7 +123,7 @@ export default class Job { ...@@ -191,7 +123,7 @@ export default class Job {
this.state = log.state; this.state = log.state;
} }
this.isScrollInBottom = this.isScrolledToBottom(); this.isScrollInBottom = isScrolledToBottom();
if (log.append) { if (log.append) {
this.$buildTraceOutput.append(log.html); this.$buildTraceOutput.append(log.html);
...@@ -231,7 +163,7 @@ export default class Job { ...@@ -231,7 +163,7 @@ export default class Job {
}) })
.then(() => { .then(() => {
if (this.isScrollInBottom) { if (this.isScrollInBottom) {
this.scrollDown(); scrollDown();
} }
}) })
.then(() => this.toggleScroll()); .then(() => this.toggleScroll());
......
import $ from 'jquery';
import { canScroll, isScrolledToBottom, toggleDisableButton } from './scroll_utils';
export default class LogOutputBehaviours {
constructor() {
// Scroll buttons
this.$scrollTopBtn = $('.js-scroll-up');
this.$scrollBottomBtn = $('.js-scroll-down');
this.$scrollTopBtn.off('click').on('click', this.scrollToTop.bind(this));
this.$scrollBottomBtn.off('click').on('click', this.scrollToBottom.bind(this));
}
toggleScroll() {
const $document = $(document);
const currentPosition = $document.scrollTop();
const scrollHeight = $document.height();
const windowHeight = $(window).height();
if (canScroll()) {
if (currentPosition > 0 && scrollHeight - currentPosition !== windowHeight) {
// User is in the middle of the log
toggleDisableButton(this.$scrollTopBtn, false);
toggleDisableButton(this.$scrollBottomBtn, false);
} else if (currentPosition === 0) {
// User is at Top of Log
toggleDisableButton(this.$scrollTopBtn, true);
toggleDisableButton(this.$scrollBottomBtn, false);
} else if (isScrolledToBottom()) {
// User is at the bottom of the build log.
toggleDisableButton(this.$scrollTopBtn, false);
toggleDisableButton(this.$scrollBottomBtn, true);
}
} else {
toggleDisableButton(this.$scrollTopBtn, true);
toggleDisableButton(this.$scrollBottomBtn, true);
}
}
toggleScrollAnimation(toggle) {
this.$scrollBottomBtn.toggleClass('animate', toggle);
}
}
import $ from 'jquery';
export const canScroll = () => $(document).height() > $(window).height();
/**
* Checks if the entire page is scrolled down all the way to the bottom
*/
export const isScrolledToBottom = () => {
const $document = $(document);
const currentPosition = $document.scrollTop();
const scrollHeight = $document.height();
const windowHeight = $(window).height();
return scrollHeight - currentPosition === windowHeight;
};
export const scrollDown = () => {
const $document = $(document);
$document.scrollTop($document.height());
};
export const toggleDisableButton = ($button, disable) => {
if (disable && $button.prop('disabled')) return;
$button.prop('disabled', disable);
};
export default {};
...@@ -125,6 +125,7 @@ ...@@ -125,6 +125,7 @@
align-items: center; align-items: center;
svg { svg {
width: 15px;
height: 15px; height: 15px;
display: block; display: block;
fill: $gl-text-color; fill: $gl-text-color;
...@@ -159,7 +160,12 @@ ...@@ -159,7 +160,12 @@
} }
} }
.btn-scroll:disabled { .btn-refresh {
border-radius: 4px;
}
.btn-scroll:disabled,
.btn-refresh:disabled {
opacity: 0.35; opacity: 0.35;
cursor: not-allowed; cursor: not-allowed;
} }
...@@ -447,3 +453,14 @@ ...@@ -447,3 +453,14 @@
right: 0; right: 0;
margin-top: -17px; margin-top: -17px;
} }
@include media-breakpoint-down(sm) {
.top-bar {
.truncated-info {
white-space: nowrap;
overflow: hidden;
max-width: 220px;
text-overflow: ellipsis;
}
}
}
...@@ -232,6 +232,13 @@ ...@@ -232,6 +232,13 @@
&-running { &-running {
background-color: $green-100; background-color: $green-100;
border-color: $green-400; border-color: $green-400;
// EE-specific start
&:hover {
background-color: $green-300;
border-color: $green-500;
}
// EE-specific end
} }
&-succeeded { &-succeeded {
......
...@@ -9,6 +9,8 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -9,6 +9,8 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :verify_api_request!, only: :terminal_websocket_authorize before_action :verify_api_request!, only: :terminal_websocket_authorize
before_action :expire_etag_cache, only: [:index] before_action :expire_etag_cache, only: [:index]
prepend ::EE::Projects::EnvironmentsController
def index def index
@environments = project.environments @environments = project.environments
.with_state(params[:scope] || :available) .with_state(params[:scope] || :available)
......
class EnvironmentEntity < Grape::Entity class EnvironmentEntity < Grape::Entity
include RequestAwareEntity include RequestAwareEntity
prepend ::EE::EnvironmentEntity
expose :id expose :id
expose :name expose :name
expose :state expose :state
......
...@@ -88,9 +88,7 @@ ...@@ -88,9 +88,7 @@
%button.js-scroll-down.btn-scroll.btn-transparent.btn-blank{ type: 'button', disabled: true } %button.js-scroll-down.btn-scroll.btn-transparent.btn-blank{ type: 'button', disabled: true }
= custom_icon('scroll_down') = custom_icon('scroll_down')
%pre.build-trace#build-trace = render 'shared/builds/build_output'
%code.bash.js-build-output
.build-loader-animation.js-build-refresh
- else - else
= render "empty_states" = render "empty_states"
......
%pre.build-trace#build-trace
%code.bash.js-build-output
.build-loader-animation.js-build-refresh
...@@ -265,6 +265,9 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -265,6 +265,9 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get :metrics get :metrics
get :additional_metrics get :additional_metrics
get '/terminal.ws/authorize', to: 'environments#terminal_websocket_authorize', constraints: { format: nil } get '/terminal.ws/authorize', to: 'environments#terminal_websocket_authorize', constraints: { format: nil }
# EE
get :logs
end end
collection do collection do
......
...@@ -32,6 +32,11 @@ ...@@ -32,6 +32,11 @@
type: Boolean, type: Boolean,
required: true, required: true,
}, },
logsPath: {
type: String,
required: false,
default: '',
},
}, },
computed: { computed: {
canRenderDeployBoard() { canRenderDeployBoard() {
...@@ -74,6 +79,8 @@ ...@@ -74,6 +79,8 @@
<instance-component <instance-component
:status="instance.status" :status="instance.status"
:tooltip-text="instance.tooltip" :tooltip-text="instance.tooltip"
:pod-name="instance.pod_name"
:logs-path="logsPath"
:stable="instance.stable" :stable="instance.stable"
:key="i" :key="i"
/> />
......
...@@ -43,6 +43,17 @@ ...@@ -43,6 +43,17 @@
required: false, required: false,
default: true, default: true,
}, },
podName: {
type: String,
required: false,
default: '',
},
logsPath: {
type: String,
required: true,
},
}, },
computed: { computed: {
...@@ -55,16 +66,21 @@ ...@@ -55,16 +66,21 @@
return cssClassName; return cssClassName;
}, },
computedLogPath() {
return `${this.logsPath}?pod_name=${this.podName}`;
},
}, },
}; };
</script> </script>
<template> <template>
<div <a
v-tooltip v-tooltip
class="deploy-board-instance" class="deploy-board-instance"
:class="cssClass" :class="cssClass"
:data-title="tooltipText" :data-title="tooltipText"
data-placement="top" data-placement="top"
:href="computedLogPath"
> >
</div> </a>
</template> </template>
import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
import { getParameterValues } from '~/lib/utils/url_utility';
import { isScrolledToBottom, scrollDown, toggleDisableButton } from '~/lib/utils/scroll_utils';
import LogOutputBehaviours from '~/lib/utils/logoutput_behaviours';
import createFlash from '~/flash';
import { __, s__, sprintf } from '~/locale';
import _ from 'underscore';
export default class KubernetesPodLogs extends LogOutputBehaviours {
constructor(container) {
super();
this.options = $(container).data();
this.podNameContainer = $(container).find('.js-pod-name');
this.podName = getParameterValues('pod_name')[0];
this.$buildOutputContainer = $(container).find('.js-build-output');
this.$window = $(window);
this.$refreshLogBtn = $(container).find('.js-refresh-log');
this.$buildRefreshAnimation = $(container).find('.js-build-refresh');
this.isLogComplete = false;
this.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100);
if (!this.podName) {
createFlash(s__('Environments|No pod name has been specified'));
return;
}
const podTitle = sprintf(
s__('Environments|Pod logs from %{podName}'),
{
podName: `<strong>${_.escape(this.podName)}</strong>`,
},
false,
);
this.podNameContainer.empty();
this.podNameContainer.append(podTitle);
this.$window.off('scroll').on('scroll', () => {
if (!isScrolledToBottom()) {
this.toggleScrollAnimation(false);
} else if (isScrolledToBottom() && !this.isLogComplete) {
this.toggleScrollAnimation(true);
}
this.scrollThrottled();
});
this.$refreshLogBtn.off('click').on('click', this.getPodLogs.bind(this));
}
scrollToBottom() {
scrollDown();
this.toggleScroll();
}
scrollToTop() {
$(document).scrollTop(0);
this.toggleScroll();
}
getPodLogs() {
this.scrollToTop();
this.$buildOutputContainer.empty();
this.$buildRefreshAnimation.show();
toggleDisableButton(this.$refreshLogBtn, 'true');
return axios
.get(this.options.logsPath, {
params: { pod_name: this.podName },
})
.then(res => {
const logs = res.data.logs;
const formattedLogs = logs.map(logEntry => `${_.escape(logEntry)} <br />`);
this.$buildOutputContainer.append(formattedLogs);
scrollDown();
this.isLogComplete = true;
this.$buildRefreshAnimation.hide();
toggleDisableButton(this.$refreshLogBtn, false);
})
.catch(() => createFlash(__('Something went wrong on our end')));
}
}
import KubernetesLogs from '../../../../kubernetes_logs';
document.addEventListener('DOMContentLoaded', () => {
const kubernetesLogContainer = document.querySelector('.js-kubernetes-logs');
const kubernetesLog = new KubernetesLogs(kubernetesLogContainer);
kubernetesLog.getPodLogs();
});
module EE
module Projects
module EnvironmentsController
extend ActiveSupport::Concern
prepended do
before_action :authorize_read_pod_logs!, only: [:logs]
before_action :environment_ee, only: [:logs]
end
def logs
respond_to do |format|
format.html
format.json do
::Gitlab::PollingInterval.set_header(response, interval: 3_000)
render json: {
logs: pod_logs.strip.split("\n").as_json
}
end
end
end
private
def environment_ee
environment
end
def pod_logs
@pod_logs ||= environment.deployment_platform.read_pod_logs(params[:pod_name])
end
end
end
end
module EE module EE
module KubernetesService module KubernetesService
extend ActiveSupport::Concern
LOGS_LIMIT = 500.freeze
def rollout_status(environment) def rollout_status(environment)
result = with_reactive_cache do |data| result = with_reactive_cache do |data|
deployments = filter_by_label(data[:deployments], app: environment.slug) deployments = filter_by_label(data[:deployments], app: environment.slug)
...@@ -35,5 +39,13 @@ module EE ...@@ -35,5 +39,13 @@ module EE
[] []
end end
def read_pod_logs(pod_name, container: nil)
kubeclient.get_pod_log(pod_name, actual_namespace, container: container, tail_lines: LOGS_LIMIT).as_json
rescue ::Kubeclient::HttpError => err
raise err unless err.error_code == 404
[]
end
end end
end end
...@@ -71,6 +71,7 @@ class License < ActiveRecord::Base ...@@ -71,6 +71,7 @@ class License < ActiveRecord::Base
epics epics
ide ide
chatops chatops
pod_logs
].freeze ].freeze
# List all features available for early adopters, # List all features available for early adopters,
......
...@@ -44,6 +44,11 @@ module EE ...@@ -44,6 +44,11 @@ module EE
!PushRule.global&.commit_committer_check !PushRule.global&.commit_committer_check
end end
with_scope :subject
condition(:pod_logs_enabled) do
@subject.feature_available?(:pod_logs, @user)
end
rule { admin }.enable :change_repository_storage rule { admin }.enable :change_repository_storage
rule { support_bot }.enable :guest_access rule { support_bot }.enable :guest_access
...@@ -90,6 +95,8 @@ module EE ...@@ -90,6 +95,8 @@ module EE
enable :update_approvers enable :update_approvers
end end
rule { pod_logs_enabled & can?(:master_access) }.enable :read_pod_logs
rule { auditor }.policy do rule { auditor }.policy do
enable :public_user_access enable :public_user_access
prevent :request_access prevent :request_access
......
module EE
module EnvironmentEntity
extend ActiveSupport::Concern
prepended do
expose :logs_path, if: -> (*) { can_read_pod_logs? } do |environment|
logs_project_environment_path(environment.project, environment)
end
end
def can_read_pod_logs?
can?(current_user, :read_pod_logs, environment.project)
end
end
end
.js-kubernetes-logs{ data: { logs_path: logs_project_environment_path(@project, @environment, format: :json) } }
.build-page
.build-trace-container.prepend-top-default
.top-bar.js-top-bar
.truncated-info.hidden-xs.pull-left.js-pod-name
.controllers.pull-right
.has-tooltip.controllers-buttons{ title: _('Scroll to top'), data: { placement: 'top', container: 'body'} }
%button.js-scroll-up.btn-scroll.btn-transparent.btn-blank{ type: 'button', disabled: true }
= custom_icon('scroll_up')
.has-tooltip.controllers-buttons{ title: _('Scroll to bottom'), data: { placement: 'top', container: 'body'} }
%button.js-scroll-down.btn-scroll.btn-transparent.btn-blank{ type: 'button', disabled: true }
= custom_icon('scroll_down')
.has-tooltip.controllers-buttons{ title: _('Refresh'), data: { placement: 'top', container: 'body'} }
%button.js-refresh-log.btn-default.btn-refresh{ type: 'button', disabled: true }
= sprite_icon('retry')
= render 'shared/builds/build_output'
---
title: Allows the review of kubernetes pod logs within GitLab
merge_request: 4752
author:
type: added
...@@ -74,6 +74,7 @@ module Gitlab ...@@ -74,6 +74,7 @@ module Gitlab
def deployment_instance(pod_name:, pod_status:) def deployment_instance(pod_name:, pod_status:)
{ {
status: pod_status&.downcase, status: pod_status&.downcase,
pod_name: pod_name,
tooltip: "#{name} (#{pod_name}) #{pod_status}", tooltip: "#{name} (#{pod_name}) #{pod_status}",
track: track, track: track,
stable: stable? stable: stable?
......
...@@ -74,6 +74,50 @@ describe Projects::EnvironmentsController do ...@@ -74,6 +74,50 @@ describe Projects::EnvironmentsController do
end end
end end
describe 'GET logs' do
let(:logs) { "Log 1\nLog 2\nLog 3" }
let(:pod_name) { 'foo' }
before do
stub_licensed_features(pod_logs: true)
create(:cluster, :provided_by_gcp,
environment_scope: '*', projects: [project])
allow_any_instance_of(EE::KubernetesService).to receive(:read_pod_logs).with(pod_name).and_return(logs)
end
context 'when unlicensed' do
before do
stub_licensed_features(pod_logs: false)
end
it 'renders forbidden' do
get :logs, environment_params(pod_name: pod_name)
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when using HTML format' do
it 'renders logs template' do
get :logs, environment_params(pod_name: pod_name)
expect(response).to be_ok
expect(response).to render_template 'logs'
end
end
context 'when using JSON format' do
it 'returns the logs for a specific pod' do
get :logs, environment_params(pod_name: pod_name, format: :json)
expect(response).to be_ok
expect(json_response["logs"]).to match_array(["Log 1", "Log 2", "Log 3"])
end
end
end
def environment_params(opts = {}) def environment_params(opts = {})
opts.reverse_merge(namespace_id: project.namespace, opts.reverse_merge(namespace_id: project.namespace,
project_id: project, project_id: project,
......
...@@ -57,6 +57,9 @@ ...@@ -57,6 +57,9 @@
"folder_path": { "folder_path": {
"type": "string" "type": "string"
}, },
"logs_path": {
"type": "string"
},
"created_at": { "created_at": {
"type": "date" "type": "date"
}, },
......
...@@ -21,6 +21,7 @@ ...@@ -21,6 +21,7 @@
"type": "object", "type": "object",
"required": [ "required": [
"status", "status",
"pod_name",
"tooltip", "tooltip",
"track", "track",
"stable" "stable"
...@@ -29,6 +30,9 @@ ...@@ -29,6 +30,9 @@
"status": { "status": {
"type": "string" "type": "string"
}, },
"pod_name": {
"type": "string"
},
"tooltip": { "tooltip": {
"type": "string" "type": "string"
}, },
......
...@@ -54,10 +54,10 @@ describe Gitlab::Kubernetes::Deployment do ...@@ -54,10 +54,10 @@ describe Gitlab::Kubernetes::Deployment do
it 'returns all pods with generated names and pending' do it 'returns all pods with generated names and pending' do
expected = [ expected = [
{ status: 'pending', tooltip: 'unknown (generated-name-with-suffix) Pending', track: 'stable', stable: true }, { status: 'pending', pod_name: 'generated-name-with-suffix', tooltip: 'unknown (generated-name-with-suffix) Pending', track: 'stable', stable: true },
{ status: 'pending', tooltip: 'unknown (generated-name-with-suffix) Pending', track: 'stable', stable: true }, { status: 'pending', pod_name: 'generated-name-with-suffix', tooltip: 'unknown (generated-name-with-suffix) Pending', track: 'stable', stable: true },
{ status: 'pending', tooltip: 'unknown (generated-name-with-suffix) Pending', track: 'stable', stable: true }, { status: 'pending', pod_name: 'generated-name-with-suffix', tooltip: 'unknown (generated-name-with-suffix) Pending', track: 'stable', stable: true },
{ status: 'pending', tooltip: 'unknown (generated-name-with-suffix) Pending', track: 'stable', stable: true } { status: 'pending', pod_name: 'generated-name-with-suffix', tooltip: 'unknown (generated-name-with-suffix) Pending', track: 'stable', stable: true }
] ]
expect(deployment.instances).to eq(expected) expect(deployment.instances).to eq(expected)
...@@ -73,9 +73,9 @@ describe Gitlab::Kubernetes::Deployment do ...@@ -73,9 +73,9 @@ describe Gitlab::Kubernetes::Deployment do
it 'returns not spawned pods as pending and unknown and running' do it 'returns not spawned pods as pending and unknown and running' do
expected = [ expected = [
{ status: 'running', tooltip: 'unknown (generated-name-with-suffix) Running', track: 'stable', stable: true }, { status: 'running', pod_name: 'generated-name-with-suffix', tooltip: 'unknown (generated-name-with-suffix) Running', track: 'stable', stable: true },
{ status: 'pending', tooltip: 'unknown (Not provided) Pending', track: 'stable', stable: true }, { status: 'pending', pod_name: 'Not provided', tooltip: 'unknown (Not provided) Pending', track: 'stable', stable: true },
{ status: 'pending', tooltip: 'unknown (Not provided) Pending', track: 'stable', stable: true } { status: 'pending', pod_name: 'Not provided', tooltip: 'unknown (Not provided) Pending', track: 'stable', stable: true }
] ]
expect(deployment.instances).to eq(expected) expect(deployment.instances).to eq(expected)
...@@ -96,10 +96,10 @@ describe Gitlab::Kubernetes::Deployment do ...@@ -96,10 +96,10 @@ describe Gitlab::Kubernetes::Deployment do
it 'returns all instances as named and waiting' do it 'returns all instances as named and waiting' do
expected = [ expected = [
{ status: 'pending', tooltip: 'foo (kube-pod) Pending', track: 'stable', stable: true }, { status: 'pending', pod_name: 'kube-pod', tooltip: 'foo (kube-pod) Pending', track: 'stable', stable: true },
{ status: 'pending', tooltip: 'foo (kube-pod1) Pending', track: 'stable', stable: true }, { status: 'pending', pod_name: 'kube-pod1', tooltip: 'foo (kube-pod1) Pending', track: 'stable', stable: true },
{ status: 'pending', tooltip: 'foo (kube-pod2) Pending', track: 'stable', stable: true }, { status: 'pending', pod_name: 'kube-pod2', tooltip: 'foo (kube-pod2) Pending', track: 'stable', stable: true },
{ status: 'pending', tooltip: 'foo (kube-pod3) Pending', track: 'stable', stable: true } { status: 'pending', pod_name: 'kube-pod3', tooltip: 'foo (kube-pod3) Pending', track: 'stable', stable: true }
] ]
expect(deployment.instances).to eq(expected) expect(deployment.instances).to eq(expected)
...@@ -120,10 +120,10 @@ describe Gitlab::Kubernetes::Deployment do ...@@ -120,10 +120,10 @@ describe Gitlab::Kubernetes::Deployment do
it 'returns all instances' do it 'returns all instances' do
expected = [ expected = [
{ status: 'succeeded', tooltip: 'foo (kube-pod) Succeeded', track: 'stable', stable: true }, { status: 'succeeded', pod_name: 'kube-pod', tooltip: 'foo (kube-pod) Succeeded', track: 'stable', stable: true },
{ status: 'running', tooltip: 'foo (kube-pod1) Running', track: 'stable', stable: true }, { status: 'running', pod_name: 'kube-pod1', tooltip: 'foo (kube-pod1) Running', track: 'stable', stable: true },
{ status: 'pending', tooltip: 'foo (kube-pod2) Pending', track: 'stable', stable: true }, { status: 'pending', pod_name: 'kube-pod2', tooltip: 'foo (kube-pod2) Pending', track: 'stable', stable: true },
{ status: 'pending', tooltip: 'foo (kube-pod3) Pending', track: 'stable', stable: true } { status: 'pending', pod_name: 'kube-pod3', tooltip: 'foo (kube-pod3) Pending', track: 'stable', stable: true }
] ]
expect(deployment.instances).to eq(expected) expect(deployment.instances).to eq(expected)
...@@ -140,7 +140,7 @@ describe Gitlab::Kubernetes::Deployment do ...@@ -140,7 +140,7 @@ describe Gitlab::Kubernetes::Deployment do
it 'returns all instances' do it 'returns all instances' do
expected = [ expected = [
{ status: 'pending', tooltip: 'foo (kube-pod) Pending', track: 'stable', stable: true } { status: 'pending', pod_name: 'kube-pod', tooltip: 'foo (kube-pod) Pending', track: 'stable', stable: true }
] ]
expect(deployment.instances).to eq(expected) expect(deployment.instances).to eq(expected)
...@@ -153,7 +153,7 @@ describe Gitlab::Kubernetes::Deployment do ...@@ -153,7 +153,7 @@ describe Gitlab::Kubernetes::Deployment do
it 'returns all instances' do it 'returns all instances' do
expected = [ expected = [
{ status: 'pending', tooltip: 'foo (kube-pod) Pending', track: 'canary', stable: false } { status: 'pending', pod_name: 'kube-pod', tooltip: 'foo (kube-pod) Pending', track: 'canary', stable: false }
] ]
expect(deployment.instances).to eq(expected) expect(deployment.instances).to eq(expected)
......
...@@ -45,12 +45,12 @@ describe Gitlab::Kubernetes::RolloutStatus do ...@@ -45,12 +45,12 @@ describe Gitlab::Kubernetes::RolloutStatus do
it 'stores the union of deployment instances' do it 'stores the union of deployment instances' do
expected = [ expected = [
{ status: 'running', tooltip: 'two (two) Running', track: 'any', stable: false }, { status: 'running', pod_name: "two", tooltip: 'two (two) Running', track: 'any', stable: false },
{ status: 'running', tooltip: 'two (two) Running', track: 'any', stable: false }, { status: 'running', pod_name: "two", tooltip: 'two (two) Running', track: 'any', stable: false },
{ status: 'running', tooltip: 'two (two) Running', track: 'any', stable: false }, { status: 'running', pod_name: "two", tooltip: 'two (two) Running', track: 'any', stable: false },
{ status: 'running', tooltip: 'one (one) Running', track: 'stable', stable: true }, { status: 'running', pod_name: "one", tooltip: 'one (one) Running', track: 'stable', stable: true },
{ status: 'running', tooltip: 'one (one) Running', track: 'stable', stable: true }, { status: 'running', pod_name: "one", tooltip: 'one (one) Running', track: 'stable', stable: true },
{ status: 'running', tooltip: 'one (one) Running', track: 'stable', stable: true } { status: 'running', pod_name: "one", tooltip: 'one (one) Running', track: 'stable', stable: true }
] ]
expect(rollout_status.instances).to eq(expected) expect(rollout_status.instances).to eq(expected)
...@@ -66,12 +66,12 @@ describe Gitlab::Kubernetes::RolloutStatus do ...@@ -66,12 +66,12 @@ describe Gitlab::Kubernetes::RolloutStatus do
it 'stores the union of deployment instances' do it 'stores the union of deployment instances' do
expected = [ expected = [
{ status: 'running', tooltip: 'two (two) Running', track: 'canary', stable: false }, { status: 'running', pod_name: "two", tooltip: 'two (two) Running', track: 'canary', stable: false },
{ status: 'running', tooltip: 'two (two) Running', track: 'canary', stable: false }, { status: 'running', pod_name: "two", tooltip: 'two (two) Running', track: 'canary', stable: false },
{ status: 'running', tooltip: 'two (two) Running', track: 'canary', stable: false }, { status: 'running', pod_name: "two", tooltip: 'two (two) Running', track: 'canary', stable: false },
{ status: 'running', tooltip: 'one (one) Running', track: 'stable', stable: true }, { status: 'running', pod_name: "one", tooltip: 'one (one) Running', track: 'stable', stable: true },
{ status: 'running', tooltip: 'one (one) Running', track: 'stable', stable: true }, { status: 'running', pod_name: "one", tooltip: 'one (one) Running', track: 'stable', stable: true },
{ status: 'running', tooltip: 'one (one) Running', track: 'stable', stable: true } { status: 'running', pod_name: "one", tooltip: 'one (one) Running', track: 'stable', stable: true }
] ]
expect(rollout_status.instances).to eq(expected) expect(rollout_status.instances).to eq(expected)
......
...@@ -28,4 +28,38 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching ...@@ -28,4 +28,38 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
it { is_expected.to eq(pods: [], deployments: []) } it { is_expected.to eq(pods: [], deployments: []) }
end end
end end
describe '#read_pod_logs' do
subject { service.read_pod_logs(pod_name) }
let(:pod_name) { 'foo' }
let!(:cluster) { create(:cluster, :project, enabled: true, platform_kubernetes: service) }
let(:service) { create(:cluster_platform_kubernetes, :configured) }
context 'when kubernetes responds with valid logs' do
before do
stub_kubeclient_logs(pod_name)
end
it 'returns logs' do
expect(subject.body).to eq("\"Log 1\\nLog 2\\nLog 3\"")
end
end
context 'when kubernetes response with 500s' do
before do
stub_kubeclient_logs(pod_name, status: 500)
end
it { expect { subject }.to raise_error(::Kubeclient::HttpError) }
end
context 'when kubernetes responds with 404s' do
before do
stub_kubeclient_logs(pod_name, status: 404)
end
it { is_expected.to be_empty }
end
end
end end
...@@ -4,7 +4,7 @@ module QA::Page ...@@ -4,7 +4,7 @@ module QA::Page
COMPLETED_STATUSES = %w[passed failed canceled blocked skipped manual].freeze # excludes created, pending, running COMPLETED_STATUSES = %w[passed failed canceled blocked skipped manual].freeze # excludes created, pending, running
PASSED_STATUS = 'passed'.freeze PASSED_STATUS = 'passed'.freeze
view 'app/views/projects/jobs/show.html.haml' do view 'app/views/shared/builds/_build_output.html.haml' do
element :build_output, '.js-build-output' element :build_output, '.js-build-output'
end end
......
export const logMockData = [
'[2018-05-17 16:31:10] INFO WEBrick 1.3.1',
'[2018-05-17 16:31:10] INFO ruby 2.4.1 (2017-03-22) [x86_64-linux-musl]',
'[2018-05-17 16:31:10] INFO WEBrick::HTTPServer#start: pid=5 port=5000',
'172.17.0.1 - - [17/May/2018:16:31:14 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:31:24 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:31:32 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:31:34 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:31:42 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:31:44 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:31:52 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:31:54 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:32:02 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:32:04 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:32:12 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:32:14 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:32:22 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:32:24 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:32:32 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:32:34 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:32:42 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:32:44 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:32:52 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:32:54 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:33:02 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:33:04 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:33:12 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:33:14 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:33:22 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:33:24 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:33:32 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:33:34 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:33:42 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:33:44 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:33:52 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:33:54 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:34:02 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:34:04 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:34:12 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:34:14 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:34:22 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:34:24 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:34:32 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:34:34 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:34:42 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:34:44 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:34:52 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:34:54 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:35:02 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:35:04 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:35:12 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:35:14 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:35:22 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
'172.17.0.1 - - [17/May/2018:16:35:24 UTC] \'GET / HTTP/1.1\' 200 13',
'- -> /',
];
export default {};
import Vue from 'vue'; import Vue from 'vue';
import DeployBoard from 'ee/environments/components/deploy_board_component.vue'; import DeployBoard from 'ee/environments/components/deploy_board_component.vue';
import { deployBoardMockData } from './mock_data'; import { deployBoardMockData, environment } from './mock_data';
describe('Deploy Board', () => { describe('Deploy Board', () => {
let DeployBoardComponent; let DeployBoardComponent;
...@@ -18,6 +18,7 @@ describe('Deploy Board', () => { ...@@ -18,6 +18,7 @@ describe('Deploy Board', () => {
deployBoardData: deployBoardMockData, deployBoardData: deployBoardMockData,
isLoading: false, isLoading: false,
isEmpty: false, isEmpty: false,
logsPath: environment.log_path,
}, },
}).$mount(); }).$mount();
}); });
...@@ -29,7 +30,7 @@ describe('Deploy Board', () => { ...@@ -29,7 +30,7 @@ describe('Deploy Board', () => {
}); });
it('should render all instances', () => { it('should render all instances', () => {
const instances = component.$el.querySelectorAll('.deploy-board-instances-container div'); const instances = component.$el.querySelectorAll('.deploy-board-instances-container a');
expect(instances.length).toEqual(deployBoardMockData.instances.length); expect(instances.length).toEqual(deployBoardMockData.instances.length);
...@@ -55,6 +56,7 @@ describe('Deploy Board', () => { ...@@ -55,6 +56,7 @@ describe('Deploy Board', () => {
deployBoardData: {}, deployBoardData: {},
isLoading: false, isLoading: false,
isEmpty: true, isEmpty: true,
logsPath: environment.log_path,
}, },
}).$mount(); }).$mount();
}); });
...@@ -74,6 +76,7 @@ describe('Deploy Board', () => { ...@@ -74,6 +76,7 @@ describe('Deploy Board', () => {
deployBoardData: {}, deployBoardData: {},
isLoading: true, isLoading: true,
isEmpty: false, isEmpty: false,
logsPath: environment.log_path,
}, },
}).$mount(); }).$mount();
}); });
......
import Vue from 'vue'; import Vue from 'vue';
import DeployBoardInstance from 'ee/environments/components/deploy_board_instance_component.vue'; import DeployBoardInstance from 'ee/environments/components/deploy_board_instance_component.vue';
import { folder } from './mock_data';
describe('Deploy Board Instance', () => { describe('Deploy Board Instance', () => {
let DeployBoardInstanceComponent; let DeployBoardInstanceComponent;
...@@ -13,6 +14,7 @@ describe('Deploy Board Instance', () => { ...@@ -13,6 +14,7 @@ describe('Deploy Board Instance', () => {
propsData: { propsData: {
status: 'ready', status: 'ready',
tooltipText: 'This is a pod', tooltipText: 'This is a pod',
logsPath: folder.log_path,
}, },
}).$mount(); }).$mount();
...@@ -24,6 +26,7 @@ describe('Deploy Board Instance', () => { ...@@ -24,6 +26,7 @@ describe('Deploy Board Instance', () => {
const component = new DeployBoardInstanceComponent({ const component = new DeployBoardInstanceComponent({
propsData: { propsData: {
status: 'deploying', status: 'deploying',
logsPath: folder.log_path,
}, },
}).$mount(); }).$mount();
...@@ -36,9 +39,23 @@ describe('Deploy Board Instance', () => { ...@@ -36,9 +39,23 @@ describe('Deploy Board Instance', () => {
propsData: { propsData: {
status: 'deploying', status: 'deploying',
stable: false, stable: false,
logsPath: folder.log_path,
}, },
}).$mount(); }).$mount();
expect(component.$el.classList.contains('deploy-board-instance-canary')).toBe(true); expect(component.$el.classList.contains('deploy-board-instance-canary')).toBe(true);
}); });
it('should have a log path computed with a pod name as a parameter', () => {
const component = new DeployBoardInstanceComponent({
propsData: {
status: 'deploying',
stable: false,
logsPath: folder.log_path,
podName: 'tanuki-1',
},
}).$mount();
expect(component.computedLogPath).toEqual('/root/review-app/environments/12/logs?pod_name=tanuki-1');
});
}); });
...@@ -20,6 +20,7 @@ describe('Environment item', () => { ...@@ -20,6 +20,7 @@ describe('Environment item', () => {
size: 3, size: 3,
isFolder: true, isFolder: true,
environment_path: 'url', environment_path: 'url',
log_path: 'url',
}; };
component = new EnvironmentItem({ component = new EnvironmentItem({
...@@ -108,6 +109,7 @@ describe('Environment item', () => { ...@@ -108,6 +109,7 @@ describe('Environment item', () => {
}, },
'stop_action?': true, 'stop_action?': true,
environment_path: 'root/ci-folders/environments/31', environment_path: 'root/ci-folders/environments/31',
log_path: 'root/ci-folders/environments/31/logs',
created_at: '2016-11-07T11:11:16.525Z', created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-10T15:55:58.778Z', updated_at: '2016-11-10T15:55:58.778Z',
}; };
......
...@@ -12,6 +12,7 @@ export const environmentsList = [ ...@@ -12,6 +12,7 @@ export const environmentsList = [
stop_path: '/root/review-app/environments/7/stop', stop_path: '/root/review-app/environments/7/stop',
created_at: '2017-01-31T10:53:46.894Z', created_at: '2017-01-31T10:53:46.894Z',
updated_at: '2017-01-31T10:53:46.894Z', updated_at: '2017-01-31T10:53:46.894Z',
log_path: '/root/review-app/environments/7/logs',
rollout_status: {}, rollout_status: {},
}, },
{ {
...@@ -28,6 +29,7 @@ export const environmentsList = [ ...@@ -28,6 +29,7 @@ export const environmentsList = [
stop_path: '/root/review-app/environments/12/stop', stop_path: '/root/review-app/environments/12/stop',
created_at: '2017-02-01T19:42:18.400Z', created_at: '2017-02-01T19:42:18.400Z',
updated_at: '2017-02-01T19:42:18.400Z', updated_at: '2017-02-01T19:42:18.400Z',
log_path: '/root/review-app/environments/12/logs',
rollout_status: {}, rollout_status: {},
}, },
]; ];
...@@ -120,37 +122,39 @@ export const folder = { ...@@ -120,37 +122,39 @@ export const folder = {
stop_path: '/root/review-app/environments/12/stop', stop_path: '/root/review-app/environments/12/stop',
created_at: '2017-02-01T19:42:18.400Z', created_at: '2017-02-01T19:42:18.400Z',
updated_at: '2017-02-01T19:42:18.400Z', updated_at: '2017-02-01T19:42:18.400Z',
rollout_status: {},
log_path: '/root/review-app/environments/12/logs',
}; };
export const deployBoardMockData = { export const deployBoardMockData = {
instances: [ instances: [
{ status: 'finished', tooltip: 'tanuki-2334 Finished' }, { status: 'finished', tooltip: 'tanuki-2334 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2335 Finished' }, { status: 'finished', tooltip: 'tanuki-2335 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2336 Finished' }, { status: 'finished', tooltip: 'tanuki-2336 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2337 Finished' }, { status: 'finished', tooltip: 'tanuki-2337 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2338 Finished' }, { status: 'finished', tooltip: 'tanuki-2338 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2339 Finished' }, { status: 'finished', tooltip: 'tanuki-2339 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2340 Finished' }, { status: 'finished', tooltip: 'tanuki-2340 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2334 Finished' }, { status: 'finished', tooltip: 'tanuki-2334 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2335 Finished' }, { status: 'finished', tooltip: 'tanuki-2335 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2336 Finished' }, { status: 'finished', tooltip: 'tanuki-2336 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2337 Finished' }, { status: 'finished', tooltip: 'tanuki-2337 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2338 Finished' }, { status: 'finished', tooltip: 'tanuki-2338 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2339 Finished' }, { status: 'finished', tooltip: 'tanuki-2339 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2340 Finished' }, { status: 'finished', tooltip: 'tanuki-2340 Finished', pod_name: 'production-tanuki-1' },
{ status: 'deploying', tooltip: 'tanuki-2341 Deploying' }, { status: 'deploying', tooltip: 'tanuki-2341 Deploying', pod_name: 'production-tanuki-1' },
{ status: 'deploying', tooltip: 'tanuki-2342 Deploying' }, { status: 'deploying', tooltip: 'tanuki-2342 Deploying', pod_name: 'production-tanuki-1' },
{ status: 'deploying', tooltip: 'tanuki-2343 Deploying' }, { status: 'deploying', tooltip: 'tanuki-2343 Deploying', pod_name: 'production-tanuki-1' },
{ status: 'failed', tooltip: 'tanuki-2344 Failed' }, { status: 'failed', tooltip: 'tanuki-2344 Failed', pod_name: 'production-tanuki-1' },
{ status: 'ready', tooltip: 'tanuki-2345 Ready' }, { status: 'ready', tooltip: 'tanuki-2345 Ready', pod_name: 'production-tanuki-1' },
{ status: 'ready', tooltip: 'tanuki-2346 Ready' }, { status: 'ready', tooltip: 'tanuki-2346 Ready', pod_name: 'production-tanuki-1' },
{ status: 'preparing', tooltip: 'tanuki-2348 Preparing' }, { status: 'preparing', tooltip: 'tanuki-2348 Preparing', pod_name: 'production-tanuki-1' },
{ status: 'preparing', tooltip: 'tanuki-2349 Preparing' }, { status: 'preparing', tooltip: 'tanuki-2349 Preparing', pod_name: 'production-tanuki-1' },
{ status: 'preparing', tooltip: 'tanuki-2350 Preparing' }, { status: 'preparing', tooltip: 'tanuki-2350 Preparing', pod_name: 'production-tanuki-1' },
{ status: 'preparing', tooltip: 'tanuki-2353 Preparing' }, { status: 'preparing', tooltip: 'tanuki-2353 Preparing', pod_name: 'production-tanuki-1' },
{ status: 'waiting', tooltip: 'tanuki-2354 Waiting' }, { status: 'waiting', tooltip: 'tanuki-2354 Waiting', pod_name: 'production-tanuki-1' },
{ status: 'waiting', tooltip: 'tanuki-2355 Waiting' }, { status: 'waiting', tooltip: 'tanuki-2355 Waiting', pod_name: 'production-tanuki-1' },
{ status: 'waiting', tooltip: 'tanuki-2356 Waiting' }, { status: 'waiting', tooltip: 'tanuki-2356 Waiting', pod_name: 'production-tanuki-1' },
], ],
abort_url: 'url', abort_url: 'url',
rollback_url: 'url', rollback_url: 'url',
......
.js-kubernetes-logs{ data: { logs_path: '/root/kubernetes-app/environments/1/logs' } }
.build-page
.build-trace-container.prepend-top-default
.top-bar.js-top-bar
.truncated-info.hidden-xs.pull-left.js-pod-name
Pod logs from pod name
.controllers.pull-right
.has-tooltip.controllers-buttons{ title: 'Scroll to top', data: { placement: 'top', container: 'body'} }
%button.js-scroll-up.btn-scroll.btn-transparent.btn-blank{ type: 'button', disabled: true }
.has-tooltip.controllers-buttons{ title: 'Scroll to bottom', data: { placement: 'top', container: 'body'} }
%button.js-scroll-down.btn-scroll.btn-transparent.btn-blank{ type: 'button', disabled: true }
.has-tooltip.controllers-buttons{ title: 'Refresh', data: { placement: 'top', container: 'body'} }
%button.js-refresh-log.btn-default.btn-refresh{ type: 'button', disabled: true }
%pre.build-trace#build-trace
%code.bash.js-build-output
.build-loader-animation.js-build-refresh
import KubernetesLogs from 'ee/kubernetes_logs';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { logMockData } from './ee/kubernetes_mock_data';
describe('Kubernetes Logs', () => {
const fixtureTemplate = 'static/environments_logs.html.raw';
const mockPodName = 'production-tanuki-1';
const logMockPath = '/root/kubernetes-app/environments/1/logs';
let kubernetesLogContainer;
let kubernetesLog;
let mock;
preloadFixtures(fixtureTemplate);
describe('When data is requested correctly', () => {
beforeEach(() => {
loadFixtures(fixtureTemplate);
spyOnDependency(KubernetesLogs, 'getParameterValues').and.callFake(() => [mockPodName]);
mock = new MockAdapter(axios);
mock.onGet(logMockPath).reply(200, { logs: logMockData });
kubernetesLogContainer = document.querySelector('.js-kubernetes-logs');
});
afterEach(() => {
mock.restore();
});
it('has the pod name placed on the top bar', () => {
kubernetesLog = new KubernetesLogs(kubernetesLogContainer);
const topBar = document.querySelector('.js-pod-name');
expect(topBar.textContent).toContain(kubernetesLog.podName);
});
it('queries the pod log data and sets the dom elements', (done) => {
const scrollSpy = spyOnDependency(KubernetesLogs, 'scrollDown').and.callThrough();
const toggleDisableSpy = spyOnDependency(KubernetesLogs, 'toggleDisableButton').and.stub();
kubernetesLog = new KubernetesLogs(kubernetesLogContainer);
kubernetesLog.getPodLogs();
setTimeout(() => {
expect(kubernetesLog.isLogComplete).toEqual(true);
expect(kubernetesLog.$buildOutputContainer.text()).toContain(logMockData[0].trim());
expect(scrollSpy).toHaveBeenCalled();
expect(toggleDisableSpy).toHaveBeenCalled();
done();
}, 0);
});
});
describe('When no pod name is available', () => {
beforeEach(() => {
loadFixtures(fixtureTemplate);
kubernetesLogContainer = document.querySelector('.js-kubernetes-logs');
});
it('shows up a flash message when no pod name is specified', () => {
const createFlashSpy = spyOnDependency(KubernetesLogs, 'createFlash').and.stub();
kubernetesLog = new KubernetesLogs(kubernetesLogContainer);
expect(createFlashSpy).toHaveBeenCalled();
});
});
});
...@@ -9,6 +9,10 @@ module KubernetesHelpers ...@@ -9,6 +9,10 @@ module KubernetesHelpers
kube_response(kube_pods_body) kube_response(kube_pods_body)
end end
def kube_logs_response
kube_response(kube_logs_body)
end
def kube_deployments_response def kube_deployments_response
kube_response(kube_deployments_body) kube_response(kube_deployments_body)
end end
...@@ -25,6 +29,13 @@ module KubernetesHelpers ...@@ -25,6 +29,13 @@ module KubernetesHelpers
WebMock.stub_request(:get, pods_url).to_return(response || kube_pods_response) WebMock.stub_request(:get, pods_url).to_return(response || kube_pods_response)
end end
def stub_kubeclient_logs(pod_name, response = nil)
stub_kubeclient_discover(service.api_url)
logs_url = service.api_url + "/api/v1/namespaces/#{service.actual_namespace}/pods/#{pod_name}/log?tailLines=#{Clusters::Platforms::Kubernetes::LOGS_LIMIT}"
WebMock.stub_request(:get, logs_url).to_return(response || kube_logs_response)
end
def stub_kubeclient_deployments(response = nil) def stub_kubeclient_deployments(response = nil)
stub_kubeclient_discover(service.api_url) stub_kubeclient_discover(service.api_url)
deployments_url = service.api_url + "/apis/extensions/v1beta1/namespaces/#{service.actual_namespace}/deployments" deployments_url = service.api_url + "/apis/extensions/v1beta1/namespaces/#{service.actual_namespace}/deployments"
...@@ -89,6 +100,10 @@ module KubernetesHelpers ...@@ -89,6 +100,10 @@ module KubernetesHelpers
} }
end end
def kube_logs_body
"Log 1\nLog 2\nLog 3"
end
def kube_deployments_body def kube_deployments_body
{ {
"kind" => "DeploymentList", "kind" => "DeploymentList",
......
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