Commit a906752d authored by Kamil Trzcinski's avatar Kamil Trzcinski

Merge branch '38464-k8s-apps' into add-ingress-to-cluster-applications

parents fd9be1dd 67e12219
/* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */
import _ from 'underscore';
import { insertText, getSelectedFragment, nodeMatchesSelector } from './lib/utils/common_utils';
import { placeholderImage } from './lazy_loader';
import { insertText, getSelectedFragment, nodeMatchesSelector } from '../lib/utils/common_utils';
import { placeholderImage } from '../lazy_loader';
const gfmRules = {
// The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert
......@@ -284,7 +285,7 @@ const gfmRules = {
},
};
class CopyAsGFM {
export class CopyAsGFM {
constructor() {
$(document).on('copy', '.md, .wiki', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformGFMSelection); });
$(document).on('copy', 'pre.code.highlight, .diff-content .line_content', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformCodeSelection); });
......@@ -469,7 +470,12 @@ class CopyAsGFM {
}
}
window.gl = window.gl || {};
window.gl.CopyAsGFM = CopyAsGFM;
// Export CopyAsGFM as a global for rspec to access
// see /spec/features/copy_as_gfm_spec.rb
if (process.env.NODE_ENV !== 'production') {
window.CopyAsGFM = CopyAsGFM;
}
new CopyAsGFM();
export default function initCopyAsGFM() {
return new CopyAsGFM();
}
import './autosize';
import './bind_in_out';
import initCopyAsGFM from './copy_as_gfm';
import './details_behavior';
import installGlEmojiElement from './gl_emoji';
import './quick_submit';
......@@ -7,3 +8,4 @@ import './requires_input';
import './toggler_behavior';
installGlEmojiElement();
initCopyAsGFM();
......@@ -163,12 +163,10 @@ export default class Clusters {
.map(appId => newApplicationMap[appId].title);
if (appTitles.length > 0) {
this.successApplicationContainer.textContent = sprintf(s__('ClusterIntegration|%{appList} was successfully installed on your cluster'), {
const text = sprintf(s__('ClusterIntegration|%{appList} was successfully installed on your cluster'), {
appList: appTitles.join(', '),
});
this.successApplicationContainer.classList.remove('hidden');
} else {
this.successApplicationContainer.classList.add('hidden');
Flash(text, 'notice', this.successApplicationContainer);
}
}
......
......@@ -66,7 +66,7 @@ export default {
// Avoid the potential for the real-time data to say APPLICATION_INSTALLABLE but
// we already made a request to install and are just waiting for the real-time
// to sync up.
return this.status !== APPLICATION_INSTALLABLE ||
return (this.status !== APPLICATION_INSTALLABLE && this.status !== APPLICATION_ERROR) ||
this.requestStatus === REQUEST_LOADING ||
this.requestStatus === REQUEST_SUCCESS;
},
......
......@@ -46,7 +46,6 @@ import './commits';
import './compare';
import './compare_autocomplete';
import './confirm_danger_modal';
import './copy_as_gfm';
import './copy_to_clipboard';
import Flash, { removeFlashClickListener } from './flash';
import './gl_dropdown';
......
......@@ -4,6 +4,7 @@
import _ from 'underscore';
import 'mousetrap';
import ShortcutsNavigation from './shortcuts_navigation';
import { CopyAsGFM } from './behaviors/copy_as_gfm';
export default class ShortcutsIssuable extends ShortcutsNavigation {
constructor(isMergeRequest) {
......@@ -33,8 +34,8 @@ export default class ShortcutsIssuable extends ShortcutsNavigation {
return false;
}
const el = window.gl.CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true));
const selected = window.gl.CopyAsGFM.nodeToGFM(el);
const el = CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true));
const selected = CopyAsGFM.nodeToGFM(el);
if (selected.trim() === '') {
return false;
......
......@@ -121,7 +121,7 @@
}
.disabled-comment {
border: none;
border: 0;
border-radius: $label-border-radius;
padding-top: $gl-vert-padding;
padding-bottom: $gl-vert-padding;
......
......@@ -11,14 +11,16 @@ module Clusters
validates :cluster, presence: true
after_initialize :set_initial_status
def self.application_name
self.to_s.demodulize.underscore
end
def set_initial_status
self.status = 0 unless cluster&.platform_kubernetes_active?
def initial_status
if cluster&.platform_kubernetes_active?
:installable
else
:not_installable
end
end
def name
......
......@@ -4,7 +4,7 @@ module Clusters
extend ActiveSupport::Concern
included do
state_machine :status, initial: :installable do
state_machine :status, initial: ->(application) { application.initial_status } do
state :not_installable, value: -2
state :errored, value: -1
state :installable, value: 0
......@@ -12,20 +12,20 @@ module Clusters
state :installing, value: 2
state :installed, value: 3
event :make_scheduled do
transition %i(installable errored) => :scheduled
end
event :make_installing do
transition any - [:installing] => :installing
transition %i(scheduled) => :installing
end
event :make_installed do
transition any - [:installed] => :installed
transition %i(installing) => :installed
end
event :make_errored do
transition any - [:errored] => :errored
end
event :make_scheduled do
transition %i(installable errored) => :scheduled
transition any => :errored
end
before_transition any => [:scheduled] do |app_status, _|
......
......@@ -6,7 +6,7 @@ module Clusters
case installation_phase
when Gitlab::Kubernetes::Pod::SUCCEEDED
finalize_installation
on_success
when Gitlab::Kubernetes::Pod::FAILED
on_failed
else
......@@ -18,23 +18,39 @@ module Clusters
private
def on_success
app.make_installed!
ensure
remove_installation_pod
end
def on_failed
app.make_errored!(installation_errors || 'Installation silently failed')
finalize_installation
ensure
remove_installation_pod
end
def check_timeout
if Time.now.utc - app.updated_at.to_time.utc > ClusterWaitForAppInstallationWorker::TIMEOUT
app.make_errored!('Installation timeouted')
finalize_installation
if timeouted?
begin
app.make_errored!('Installation timeouted')
ensure
remove_installation_pod
end
else
ClusterWaitForAppInstallationWorker.perform_in(
ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id)
end
end
def finalize_installation
FinalizeInstallationService.new(app).execute
def timeouted?
Time.now.utc - app.updated_at.to_time.utc > ClusterWaitForAppInstallationWorker::TIMEOUT
end
def remove_installation_pod
helm_api.delete_installation_pod!(app)
rescue
# no-op
end
def installation_phase
......
module Clusters
module Applications
class FinalizeInstallationService < BaseHelmService
def execute
helm_api.delete_installation_pod!(app)
app.make_installed! if app.installing?
end
end
end
end
......@@ -14,7 +14,8 @@
help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications') } }
.hidden.js-cluster-application-notice.alert.alert-info.alert-block.append-bottom-10{ role: 'alert' }
.js-cluster-application-notice
.flash-container
%section.settings.no-animate.expanded
%h4= s_('ClusterIntegration|Enable cluster integration')
......
......@@ -54,8 +54,6 @@ project_tree:
- :auto_devops
- :triggers
- :pipeline_schedules
- clusters:
- :application_helm
- :services
- :hooks
- protected_branches:
......
......@@ -8,8 +8,6 @@ module Gitlab
triggers: 'Ci::Trigger',
pipeline_schedules: 'Ci::PipelineSchedule',
builds: 'Ci::Build',
clusters: 'Clusters::Cluster',
application_helm: 'Clusters::Applications::Helm',
hooks: 'ProjectHook',
merge_access_levels: 'ProtectedBranch::MergeAccessLevel',
push_access_levels: 'ProtectedBranch::PushAccessLevel',
......
FactoryGirl.define do
factory :applications_helm, class: Clusters::Applications::Helm do
trait :cluster do
before(:create) do |app, _|
app.cluster = create(:cluster)
end
end
factory :cluster_applications_helm, class: Clusters::Applications::Helm do
cluster factory: %i(cluster provided_by_gcp)
trait :installable do
cluster
status 0
end
trait :scheduled do
cluster
status 1
end
trait :installing do
cluster
status 2
end
trait :installed do
cluster
status 3
end
trait :errored do
cluster
status(-1)
status_reason 'something went wrong'
end
......
......@@ -664,7 +664,7 @@ describe 'Copy as GFM', :js do
def html_to_gfm(html, transformer = 'transformGFMSelection', target: nil)
js = <<-JS.strip_heredoc
(function(html) {
var transformer = window.gl.CopyAsGFM[#{transformer.inspect}];
var transformer = window.CopyAsGFM[#{transformer.inspect}];
var node = document.createElement('div');
$(html).each(function() { node.appendChild(this) });
......@@ -678,7 +678,7 @@ describe 'Copy as GFM', :js do
node = transformer(node, target);
if (!node) return null;
return window.gl.CopyAsGFM.nodeToGFM(node);
return window.CopyAsGFM.nodeToGFM(node);
})("#{escape_javascript(html)}")
JS
page.evaluate_script(js)
......
import { CopyAsGFM } from '~/behaviors/copy_as_gfm';
describe('CopyAsGFM', () => {
describe('CopyAsGFM.pasteGFM', () => {
function callPasteGFM() {
const e = {
originalEvent: {
clipboardData: {
getData(mimeType) {
// When GFM code is copied, we put the regular plain text
// on the clipboard as `text/plain`, and the GFM as `text/x-gfm`.
// This emulates the behavior of `getData` with that data.
if (mimeType === 'text/plain') {
return 'code';
}
if (mimeType === 'text/x-gfm') {
return '`code`';
}
return null;
},
},
},
preventDefault() {},
};
CopyAsGFM.pasteGFM(e);
}
it('wraps pasted code when not already in code tags', () => {
spyOn(window.gl.utils, 'insertText').and.callFake((el, textFunc) => {
const insertedText = textFunc('This is code: ', '');
expect(insertedText).toEqual('`code`');
});
callPasteGFM();
});
it('does not wrap pasted code when already in code tags', () => {
spyOn(window.gl.utils, 'insertText').and.callFake((el, textFunc) => {
const insertedText = textFunc('This is code: `', '`');
expect(insertedText).toEqual('code');
});
callPasteGFM();
});
});
});
......@@ -49,7 +49,7 @@ describe('Clusters', () => {
helm: { status: APPLICATION_INSTALLABLE, title: 'Helm Tiller' },
});
expect(document.querySelector('.js-cluster-application-notice.hidden')).toBeDefined();
expect(document.querySelector('.js-cluster-application-notice .flash-text')).toBeNull();
});
it('shows an alert when something gets newly installed', () => {
......@@ -61,8 +61,8 @@ describe('Clusters', () => {
helm: { status: APPLICATION_INSTALLED, title: 'Helm Tiller' },
});
expect(document.querySelector('.js-cluster-application-notice:not(.hidden)')).toBeDefined();
expect(document.querySelector('.js-cluster-application-notice').textContent.trim()).toEqual('Helm Tiller was successfully installed on your cluster');
expect(document.querySelector('.js-cluster-application-notice .flash-text')).toBeDefined();
expect(document.querySelector('.js-cluster-application-notice .flash-text').textContent.trim()).toEqual('Helm Tiller was successfully installed on your cluster');
});
it('shows an alert when multiple things gets newly installed', () => {
......@@ -76,28 +76,8 @@ describe('Clusters', () => {
ingress: { status: APPLICATION_INSTALLED, title: 'Ingress' },
});
expect(document.querySelector('.js-cluster-application-notice:not(.hidden)')).toBeDefined();
expect(document.querySelector('.js-cluster-application-notice').textContent.trim()).toEqual('Helm Tiller, Ingress was successfully installed on your cluster');
});
it('hides existing alert when we call again and nothing is newly installed', () => {
const installedState = {
...INITIAL_APP_MAP,
helm: { status: APPLICATION_INSTALLED, title: 'Helm Tiller' },
};
// Show the banner
cluster.checkForNewInstalls({
...INITIAL_APP_MAP,
helm: { status: APPLICATION_INSTALLING, title: 'Helm Tiller' },
}, installedState);
expect(document.querySelector('.js-cluster-application-notice:not(.hidden)')).toBeDefined();
// Banner should go back hidden
cluster.checkForNewInstalls(installedState, installedState);
expect(document.querySelector('.js-cluster-application-notice.hidden')).toBeDefined();
expect(document.querySelector('.js-cluster-application-notice .flash-text')).toBeDefined();
expect(document.querySelector('.js-cluster-application-notice .flash-text').textContent.trim()).toEqual('Helm Tiller, Ingress was successfully installed on your cluster');
});
});
......
......@@ -117,7 +117,7 @@ describe('Application Row', () => {
expect(vm.installButtonDisabled).toEqual(true);
});
it('has disabled "Install" when APPLICATION_ERROR', () => {
it('has enabled "Install" when APPLICATION_ERROR', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_ERROR,
......@@ -125,7 +125,7 @@ describe('Application Row', () => {
expect(vm.installButtonLabel).toEqual('Install');
expect(vm.installButtonLoading).toEqual(false);
expect(vm.installButtonDisabled).toEqual(true);
expect(vm.installButtonDisabled).toEqual(false);
});
it('has loading "Install" when REQUEST_LOADING', () => {
......
import '~/copy_as_gfm';
(() => {
describe('gl.CopyAsGFM', () => {
describe('gl.CopyAsGFM.pasteGFM', () => {
function callPasteGFM() {
const e = {
originalEvent: {
clipboardData: {
getData(mimeType) {
// When GFM code is copied, we put the regular plain text
// on the clipboard as `text/plain`, and the GFM as `text/x-gfm`.
// This emulates the behavior of `getData` with that data.
if (mimeType === 'text/plain') {
return 'code';
}
if (mimeType === 'text/x-gfm') {
return '`code`';
}
return null;
},
},
},
preventDefault() {},
};
window.gl.CopyAsGFM.pasteGFM(e);
}
it('wraps pasted code when not already in code tags', () => {
spyOn(window.gl.utils, 'insertText').and.callFake((el, textFunc) => {
const insertedText = textFunc('This is code: ', '');
expect(insertedText).toEqual('`code`');
});
callPasteGFM();
});
it('does not wrap pasted code when already in code tags', () => {
spyOn(window.gl.utils, 'insertText').and.callFake((el, textFunc) => {
const insertedText = textFunc('This is code: `', '`');
expect(insertedText).toEqual('code');
});
callPasteGFM();
});
});
});
})();
import '~/copy_as_gfm';
import initCopyAsGFM from '~/behaviors/copy_as_gfm';
import ShortcutsIssuable from '~/shortcuts_issuable';
initCopyAsGFM();
describe('ShortcutsIssuable', () => {
const fixtureName = 'merge_requests/diff_comment.html.raw';
preloadFixtures(fixtureName);
......
......@@ -147,22 +147,6 @@ deploy_keys:
- user
- deploy_keys_projects
- projects
clusters:
- application_helm
- cluster_projects
- projects
- user
- provider_gcp
- platform_kubernetes
cluster_projects:
- projects
- clusters
provider_gcp:
- cluster
platform_kubernetes:
- cluster
application_helm:
- cluster
services:
- project
- service_hook
......
......@@ -28,7 +28,7 @@ RSpec.describe Clusters::Applications::Helm, type: :model do
describe 'status state machine' do
describe '#make_installing' do
subject { create(:applications_helm, :scheduled) }
subject { create(:cluster_applications_helm, :scheduled) }
it 'is installing' do
subject.make_installing!
......@@ -38,7 +38,7 @@ RSpec.describe Clusters::Applications::Helm, type: :model do
end
describe '#make_installed' do
subject { create(:applications_helm, :installing) }
subject { create(:cluster_applications_helm, :installing) }
it 'is installed' do
subject.make_installed
......@@ -48,7 +48,7 @@ RSpec.describe Clusters::Applications::Helm, type: :model do
end
describe '#make_errored' do
subject { create(:applications_helm, :installing) }
subject { create(:cluster_applications_helm, :installing) }
let(:reason) { 'some errors' }
it 'is errored' do
......@@ -60,7 +60,7 @@ RSpec.describe Clusters::Applications::Helm, type: :model do
end
describe '#make_scheduled' do
subject { create(:applications_helm, :installable) }
subject { create(:cluster_applications_helm, :installable) }
it 'is scheduled' do
subject.make_scheduled
......@@ -69,7 +69,7 @@ RSpec.describe Clusters::Applications::Helm, type: :model do
end
describe 'when was errored' do
subject { create(:applications_helm, :errored) }
subject { create(:cluster_applications_helm, :errored) }
it 'clears #status_reason' do
expect(subject.status_reason).not_to be_nil
......
......@@ -2,7 +2,7 @@ require 'spec_helper'
describe ClusterApplicationEntity do
describe '#as_json' do
let(:application) { build(:applications_helm) }
let(:application) { build(:cluster_applications_helm) }
subject { described_class.new(application).as_json }
it 'has name' do
......@@ -18,7 +18,7 @@ describe ClusterApplicationEntity do
end
context 'when application is errored' do
let(:application) { build(:applications_helm, :errored) }
let(:application) { build(:cluster_applications_helm, :errored) }
it 'has corresponded data' do
expect(subject[:status]).to eq(:errored)
......
......@@ -29,8 +29,8 @@ describe ClusterEntity do
context 'when provider type is user' do
let(:cluster) { create(:cluster, provider_type: :user) }
it 'has nil' do
expect(subject[:status]).to be_nil
it 'has corresponded data' do
expect(subject[:status]).to eq(:created)
expect(subject[:status_reason]).to be_nil
end
end
......
......@@ -3,14 +3,14 @@ require 'spec_helper'
describe Clusters::Applications::CheckInstallationProgressService do
RESCHEDULE_PHASES = Gitlab::Kubernetes::Pod::PHASES - [Gitlab::Kubernetes::Pod::SUCCEEDED, Gitlab::Kubernetes::Pod::FAILED].freeze
let(:application) { create(:applications_helm, :installing) }
let(:application) { create(:cluster_applications_helm, :installing) }
let(:service) { described_class.new(application) }
let(:phase) { Gitlab::Kubernetes::Pod::UNKNOWN }
let(:errors) { nil }
shared_examples 'a terminated installation' do
it 'finalize the installation' do
expect(service).to receive(:finalize_installation).once
it 'removes the installation POD' do
expect(service).to receive(:remove_installation_pod).once
service.execute
end
......@@ -23,7 +23,7 @@ describe Clusters::Applications::CheckInstallationProgressService do
context 'when not timeouted' do
it 'reschedule a new check' do
expect(ClusterWaitForAppInstallationWorker).to receive(:perform_in).once
expect(service).not_to receive(:finalize_installation)
expect(service).not_to receive(:remove_installation_pod)
service.execute
......@@ -33,7 +33,7 @@ describe Clusters::Applications::CheckInstallationProgressService do
end
context 'when timeouted' do
let(:application) { create(:applications_helm, :timeouted) }
let(:application) { create(:cluster_applications_helm, :timeouted) }
it_behaves_like 'a terminated installation'
......@@ -53,7 +53,7 @@ describe Clusters::Applications::CheckInstallationProgressService do
expect(service).to receive(:installation_phase).once.and_return(phase)
allow(service).to receive(:installation_errors).and_return(errors)
allow(service).to receive(:finalize_installation).and_return(nil)
allow(service).to receive(:remove_installation_pod).and_return(nil)
end
describe '#execute' do
......@@ -61,6 +61,15 @@ describe Clusters::Applications::CheckInstallationProgressService do
let(:phase) { Gitlab::Kubernetes::Pod::SUCCEEDED }
it_behaves_like 'a terminated installation'
it 'make the application installed' do
expect(ClusterWaitForAppInstallationWorker).not_to receive(:perform_in)
service.execute
expect(application).to be_installed
expect(application.status_reason).to be_nil
end
end
context 'when installation POD failed' do
......
require 'spec_helper'
describe Clusters::Applications::FinalizeInstallationService do
describe '#execute' do
let(:application) { create(:applications_helm, :installing) }
let(:service) { described_class.new(application) }
before do
expect_any_instance_of(Gitlab::Kubernetes::Helm).to receive(:delete_installation_pod!).with(application)
end
context 'when installation POD succeeded' do
it 'make the application installed' do
service.execute
expect(application).to be_installed
expect(application.status_reason).to be_nil
end
end
context 'when installation POD failed' do
let(:application) { create(:applications_helm, :errored) }
it 'make the application errored' do
service.execute
expect(application).to be_errored
expect(application.status_reason).not_to be_nil
end
end
end
end
......@@ -2,16 +2,22 @@ require 'spec_helper'
describe Clusters::Applications::InstallService do
describe '#execute' do
let(:application) { create(:applications_helm, :scheduled) }
let(:application) { create(:cluster_applications_helm, :scheduled) }
let(:service) { described_class.new(application) }
let(:helm_client) { instance_double(Gitlab::Kubernetes::Helm) }
before do
allow(service).to receive(:helm_api).and_return(helm_client)
end
context 'when there are no errors' do
before do
expect_any_instance_of(Gitlab::Kubernetes::Helm).to receive(:install).with(application)
expect(helm_client).to receive(:install).with(application)
allow(ClusterWaitForAppInstallationWorker).to receive(:perform_in).and_return(nil)
end
it 'make the application installing' do
expect(application.cluster).not_to be_nil
service.execute
expect(application).to be_installing
......@@ -27,7 +33,7 @@ describe Clusters::Applications::InstallService do
context 'when k8s cluster communication fails' do
before do
error = KubeException.new(500, 'system failure', nil)
expect_any_instance_of(Gitlab::Kubernetes::Helm).to receive(:install).with(application).and_raise(error)
expect(helm_client).to receive(:install).with(application).and_raise(error)
end
it 'make the application errored' do
......@@ -39,11 +45,11 @@ describe Clusters::Applications::InstallService do
end
context 'when application cannot be persisted' do
let(:application) { build(:applications_helm, :scheduled) }
let(:application) { build(:cluster_applications_helm, :scheduled) }
it 'make the application errored' do
expect(application).to receive(:make_installing!).once.and_raise(ActiveRecord::RecordInvalid)
expect_any_instance_of(Gitlab::Kubernetes::Helm).not_to receive(:install)
expect(helm_client).not_to receive(:install)
service.execute
......
......@@ -32,7 +32,7 @@ describe Clusters::Applications::ScheduleInstallationService do
end
context 'when installation is already in progress' do
let(:application) { create(:applications_helm, :installing) }
let(:application) { create(:cluster_applications_helm, :installing) }
let(:cluster) { application.cluster }
it_behaves_like 'a failing service'
......
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