Upgrade cluster applications, starting with runner

This commit is contained in:
Thong Kuah 2019-02-07 21:40:55 +00:00 committed by Douglas Barbosa Alexandre
parent e2966a6d8c
commit f67fc23727
39 changed files with 897 additions and 353 deletions

View file

@ -6,7 +6,13 @@ import Flash from '../flash';
import Poll from '../lib/utils/poll';
import initSettingsPanels from '../settings_panels';
import eventHub from './event_hub';
import { APPLICATION_STATUS, REQUEST_SUBMITTED, REQUEST_FAILURE } from './constants';
import {
APPLICATION_STATUS,
REQUEST_SUBMITTED,
REQUEST_FAILURE,
UPGRADE_REQUESTED,
UPGRADE_REQUEST_FAILURE,
} from './constants';
import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store';
import Applications from './components/applications.vue';
@ -120,11 +126,17 @@ export default class Clusters {
addListeners() {
if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken);
eventHub.$on('installApplication', this.installApplication);
eventHub.$on('upgradeApplication', data => this.upgradeApplication(data));
eventHub.$on('upgradeFailed', appId => this.upgradeFailed(appId));
eventHub.$on('dismissUpgradeSuccess', appId => this.dismissUpgradeSuccess(appId));
}
removeListeners() {
if (this.showTokenButton) this.showTokenButton.removeEventListener('click', this.showToken);
eventHub.$off('installApplication', this.installApplication);
eventHub.$off('upgradeApplication', this.upgradeApplication);
eventHub.$off('upgradeFailed', this.upgradeFailed);
eventHub.$off('dismissUpgradeSuccess', this.dismissUpgradeSuccess);
}
initPolling() {
@ -245,6 +257,21 @@ export default class Clusters {
});
}
upgradeApplication(data) {
const appId = data.id;
this.store.updateAppProperty(appId, 'requestStatus', UPGRADE_REQUESTED);
this.store.updateAppProperty(appId, 'status', APPLICATION_STATUS.UPDATING);
this.service.installApplication(appId, data.params).catch(() => this.upgradeFailed(appId));
}
upgradeFailed(appId) {
this.store.updateAppProperty(appId, 'requestStatus', UPGRADE_REQUEST_FAILURE);
}
dismissUpgradeSuccess(appId) {
this.store.updateAppProperty(appId, 'requestStatus', null);
}
destroy() {
this.destroyed = true;

View file

@ -1,15 +1,24 @@
<script>
/* eslint-disable vue/require-default-prop */
import { GlLink } from '@gitlab/ui';
import TimeagoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
import { s__, sprintf } from '../../locale';
import eventHub from '../event_hub';
import identicon from '../../vue_shared/components/identicon.vue';
import loadingButton from '../../vue_shared/components/loading_button.vue';
import { APPLICATION_STATUS, REQUEST_SUBMITTED, REQUEST_FAILURE } from '../constants';
import {
APPLICATION_STATUS,
REQUEST_SUBMITTED,
REQUEST_FAILURE,
UPGRADE_REQUESTED,
} from '../constants';
export default {
components: {
loadingButton,
identicon,
TimeagoTooltip,
GlLink,
},
props: {
id: {
@ -54,6 +63,18 @@ export default {
type: String,
required: false,
},
version: {
type: String,
required: false,
},
chartRepo: {
type: String,
required: false,
},
upgradeAvailable: {
type: Boolean,
required: false,
},
installApplicationRequestParams: {
type: Object,
required: false,
@ -78,7 +99,8 @@ export default {
return (
this.status === APPLICATION_STATUS.INSTALLED ||
this.status === APPLICATION_STATUS.UPDATED ||
this.status === APPLICATION_STATUS.UPDATING
this.status === APPLICATION_STATUS.UPDATING ||
this.status === APPLICATION_STATUS.UPDATE_ERRORED
);
},
canInstall() {
@ -146,6 +168,69 @@ export default {
title: this.title,
});
},
versionLabel() {
if (this.upgradeFailed) {
return s__('ClusterIntegration|Upgrade failed');
} else if (this.isUpgrading) {
return s__('ClusterIntegration|Upgrading');
}
return s__('ClusterIntegration|Upgraded');
},
upgradeRequested() {
return this.requestStatus === UPGRADE_REQUESTED;
},
upgradeSuccessful() {
return this.status === APPLICATION_STATUS.UPDATED;
},
upgradeFailed() {
if (this.isUpgrading) {
return false;
}
return this.status === APPLICATION_STATUS.UPDATE_ERRORED;
},
upgradeFailureDescription() {
return sprintf(
s__(
'ClusterIntegration|Something went wrong when upgrading %{title}. Please check the logs and try again.',
),
{
title: this.title,
},
);
},
upgradeSuccessDescription() {
return sprintf(s__('ClusterIntegration|%{title} upgraded successfully.'), {
title: this.title,
});
},
upgradeButtonLabel() {
let label;
if (this.upgradeAvailable && !this.upgradeFailed && !this.isUpgrading) {
label = s__('ClusterIntegration|Upgrade');
} else if (this.isUpgrading) {
label = s__('ClusterIntegration|Upgrading');
} else if (this.upgradeFailed) {
label = s__('ClusterIntegration|Retry upgrade');
}
return label;
},
isUpgrading() {
// Since upgrading is handled asynchronously on the backend we need this check to prevent any delay on the frontend
return (
this.status === APPLICATION_STATUS.UPDATING ||
(this.upgradeRequested && !this.upgradeSuccessful)
);
},
},
watch: {
status() {
if (this.status === APPLICATION_STATUS.UPDATE_ERRORED) {
eventHub.$emit('upgradeFailed', this.id);
}
},
},
methods: {
installClicked() {
@ -154,6 +239,15 @@ export default {
params: this.installApplicationRequestParams,
});
},
upgradeClicked() {
eventHub.$emit('upgradeApplication', {
id: this.id,
params: this.installApplicationRequestParams,
});
},
dismissUpgradeSuccess() {
eventHub.$emit('dismissUpgradeSuccess', this.id);
},
},
};
</script>
@ -207,6 +301,51 @@ export default {
</li>
</ul>
</div>
<div
v-if="(upgradeSuccessful || upgradeFailed) && !upgradeAvailable"
class="form-text text-muted label p-0 js-cluster-application-upgrade-details"
>
{{ versionLabel }}
<span v-if="upgradeSuccessful"> to</span>
<gl-link
v-if="upgradeSuccessful"
:href="chartRepo"
target="_blank"
class="js-cluster-application-upgrade-version"
>
chart v{{ version }}
</gl-link>
</div>
<div
v-if="upgradeFailed && !isUpgrading"
class="bs-callout bs-callout-danger cluster-application-banner mt-2 mb-0 js-cluster-application-upgrade-failure-message"
>
{{ upgradeFailureDescription }}
</div>
<div
v-if="upgradeRequested && upgradeSuccessful"
class="bs-callout bs-callout-success cluster-application-banner mt-2 mb-0 p-0 pl-3"
>
{{ upgradeSuccessDescription }}
<button class="close cluster-application-banner-close" @click="dismissUpgradeSuccess">
&times;
</button>
</div>
<loading-button
v-if="upgradeAvailable || upgradeFailed || isUpgrading"
class="btn btn-primary js-cluster-application-upgrade-button mt-2"
:loading="isUpgrading"
:disabled="isUpgrading"
:label="upgradeButtonLabel"
@click="upgradeClicked"
/>
</div>
<div
:class="{ 'section-25': showManageButton, 'section-15': !showManageButton }"

View file

@ -362,6 +362,9 @@ export default {
:status-reason="applications.runner.statusReason"
:request-status="applications.runner.requestStatus"
:request-reason="applications.runner.requestReason"
:version="applications.runner.version"
:chart-repo="applications.runner.chartRepo"
:upgrade-available="applications.runner.upgradeAvailable"
:disabled="!helmInstalled"
title-link="https://docs.gitlab.com/runner/"
>

View file

@ -12,15 +12,19 @@ export const APPLICATION_STATUS = {
SCHEDULED: 'scheduled',
INSTALLING: 'installing',
INSTALLED: 'installed',
UPDATED: 'updated',
UPDATING: 'updating',
UPDATED: 'updated',
UPDATE_ERRORED: 'update_errored',
ERROR: 'errored',
};
// These are only used client-side
export const REQUEST_SUBMITTED = 'request-submitted';
export const REQUEST_FAILURE = 'request-failure';
export const UPGRADE_REQUESTED = 'upgrade-requested';
export const UPGRADE_REQUEST_FAILURE = 'upgrade-request-failure';
export const INGRESS = 'ingress';
export const JUPYTER = 'jupyter';
export const KNATIVE = 'knative';
export const RUNNER = 'runner';
export const CERT_MANAGER = 'cert_manager';

View file

@ -1,6 +1,6 @@
import { s__ } from '../../locale';
import { parseBoolean } from '../../lib/utils/common_utils';
import { INGRESS, JUPYTER, KNATIVE, CERT_MANAGER } from '../constants';
import { INGRESS, JUPYTER, KNATIVE, CERT_MANAGER, RUNNER } from '../constants';
export default class ClusterStore {
constructor() {
@ -40,6 +40,9 @@ export default class ClusterStore {
statusReason: null,
requestStatus: null,
requestReason: null,
version: null,
chartRepo: 'https://gitlab.com/charts/gitlab-runner',
upgradeAvailable: null,
},
prometheus: {
title: s__('ClusterIntegration|Prometheus'),
@ -100,7 +103,13 @@ export default class ClusterStore {
this.state.statusReason = serverState.status_reason;
serverState.applications.forEach(serverAppEntry => {
const { name: appId, status, status_reason: statusReason } = serverAppEntry;
const {
name: appId,
status,
status_reason: statusReason,
version,
update_available: upgradeAvailable,
} = serverAppEntry;
this.state.applications[appId] = {
...(this.state.applications[appId] || {}),
@ -124,6 +133,9 @@ export default class ClusterStore {
serverAppEntry.hostname || this.state.applications.knative.hostname;
this.state.applications.knative.externalIp =
serverAppEntry.external_ip || this.state.applications.knative.externalIp;
} else if (appId === RUNNER) {
this.state.applications.runner.version = version;
this.state.applications.runner.upgradeAvailable = upgradeAvailable;
}
});
}

View file

@ -58,6 +58,20 @@
}
}
.cluster-application-banner {
height: 45px;
display: flex;
align-items: center;
justify-content: space-between;
}
.cluster-application-banner-close {
align-self: flex-start;
font-weight: 500;
font-size: 20px;
margin: $gl-padding-8 14px 0 0;
}
.cluster-application-description {
flex: 1;
}

View file

@ -53,11 +53,11 @@ module Clusters
end
def upgrade_command(values)
::Gitlab::Kubernetes::Helm::UpgradeCommand.new(
name,
::Gitlab::Kubernetes::Helm::InstallCommand.new(
name: name,
version: VERSION,
chart: chart,
rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
files: files_with_replaced_values(values)
)
end

View file

@ -20,7 +20,7 @@ module Clusters
state :update_errored, value: 6
event :make_scheduled do
transition [:installable, :errored] => :scheduled
transition [:installable, :errored, :installed, :updated, :update_errored] => :scheduled
end
event :make_installing do
@ -29,16 +29,19 @@ module Clusters
event :make_installed do
transition [:installing] => :installed
transition [:updating] => :updated
end
event :make_errored do
transition any => :errored
transition any - [:updating] => :errored
transition [:updating] => :update_errored
end
event :make_updating do
transition [:installed, :updated, :update_errored] => :updating
transition [:installed, :updated, :update_errored, :scheduled] => :updating
end
# Deprecated
event :make_updated do
transition [:updating] => :updated
end
@ -74,6 +77,10 @@ module Clusters
end
end
def updateable?
installed? || updated? || update_errored?
end
def available?
installed? || updated?
end

View file

@ -12,6 +12,10 @@ module Clusters
end
end
end
def update_available?
version != self.class.const_get(:VERSION)
end
end
end
end

View file

@ -8,4 +8,5 @@ class ClusterApplicationEntity < Grape::Entity
expose :external_ip, if: -> (e, _) { e.respond_to?(:external_ip) }
expose :hostname, if: -> (e, _) { e.respond_to?(:hostname) }
expose :email, if: -> (e, _) { e.respond_to?(:email) }
expose :update_available?, as: :update_available, if: -> (e, _) { e.respond_to?(:update_available?) }
end

View file

@ -4,7 +4,7 @@ module Clusters
module Applications
class CheckInstallationProgressService < BaseHelmService
def execute
return unless app.installing?
return unless operation_in_progress?
case installation_phase
when Gitlab::Kubernetes::Pod::SUCCEEDED
@ -16,11 +16,16 @@ module Clusters
end
rescue Kubeclient::HttpError => e
log_error(e)
app.make_errored!("Kubernetes error: #{e.error_code}") unless app.errored?
app.make_errored!("Kubernetes error: #{e.error_code}")
end
private
def operation_in_progress?
app.installing? || app.updating?
end
def on_success
app.make_installed!
ensure
@ -28,13 +33,13 @@ module Clusters
end
def on_failed
app.make_errored!("Installation failed. Check pod logs for #{install_command.pod_name} for more details.")
app.make_errored!("Operation failed. Check pod logs for #{pod_name} for more details.")
end
def check_timeout
if timeouted?
begin
app.make_errored!("Installation timed out. Check pod logs for #{install_command.pod_name} for more details.")
app.make_errored!("Operation timed out. Check pod logs for #{pod_name} for more details.")
end
else
ClusterWaitForAppInstallationWorker.perform_in(
@ -42,20 +47,24 @@ module Clusters
end
end
def pod_name
install_command.pod_name
end
def timeouted?
Time.now.utc - app.updated_at.to_time.utc > ClusterWaitForAppInstallationWorker::TIMEOUT
end
def remove_installation_pod
helm_api.delete_pod!(install_command.pod_name)
helm_api.delete_pod!(pod_name)
end
def installation_phase
helm_api.status(install_command.pod_name)
helm_api.status(pod_name)
end
def installation_errors
helm_api.log(install_command.pod_name)
helm_api.log(pod_name)
end
end
end

View file

@ -10,6 +10,18 @@ module Clusters
end
def execute
application.updateable? ? schedule_upgrade : schedule_install
end
private
def schedule_upgrade
application.make_scheduled!
ClusterUpgradeAppWorker.perform_async(application.name, application.id)
end
def schedule_install
application.make_scheduled!
ClusterInstallAppWorker.perform_async(application.name, application.id)

View file

@ -0,0 +1,28 @@
# frozen_string_literal: true
module Clusters
module Applications
class UpgradeService < BaseHelmService
def execute
return unless app.scheduled?
begin
app.make_updating!
# install_command works with upgrades too
# as it basically does `helm upgrade --install`
helm_api.update(install_command)
ClusterWaitForAppInstallationWorker.perform_in(
ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id)
rescue Kubeclient::HttpError => e
log_error(e)
app.make_update_errored!("Kubernetes error: #{e.error_code}")
rescue StandardError => e
log_error(e)
app.make_update_errored!("Can't start upgrade process.")
end
end
end
end
end

View file

@ -23,6 +23,7 @@
- cronjob:prune_web_hook_logs
- gcp_cluster:cluster_install_app
- gcp_cluster:cluster_upgrade_app
- gcp_cluster:cluster_provision
- gcp_cluster:cluster_wait_for_app_installation
- gcp_cluster:wait_for_cluster_creation

View file

@ -0,0 +1,13 @@
# frozen_string_literal: true
class ClusterUpgradeAppWorker
include ApplicationWorker
include ClusterQueue
include ClusterApplications
def perform(app_name, app_id)
find_application(app_name, app_id) do |app|
Clusters::Applications::UpgradeService.new(app).execute
end
end
end

View file

@ -0,0 +1,5 @@
---
title: Added ability to upgrade cluster applications
merge_request: 24789
author:
type: added

View file

@ -20,14 +20,7 @@ module Gitlab
kubeclient.create_pod(command.pod_resource)
end
def update(command)
namespace.ensure_exists!
update_config_map(command)
delete_pod!(command.pod_name)
kubeclient.create_pod(command.pod_resource)
end
alias_method :update, :install
##
# Returns Pod phase
@ -62,6 +55,8 @@ module Gitlab
def create_config_map(command)
command.config_map_resource.tap do |config_map_resource|
break unless config_map_resource
if config_map_exists?(config_map_resource)
kubeclient.update_config_map(config_map_resource)
else

View file

@ -42,8 +42,17 @@ module Gitlab
'helm repo update' if repository
end
# Uses `helm upgrade --install` which means we can use this for both
# installation and uprade of applications
def install_command
command = ['helm', 'install', chart] + install_command_flags
command = ['helm', 'upgrade', name, chart] +
install_flag +
reset_values_flag +
optional_tls_flags +
optional_version_flag +
rbac_create_flag +
namespace_flag +
value_flag
command.shelljoin
end
@ -56,17 +65,20 @@ module Gitlab
postinstall.join("\n") if postinstall
end
def install_command_flags
name_flag = ['--name', name]
namespace_flag = ['--namespace', Gitlab::Kubernetes::Helm::NAMESPACE]
value_flag = ['-f', "/data/helm/#{name}/config/values.yaml"]
def install_flag
['--install']
end
name_flag +
optional_tls_flags +
optional_version_flag +
rbac_create_flag +
namespace_flag +
value_flag
def reset_values_flag
['--reset-values']
end
def value_flag
['-f', "/data/helm/#{name}/config/values.yaml"]
end
def namespace_flag
['--namespace', Gitlab::Kubernetes::Helm::NAMESPACE]
end
def rbac_create_flag

View file

@ -1,65 +0,0 @@
# frozen_string_literal: true
module Gitlab
module Kubernetes
module Helm
class UpgradeCommand
include BaseCommand
include ClientCommand
attr_reader :name, :chart, :version, :repository, :files
def initialize(name, chart:, files:, rbac:, version: nil, repository: nil)
@name = name
@chart = chart
@rbac = rbac
@version = version
@files = files
@repository = repository
end
def generate_script
super + [
init_command,
wait_for_tiller_command,
repository_command,
script_command
].compact.join("\n")
end
def rbac?
@rbac
end
def pod_name
"upgrade-#{name}"
end
private
def script_command
upgrade_flags = "#{optional_version_flag}#{optional_tls_flags}" \
" --reset-values" \
" --install" \
" --namespace #{::Gitlab::Kubernetes::Helm::NAMESPACE}" \
" -f /data/helm/#{name}/config/values.yaml"
"helm upgrade #{name} #{chart}#{upgrade_flags}"
end
def optional_version_flag
" --version #{version}" if version
end
def optional_tls_flags
return unless files.key?(:'ca.pem')
" --tls" \
" --tls-ca-cert #{files_dir}/ca.pem" \
" --tls-cert #{files_dir}/cert.pem" \
" --tls-key #{files_dir}/key.pem"
end
end
end
end
end

View file

@ -1554,6 +1554,9 @@ msgstr ""
msgid "ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which may incur additional costs depending on the hosting provider your Kubernetes cluster is installed on. If you are using Google Kubernetes Engine, you can %{pricingLink}."
msgstr ""
msgid "ClusterIntegration|%{title} upgraded successfully."
msgstr ""
msgid "ClusterIntegration|API URL"
msgstr ""
@ -1878,6 +1881,9 @@ msgstr ""
msgid "ClusterIntegration|Request to begin installing failed"
msgstr ""
msgid "ClusterIntegration|Retry upgrade"
msgstr ""
msgid "ClusterIntegration|Save changes"
msgstr ""
@ -1920,6 +1926,9 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong on our end."
msgstr ""
msgid "ClusterIntegration|Something went wrong when upgrading %{title}. Please check the logs and try again."
msgstr ""
msgid "ClusterIntegration|Something went wrong while creating your Kubernetes cluster on Google Kubernetes Engine"
msgstr ""
@ -1944,6 +1953,18 @@ msgstr ""
msgid "ClusterIntegration|Token"
msgstr ""
msgid "ClusterIntegration|Upgrade"
msgstr ""
msgid "ClusterIntegration|Upgrade failed"
msgstr ""
msgid "ClusterIntegration|Upgraded"
msgstr ""
msgid "ClusterIntegration|Upgrading"
msgstr ""
msgid "ClusterIntegration|Validating project billing status"
msgstr ""

View file

@ -34,7 +34,8 @@
"status_reason": { "type": ["string", "null"] },
"external_ip": { "type": ["string", "null"] },
"hostname": { "type": ["string", "null"] },
"email": { "type": ["string", "null"] }
"email": { "type": ["string", "null"] },
"update_available": { "type": ["boolean", "null"] }
},
"required" : [ "name", "status" ]
}

View file

@ -208,6 +208,144 @@ describe('Application Row', () => {
});
});
describe('Upgrade button', () => {
it('has indeterminate state on page load', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: null,
});
const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button');
expect(upgradeBtn).toBe(null);
});
it('has enabled "Upgrade" when "upgradeAvailable" is true', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
upgradeAvailable: true,
});
const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button');
expect(upgradeBtn).not.toBe(null);
expect(upgradeBtn.innerHTML).toContain('Upgrade');
});
it('has enabled "Retry upgrade" when APPLICATION_STATUS.UPDATE_ERRORED', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.UPDATE_ERRORED,
});
const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button');
expect(upgradeBtn).not.toBe(null);
expect(vm.upgradeFailed).toBe(true);
expect(upgradeBtn.innerHTML).toContain('Retry upgrade');
});
it('has disabled "Retry upgrade" when APPLICATION_STATUS.UPDATING', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.UPDATING,
});
const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button');
expect(upgradeBtn).not.toBe(null);
expect(vm.isUpgrading).toBe(true);
expect(upgradeBtn.innerHTML).toContain('Upgrading');
});
it('clicking upgrade button emits event', () => {
spyOn(eventHub, '$emit');
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.UPDATE_ERRORED,
});
const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button');
upgradeBtn.click();
expect(eventHub.$emit).toHaveBeenCalledWith('upgradeApplication', {
id: DEFAULT_APPLICATION_STATE.id,
params: {},
});
});
it('clicking disabled upgrade button emits nothing', () => {
spyOn(eventHub, '$emit');
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.UPDATING,
});
const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button');
upgradeBtn.click();
expect(eventHub.$emit).not.toHaveBeenCalled();
});
it('displays an error message if application upgrade failed', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
title: 'GitLab Runner',
status: APPLICATION_STATUS.UPDATE_ERRORED,
});
const failureMessage = vm.$el.querySelector(
'.js-cluster-application-upgrade-failure-message',
);
expect(failureMessage).not.toBe(null);
expect(failureMessage.innerHTML).toContain(
'Something went wrong when upgrading GitLab Runner. Please check the logs and try again.',
);
});
});
describe('Version', () => {
it('displays a version number if application has been upgraded', () => {
const version = '0.1.45';
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.UPDATED,
version,
});
const upgradeDetails = vm.$el.querySelector('.js-cluster-application-upgrade-details');
const versionEl = vm.$el.querySelector('.js-cluster-application-upgrade-version');
expect(upgradeDetails.innerHTML).toContain('Upgraded');
expect(versionEl).not.toBe(null);
expect(versionEl.innerHTML).toContain(version);
});
it('contains a link to the chart repo if application has been upgraded', () => {
const version = '0.1.45';
const chartRepo = 'https://gitlab.com/charts/gitlab-runner';
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.UPDATED,
chartRepo,
version,
});
const versionEl = vm.$el.querySelector('.js-cluster-application-upgrade-version');
expect(versionEl.href).toEqual(chartRepo);
expect(versionEl.target).toEqual('_blank');
});
it('does not display a version number if application upgrade failed', () => {
const version = '0.1.45';
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.UPDATE_ERRORED,
version,
});
const upgradeDetails = vm.$el.querySelector('.js-cluster-application-upgrade-details');
const versionEl = vm.$el.querySelector('.js-cluster-application-upgrade-version');
expect(upgradeDetails.innerHTML).toContain('failed');
expect(versionEl).toBe(null);
});
});
describe('Error block', () => {
it('does not show error block when there is no error', () => {
vm = mountComponent(ApplicationRow, {

View file

@ -85,6 +85,9 @@ describe('Clusters Store', () => {
statusReason: mockResponseData.applications[2].status_reason,
requestStatus: null,
requestReason: null,
version: mockResponseData.applications[2].version,
upgradeAvailable: mockResponseData.applications[2].update_available,
chartRepo: 'https://gitlab.com/charts/gitlab-runner',
},
prometheus: {
title: 'Prometheus',

View file

@ -171,51 +171,6 @@ describe Gitlab::Kubernetes::Helm::Api do
end
end
describe '#update' do
let(:rbac) { false }
let(:command) do
Gitlab::Kubernetes::Helm::UpgradeCommand.new(
application_name,
chart: 'chart-name',
files: files,
rbac: rbac
)
end
before do
allow(namespace).to receive(:ensure_exists!).once
allow(client).to receive(:update_config_map).and_return(nil)
allow(client).to receive(:create_pod).and_return(nil)
allow(client).to receive(:delete_pod).and_return(nil)
end
it 'ensures the namespace exists before creating the pod' do
expect(namespace).to receive(:ensure_exists!).once.ordered
expect(client).to receive(:create_pod).once.ordered
subject.update(command)
end
it 'removes an existing pod before updating' do
expect(client).to receive(:delete_pod).with('upgrade-app-name', 'gitlab-managed-apps').once.ordered
expect(client).to receive(:create_pod).once.ordered
subject.update(command)
end
it 'updates the config map on kubeclient when one exists' do
resource = Gitlab::Kubernetes::ConfigMap.new(
application_name, files
).generate
expect(client).to receive(:update_config_map).with(resource).once
subject.update(command)
end
end
describe '#status' do
let(:phase) { Gitlab::Kubernetes::Pod::RUNNING }
let(:pod) { Kubeclient::Resource.new(status: { phase: phase }) } # partial representation

View file

@ -21,6 +21,15 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
)
end
let(:tls_flags) do
<<~EOS.squish
--tls
--tls-ca-cert /data/helm/app-name/config/ca.pem
--tls-cert /data/helm/app-name/config/cert.pem
--tls-key /data/helm/app-name/config/key.pem
EOS
end
subject { install_command }
it_behaves_like 'helm commands' do
@ -36,12 +45,10 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
let(:helm_install_comand) do
<<~EOS.squish
helm install chart-name
--name app-name
--tls
--tls-ca-cert /data/helm/app-name/config/ca.pem
--tls-cert /data/helm/app-name/config/cert.pem
--tls-key /data/helm/app-name/config/key.pem
helm upgrade app-name chart-name
--install
--reset-values
#{tls_flags}
--version 1.2.3
--set rbac.create\\=false,rbac.enabled\\=false
--namespace gitlab-managed-apps
@ -66,12 +73,10 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
let(:helm_install_command) do
<<~EOS.squish
helm install chart-name
--name app-name
--tls
--tls-ca-cert /data/helm/app-name/config/ca.pem
--tls-cert /data/helm/app-name/config/cert.pem
--tls-key /data/helm/app-name/config/key.pem
helm upgrade app-name chart-name
--install
--reset-values
#{tls_flags}
--version 1.2.3
--set rbac.create\\=true,rbac.enabled\\=true
--namespace gitlab-managed-apps
@ -95,12 +100,10 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
let(:helm_install_command) do
<<~EOS.squish
helm install chart-name
--name app-name
--tls
--tls-ca-cert /data/helm/app-name/config/ca.pem
--tls-cert /data/helm/app-name/config/cert.pem
--tls-key /data/helm/app-name/config/key.pem
helm upgrade app-name chart-name
--install
--reset-values
#{tls_flags}
--version 1.2.3
--set rbac.create\\=false,rbac.enabled\\=false
--namespace gitlab-managed-apps
@ -120,15 +123,22 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
for i in $(seq 1 30); do helm version && break; sleep 1s; echo "Retrying ($i)..."; done
helm repo add app-name https://repository.example.com
helm repo update
/bin/date
/bin/true
#{helm_install_command}
EOS
end
let(:helm_install_command) do
<<~EOS.strip
/bin/date
/bin/true
helm install chart-name --name app-name --tls --tls-ca-cert /data/helm/app-name/config/ca.pem --tls-cert /data/helm/app-name/config/cert.pem --tls-key /data/helm/app-name/config/key.pem --version 1.2.3 --set rbac.create\\=false,rbac.enabled\\=false --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml
<<~EOS.squish
helm upgrade app-name chart-name
--install
--reset-values
#{tls_flags}
--version 1.2.3
--set rbac.create\\=false,rbac.enabled\\=false
--namespace gitlab-managed-apps
-f /data/helm/app-name/config/values.yaml
EOS
end
end
@ -145,14 +155,21 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
helm repo add app-name https://repository.example.com
helm repo update
#{helm_install_command}
/bin/date
/bin/false
EOS
end
let(:helm_install_command) do
<<~EOS.strip
helm install chart-name --name app-name --tls --tls-ca-cert /data/helm/app-name/config/ca.pem --tls-cert /data/helm/app-name/config/cert.pem --tls-key /data/helm/app-name/config/key.pem --version 1.2.3 --set rbac.create\\=false,rbac.enabled\\=false --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml
/bin/date
/bin/false
<<~EOS.squish
helm upgrade app-name chart-name
--install
--reset-values
#{tls_flags}
--version 1.2.3
--set rbac.create\\=false,rbac.enabled\\=false
--namespace gitlab-managed-apps
-f /data/helm/app-name/config/values.yaml
EOS
end
end
@ -174,8 +191,9 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
let(:helm_install_command) do
<<~EOS.squish
helm install chart-name
--name app-name
helm upgrade app-name chart-name
--install
--reset-values
--version 1.2.3
--set rbac.create\\=false,rbac.enabled\\=false
--namespace gitlab-managed-apps
@ -201,12 +219,10 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
let(:helm_install_command) do
<<~EOS.squish
helm install chart-name
--name app-name
--tls
--tls-ca-cert /data/helm/app-name/config/ca.pem
--tls-cert /data/helm/app-name/config/cert.pem
--tls-key /data/helm/app-name/config/key.pem
helm upgrade app-name chart-name
--install
--reset-values
#{tls_flags}
--set rbac.create\\=false,rbac.enabled\\=false
--namespace gitlab-managed-apps
-f /data/helm/app-name/config/values.yaml

View file

@ -1,140 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
describe Gitlab::Kubernetes::Helm::UpgradeCommand do
let(:application) { build(:clusters_applications_prometheus) }
let(:files) { { 'ca.pem': 'some file content' } }
let(:namespace) { ::Gitlab::Kubernetes::Helm::NAMESPACE }
let(:rbac) { false }
let(:upgrade_command) do
described_class.new(
application.name,
chart: application.chart,
files: files,
rbac: rbac
)
end
subject { upgrade_command }
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
helm init --upgrade
for i in $(seq 1 30); do helm version && break; sleep 1s; echo "Retrying ($i)..."; done
helm upgrade #{application.name} #{application.chart} --tls --tls-ca-cert /data/helm/#{application.name}/config/ca.pem --tls-cert /data/helm/#{application.name}/config/cert.pem --tls-key /data/helm/#{application.name}/config/key.pem --reset-values --install --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml
EOS
end
end
context 'rbac is true' do
let(:rbac) { true }
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
helm init --upgrade
for i in $(seq 1 30); do helm version && break; sleep 1s; echo "Retrying ($i)..."; done
helm upgrade #{application.name} #{application.chart} --tls --tls-ca-cert /data/helm/#{application.name}/config/ca.pem --tls-cert /data/helm/#{application.name}/config/cert.pem --tls-key /data/helm/#{application.name}/config/key.pem --reset-values --install --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml
EOS
end
end
end
context 'with an application with a repository' do
let(:ci_runner) { create(:ci_runner) }
let(:application) { build(:clusters_applications_runner, runner: ci_runner) }
let(:upgrade_command) do
described_class.new(
application.name,
chart: application.chart,
files: files,
rbac: rbac,
repository: application.repository
)
end
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
helm init --upgrade
for i in $(seq 1 30); do helm version && break; sleep 1s; echo "Retrying ($i)..."; done
helm repo add #{application.name} #{application.repository}
helm upgrade #{application.name} #{application.chart} --tls --tls-ca-cert /data/helm/#{application.name}/config/ca.pem --tls-cert /data/helm/#{application.name}/config/cert.pem --tls-key /data/helm/#{application.name}/config/key.pem --reset-values --install --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml
EOS
end
end
end
context 'when there is no ca.pem file' do
let(:files) { { 'file.txt': 'some content' } }
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
helm init --upgrade
for i in $(seq 1 30); do helm version && break; sleep 1s; echo "Retrying ($i)..."; done
helm upgrade #{application.name} #{application.chart} --reset-values --install --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml
EOS
end
end
end
describe '#pod_resource' do
subject { upgrade_command.pod_resource }
context 'rbac is enabled' do
let(:rbac) { true }
it 'generates a pod that uses the tiller serviceAccountName' do
expect(subject.spec.serviceAccountName).to eq('tiller')
end
end
context 'rbac is not enabled' do
let(:rbac) { false }
it 'generates a pod that uses the default serviceAccountName' do
expect(subject.spec.serviceAcccountName).to be_nil
end
end
end
describe '#config_map_resource' do
let(:metadata) do
{
name: "values-content-configuration-#{application.name}",
namespace: namespace,
labels: { name: "values-content-configuration-#{application.name}" }
}
end
let(:resource) { ::Kubeclient::Resource.new(metadata: metadata, data: files) }
it 'returns a KubeClient resource with config map content for the application' do
expect(subject.config_map_resource).to eq(resource)
end
end
describe '#rbac?' do
subject { upgrade_command.rbac? }
context 'rbac is enabled' do
let(:rbac) { true }
it { is_expected.to be_truthy }
end
context 'rbac is not enabled' do
let(:rbac) { false }
it { is_expected.to be_falsey }
end
end
describe '#pod_name' do
it 'returns the pod name' do
expect(subject.pod_name).to eq("upgrade-#{application.name}")
end
end
end

View file

@ -5,6 +5,7 @@ describe Clusters::Applications::CertManager do
include_examples 'cluster application core specs', :clusters_applications_cert_managers
include_examples 'cluster application status specs', :clusters_applications_cert_managers
include_examples 'cluster application version specs', :clusters_applications_cert_managers
include_examples 'cluster application initial status specs'
describe '#install_command' do

View file

@ -7,6 +7,7 @@ describe Clusters::Applications::Ingress do
include_examples 'cluster application core specs', :clusters_applications_ingress
include_examples 'cluster application status specs', :clusters_applications_ingress
include_examples 'cluster application version specs', :clusters_applications_ingress
include_examples 'cluster application helm specs', :clusters_applications_ingress
include_examples 'cluster application initial status specs'

View file

@ -3,6 +3,7 @@ require 'rails_helper'
describe Clusters::Applications::Jupyter do
include_examples 'cluster application core specs', :clusters_applications_jupyter
include_examples 'cluster application status specs', :clusters_applications_jupyter
include_examples 'cluster application version specs', :clusters_applications_jupyter
include_examples 'cluster application helm specs', :clusters_applications_jupyter
it { is_expected.to belong_to(:oauth_application) }

View file

@ -9,6 +9,7 @@ describe Clusters::Applications::Knative do
include_examples 'cluster application core specs', :clusters_applications_knative
include_examples 'cluster application status specs', :clusters_applications_knative
include_examples 'cluster application helm specs', :clusters_applications_knative
include_examples 'cluster application version specs', :clusters_applications_knative
include_examples 'cluster application initial status specs'
before do

View file

@ -5,6 +5,7 @@ describe Clusters::Applications::Prometheus do
include_examples 'cluster application core specs', :clusters_applications_prometheus
include_examples 'cluster application status specs', :clusters_applications_prometheus
include_examples 'cluster application version specs', :clusters_applications_prometheus
include_examples 'cluster application helm specs', :clusters_applications_prometheus
include_examples 'cluster application initial status specs'
@ -206,8 +207,8 @@ describe Clusters::Applications::Prometheus do
let(:prometheus) { build(:clusters_applications_prometheus) }
let(:values) { prometheus.values }
it 'returns an instance of Gitlab::Kubernetes::Helm::GetCommand' do
expect(prometheus.upgrade_command(values)).to be_an_instance_of(::Gitlab::Kubernetes::Helm::UpgradeCommand)
it 'returns an instance of Gitlab::Kubernetes::Helm::InstallCommand' do
expect(prometheus.upgrade_command(values)).to be_an_instance_of(::Gitlab::Kubernetes::Helm::InstallCommand)
end
it 'should be initialized with 3 arguments' do

View file

@ -5,6 +5,7 @@ describe Clusters::Applications::Runner do
include_examples 'cluster application core specs', :clusters_applications_runner
include_examples 'cluster application status specs', :clusters_applications_runner
include_examples 'cluster application version specs', :clusters_applications_runner
include_examples 'cluster application helm specs', :clusters_applications_runner
include_examples 'cluster application initial status specs'

View file

@ -21,6 +21,14 @@ describe ClusterApplicationEntity do
expect(subject[:status_reason]).to be_nil
end
context 'non-helm application' do
let(:application) { build(:clusters_applications_runner, version: '0.0.0') }
it 'has update_available' do
expect(subject[:update_available]).to be_truthy
end
end
context 'when application is errored' do
let(:application) { build(:clusters_applications_helm, :errored) }

View file

@ -1,6 +1,6 @@
require 'spec_helper'
describe Clusters::Applications::CheckInstallationProgressService do
describe Clusters::Applications::CheckInstallationProgressService, '#execute' do
RESCHEDULE_PHASES = Gitlab::Kubernetes::Pod::PHASES - [Gitlab::Kubernetes::Pod::SUCCEEDED, Gitlab::Kubernetes::Pod::FAILED].freeze
let(:application) { create(:clusters_applications_helm, :installing) }
@ -21,24 +21,39 @@ describe Clusters::Applications::CheckInstallationProgressService do
expect(ClusterWaitForAppInstallationWorker).to receive(:perform_in).once
expect(service).not_to receive(:remove_installation_pod)
service.execute
expect do
service.execute
application.reload
end.not_to change(application, :status)
expect(application).to be_installing
expect(application.status_reason).to be_nil
end
end
end
end
context 'when timeouted' do
let(:application) { create(:clusters_applications_helm, :timeouted) }
shared_examples 'error logging' do
context 'when installation raises a Kubeclient::HttpError' do
let(:cluster) { create(:cluster, :provided_by_user, :project) }
it 'make the application errored' do
expect(ClusterWaitForAppInstallationWorker).not_to receive(:perform_in)
before do
application.update!(cluster: cluster)
service.execute
expect(service).to receive(:installation_phase).and_raise(Kubeclient::HttpError.new(401, 'Unauthorized', nil))
end
expect(application).to be_errored
expect(application.status_reason).to eq("Installation timed out. Check pod logs for install-helm for more details.")
end
it 'shows the response code from the error' do
service.execute
expect(application).to be_errored.or(be_update_errored)
expect(application.status_reason).to eq('Kubernetes error: 401')
end
it 'should log error' do
expect(service.send(:logger)).to receive(:error)
service.execute
end
end
end
@ -48,10 +63,76 @@ describe Clusters::Applications::CheckInstallationProgressService do
allow(service).to receive(:remove_installation_pod).and_return(nil)
end
describe '#execute' do
context 'when application is updating' do
let(:application) { create(:clusters_applications_helm, :updating) }
include_examples 'error logging'
RESCHEDULE_PHASES.each { |phase| it_behaves_like 'a not yet terminated installation', phase }
context 'when installation POD succeeded' do
let(:phase) { Gitlab::Kubernetes::Pod::SUCCEEDED }
before do
expect(service).to receive(:installation_phase).once.and_return(phase)
end
it 'removes the installation POD' do
expect(service).to receive(:remove_installation_pod).once
service.execute
end
it 'make the application installed' do
expect(ClusterWaitForAppInstallationWorker).not_to receive(:perform_in)
service.execute
expect(application).to be_updated
expect(application.status_reason).to be_nil
end
end
context 'when installation POD failed' do
let(:phase) { Gitlab::Kubernetes::Pod::FAILED }
let(:errors) { 'test installation failed' }
before do
expect(service).to receive(:installation_phase).once.and_return(phase)
end
it 'make the application errored' do
service.execute
expect(application).to be_update_errored
expect(application.status_reason).to eq('Operation failed. Check pod logs for install-helm for more details.')
end
end
context 'when timed out' do
let(:application) { create(:clusters_applications_helm, :timeouted, :updating) }
before do
expect(service).to receive(:installation_phase).once.and_return(phase)
end
it 'make the application errored' do
expect(ClusterWaitForAppInstallationWorker).not_to receive(:perform_in)
service.execute
expect(application).to be_update_errored
expect(application.status_reason).to eq('Operation timed out. Check pod logs for install-helm for more details.')
end
end
end
context 'when application is installing' do
include_examples 'error logging'
RESCHEDULE_PHASES.each { |phase| it_behaves_like 'a not yet terminated installation', phase }
context 'when installation POD succeeded' do
let(:phase) { Gitlab::Kubernetes::Pod::SUCCEEDED }
before do
expect(service).to receive(:installation_phase).once.and_return(phase)
end
@ -84,32 +165,24 @@ describe Clusters::Applications::CheckInstallationProgressService do
service.execute
expect(application).to be_errored
expect(application.status_reason).to eq("Installation failed. Check pod logs for install-helm for more details.")
expect(application.status_reason).to eq('Operation failed. Check pod logs for install-helm for more details.')
end
end
RESCHEDULE_PHASES.each { |phase| it_behaves_like 'a not yet terminated installation', phase }
context 'when installation raises a Kubeclient::HttpError' do
let(:cluster) { create(:cluster, :provided_by_user, :project) }
context 'when timed out' do
let(:application) { create(:clusters_applications_helm, :timeouted) }
before do
application.update!(cluster: cluster)
expect(service).to receive(:installation_phase).and_raise(Kubeclient::HttpError.new(401, 'Unauthorized', nil))
expect(service).to receive(:installation_phase).once.and_return(phase)
end
it 'shows the response code from the error' do
it 'make the application errored' do
expect(ClusterWaitForAppInstallationWorker).not_to receive(:perform_in)
service.execute
expect(application).to be_errored
expect(application.status_reason).to eq('Kubernetes error: 401')
end
it 'should log error' do
expect(service.send(:logger)).to receive(:error)
service.execute
expect(application.status_reason).to eq('Operation timed out. Check pod logs for install-helm for more details.')
end
end
end

View file

@ -13,6 +13,7 @@ describe Clusters::Applications::CreateService do
describe '#execute' do
before do
allow(ClusterInstallAppWorker).to receive(:perform_async)
allow(ClusterUpgradeAppWorker).to receive(:perform_async)
end
subject { service.execute(test_request) }
@ -31,6 +32,22 @@ describe Clusters::Applications::CreateService do
subject
end
context 'application already installed' do
let!(:application) { create(:clusters_applications_helm, :installed, cluster: cluster) }
it 'does not create a new application' do
expect do
subject
end.not_to change(Clusters::Applications::Helm, :count)
end
it 'schedules an upgrade for the application' do
expect(Clusters::Applications::ScheduleInstallationService).to receive(:new).with(application).and_call_original
subject
end
end
context 'cert manager application' do
let(:params) do
{

View file

@ -49,5 +49,29 @@ describe Clusters::Applications::ScheduleInstallationService do
it_behaves_like 'a failing service'
end
context 'when application is installed' do
let(:application) { create(:clusters_applications_helm, :installed) }
it 'schedules an upgrade via worker' do
expect(ClusterUpgradeAppWorker).to receive(:perform_async).with(application.name, application.id).once
service.execute
expect(application).to be_scheduled
end
end
context 'when application is updated' do
let(:application) { create(:clusters_applications_helm, :updated) }
it 'schedules an upgrade via worker' do
expect(ClusterUpgradeAppWorker).to receive(:perform_async).with(application.name, application.id).once
service.execute
expect(application).to be_scheduled
end
end
end
end

View file

@ -0,0 +1,128 @@
# frozen_string_literal: true
require 'spec_helper'
describe Clusters::Applications::UpgradeService do
describe '#execute' do
let(:application) { create(:clusters_applications_helm, :scheduled) }
let!(:install_command) { application.install_command }
let(:service) { described_class.new(application) }
let(:helm_client) { instance_double(Gitlab::Kubernetes::Helm::Api) }
before do
allow(service).to receive(:install_command).and_return(install_command)
allow(service).to receive(:helm_api).and_return(helm_client)
end
context 'when there are no errors' do
before do
expect(helm_client).to receive(:update).with(install_command)
allow(ClusterWaitForAppInstallationWorker).to receive(:perform_in).and_return(nil)
end
it 'make the application updating' do
expect(application.cluster).not_to be_nil
service.execute
expect(application).to be_updating
end
it 'schedule async installation status check' do
expect(ClusterWaitForAppInstallationWorker).to receive(:perform_in).once
service.execute
end
end
context 'when kubernetes cluster communication fails' do
let(:error) { Kubeclient::HttpError.new(500, 'system failure', nil) }
before do
expect(helm_client).to receive(:update).with(install_command).and_raise(error)
end
it 'make the application errored' do
service.execute
expect(application).to be_update_errored
expect(application.status_reason).to match('Kubernetes error: 500')
end
it 'logs errors' do
expect(service.send(:logger)).to receive(:error).with(
{
exception: 'Kubeclient::HttpError',
message: 'system failure',
service: 'Clusters::Applications::UpgradeService',
app_id: application.id,
project_ids: application.cluster.project_ids,
group_ids: [],
error_code: 500
}
)
expect(Gitlab::Sentry).to receive(:track_acceptable_exception).with(
error,
extra: {
exception: 'Kubeclient::HttpError',
message: 'system failure',
service: 'Clusters::Applications::UpgradeService',
app_id: application.id,
project_ids: application.cluster.project_ids,
group_ids: [],
error_code: 500
}
)
service.execute
end
end
context 'a non kubernetes error happens' do
let(:application) { create(:clusters_applications_helm, :scheduled) }
let(:error) { StandardError.new('something bad happened') }
before do
expect(application).to receive(:make_updating!).once.and_raise(error)
end
it 'make the application errored' do
expect(helm_client).not_to receive(:update)
service.execute
expect(application).to be_update_errored
expect(application.status_reason).to eq("Can't start upgrade process.")
end
it 'logs errors' do
expect(service.send(:logger)).to receive(:error).with(
{
exception: 'StandardError',
error_code: nil,
message: 'something bad happened',
service: 'Clusters::Applications::UpgradeService',
app_id: application.id,
project_ids: application.cluster.projects.pluck(:id),
group_ids: []
}
)
expect(Gitlab::Sentry).to receive(:track_acceptable_exception).with(
error,
extra: {
exception: 'StandardError',
error_code: nil,
message: 'something bad happened',
service: 'Clusters::Applications::UpgradeService',
app_id: application.id,
project_ids: application.cluster.projects.pluck(:id),
group_ids: []
}
)
service.execute
end
end
end
end

View file

@ -48,6 +48,36 @@ shared_examples 'cluster application status specs' do |application_name|
expect(subject.version).to eq(subject.class.const_get(:VERSION))
end
context 'application is updating' do
subject { create(application_name, :updating) }
it 'is updated' do
subject.make_installed!
expect(subject).to be_updated
end
it 'updates helm version' do
subject.cluster.application_helm.update!(version: '1.2.3')
subject.make_installed!
subject.cluster.application_helm.reload
expect(subject.cluster.application_helm.version).to eq(Gitlab::Kubernetes::Helm::HELM_VERSION)
end
it 'updates the version of the application' do
subject.update!(version: '0.0.0')
subject.make_installed!
subject.reload
expect(subject.version).to eq(subject.class.const_get(:VERSION))
end
end
end
describe '#make_updated' do
@ -90,6 +120,17 @@ shared_examples 'cluster application status specs' do |application_name|
expect(subject).to be_errored
expect(subject.status_reason).to eq(reason)
end
context 'application is updating' do
subject { create(application_name, :updating) }
it 'is update_errored' do
subject.make_errored(reason)
expect(subject).to be_update_errored
expect(subject.status_reason).to eq(reason)
end
end
end
describe '#make_scheduled' do
@ -112,6 +153,18 @@ shared_examples 'cluster application status specs' do |application_name|
expect(subject.status_reason).to be_nil
end
end
describe 'when was updated_errored' do
subject { create(application_name, :update_errored) }
it 'clears #status_reason' do
expect(subject.status_reason).not_to be_nil
subject.make_scheduled!
expect(subject.status_reason).to be_nil
end
end
end
end

View file

@ -0,0 +1,20 @@
# frozen_string_literal: true
shared_examples 'cluster application version specs' do |application_name|
describe 'update_available?' do
let(:version) { '0.0.0' }
subject { create(application_name, :installed, version: version).update_available? }
context 'version is not the same as VERSION' do
it { is_expected.to be_truthy }
end
context 'version is the same as VERSION' do
let(:application) { build(application_name) }
let(:version) { application.class.const_get(:VERSION) }
it { is_expected.to be_falsey }
end
end
end