Commit ddfa1f60 authored by Douglas Barbosa Alexandre's avatar Douglas Barbosa Alexandre

Merge branch 'use_upgrade_install_for_helm_apps' into 'master'

Upgrade cluster applications, starting with runner

See merge request gitlab-org/gitlab-ce!24789
parents e2966a6d f67fc237
...@@ -6,7 +6,13 @@ import Flash from '../flash'; ...@@ -6,7 +6,13 @@ import Flash from '../flash';
import Poll from '../lib/utils/poll'; import Poll from '../lib/utils/poll';
import initSettingsPanels from '../settings_panels'; import initSettingsPanels from '../settings_panels';
import eventHub from './event_hub'; 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 ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store'; import ClustersStore from './stores/clusters_store';
import Applications from './components/applications.vue'; import Applications from './components/applications.vue';
...@@ -120,11 +126,17 @@ export default class Clusters { ...@@ -120,11 +126,17 @@ export default class Clusters {
addListeners() { addListeners() {
if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken); if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken);
eventHub.$on('installApplication', this.installApplication); 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() { removeListeners() {
if (this.showTokenButton) this.showTokenButton.removeEventListener('click', this.showToken); if (this.showTokenButton) this.showTokenButton.removeEventListener('click', this.showToken);
eventHub.$off('installApplication', this.installApplication); eventHub.$off('installApplication', this.installApplication);
eventHub.$off('upgradeApplication', this.upgradeApplication);
eventHub.$off('upgradeFailed', this.upgradeFailed);
eventHub.$off('dismissUpgradeSuccess', this.dismissUpgradeSuccess);
} }
initPolling() { initPolling() {
...@@ -245,6 +257,21 @@ export default class Clusters { ...@@ -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() { destroy() {
this.destroyed = true; this.destroyed = true;
......
<script> <script>
/* eslint-disable vue/require-default-prop */ /* 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 { s__, sprintf } from '../../locale';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import identicon from '../../vue_shared/components/identicon.vue'; import identicon from '../../vue_shared/components/identicon.vue';
import loadingButton from '../../vue_shared/components/loading_button.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 { export default {
components: { components: {
loadingButton, loadingButton,
identicon, identicon,
TimeagoTooltip,
GlLink,
}, },
props: { props: {
id: { id: {
...@@ -54,6 +63,18 @@ export default { ...@@ -54,6 +63,18 @@ export default {
type: String, type: String,
required: false, required: false,
}, },
version: {
type: String,
required: false,
},
chartRepo: {
type: String,
required: false,
},
upgradeAvailable: {
type: Boolean,
required: false,
},
installApplicationRequestParams: { installApplicationRequestParams: {
type: Object, type: Object,
required: false, required: false,
...@@ -78,7 +99,8 @@ export default { ...@@ -78,7 +99,8 @@ export default {
return ( return (
this.status === APPLICATION_STATUS.INSTALLED || this.status === APPLICATION_STATUS.INSTALLED ||
this.status === APPLICATION_STATUS.UPDATED || this.status === APPLICATION_STATUS.UPDATED ||
this.status === APPLICATION_STATUS.UPDATING this.status === APPLICATION_STATUS.UPDATING ||
this.status === APPLICATION_STATUS.UPDATE_ERRORED
); );
}, },
canInstall() { canInstall() {
...@@ -146,6 +168,69 @@ export default { ...@@ -146,6 +168,69 @@ export default {
title: this.title, 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: { methods: {
installClicked() { installClicked() {
...@@ -154,6 +239,15 @@ export default { ...@@ -154,6 +239,15 @@ export default {
params: this.installApplicationRequestParams, params: this.installApplicationRequestParams,
}); });
}, },
upgradeClicked() {
eventHub.$emit('upgradeApplication', {
id: this.id,
params: this.installApplicationRequestParams,
});
},
dismissUpgradeSuccess() {
eventHub.$emit('dismissUpgradeSuccess', this.id);
},
}, },
}; };
</script> </script>
...@@ -207,6 +301,51 @@ export default { ...@@ -207,6 +301,51 @@ export default {
</li> </li>
</ul> </ul>
</div> </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>
<div <div
:class="{ 'section-25': showManageButton, 'section-15': !showManageButton }" :class="{ 'section-25': showManageButton, 'section-15': !showManageButton }"
......
...@@ -362,6 +362,9 @@ export default { ...@@ -362,6 +362,9 @@ export default {
:status-reason="applications.runner.statusReason" :status-reason="applications.runner.statusReason"
:request-status="applications.runner.requestStatus" :request-status="applications.runner.requestStatus"
:request-reason="applications.runner.requestReason" :request-reason="applications.runner.requestReason"
:version="applications.runner.version"
:chart-repo="applications.runner.chartRepo"
:upgrade-available="applications.runner.upgradeAvailable"
:disabled="!helmInstalled" :disabled="!helmInstalled"
title-link="https://docs.gitlab.com/runner/" title-link="https://docs.gitlab.com/runner/"
> >
......
...@@ -12,15 +12,19 @@ export const APPLICATION_STATUS = { ...@@ -12,15 +12,19 @@ export const APPLICATION_STATUS = {
SCHEDULED: 'scheduled', SCHEDULED: 'scheduled',
INSTALLING: 'installing', INSTALLING: 'installing',
INSTALLED: 'installed', INSTALLED: 'installed',
UPDATED: 'updated',
UPDATING: 'updating', UPDATING: 'updating',
UPDATED: 'updated',
UPDATE_ERRORED: 'update_errored',
ERROR: 'errored', ERROR: 'errored',
}; };
// These are only used client-side // These are only used client-side
export const REQUEST_SUBMITTED = 'request-submitted'; export const REQUEST_SUBMITTED = 'request-submitted';
export const REQUEST_FAILURE = 'request-failure'; 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 INGRESS = 'ingress';
export const JUPYTER = 'jupyter'; export const JUPYTER = 'jupyter';
export const KNATIVE = 'knative'; export const KNATIVE = 'knative';
export const RUNNER = 'runner';
export const CERT_MANAGER = 'cert_manager'; export const CERT_MANAGER = 'cert_manager';
import { s__ } from '../../locale'; import { s__ } from '../../locale';
import { parseBoolean } from '../../lib/utils/common_utils'; 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 { export default class ClusterStore {
constructor() { constructor() {
...@@ -40,6 +40,9 @@ export default class ClusterStore { ...@@ -40,6 +40,9 @@ export default class ClusterStore {
statusReason: null, statusReason: null,
requestStatus: null, requestStatus: null,
requestReason: null, requestReason: null,
version: null,
chartRepo: 'https://gitlab.com/charts/gitlab-runner',
upgradeAvailable: null,
}, },
prometheus: { prometheus: {
title: s__('ClusterIntegration|Prometheus'), title: s__('ClusterIntegration|Prometheus'),
...@@ -100,7 +103,13 @@ export default class ClusterStore { ...@@ -100,7 +103,13 @@ export default class ClusterStore {
this.state.statusReason = serverState.status_reason; this.state.statusReason = serverState.status_reason;
serverState.applications.forEach(serverAppEntry => { 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] = {
...(this.state.applications[appId] || {}), ...(this.state.applications[appId] || {}),
...@@ -124,6 +133,9 @@ export default class ClusterStore { ...@@ -124,6 +133,9 @@ export default class ClusterStore {
serverAppEntry.hostname || this.state.applications.knative.hostname; serverAppEntry.hostname || this.state.applications.knative.hostname;
this.state.applications.knative.externalIp = this.state.applications.knative.externalIp =
serverAppEntry.external_ip || 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;
} }
}); });
} }
......
...@@ -58,6 +58,20 @@ ...@@ -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 { .cluster-application-description {
flex: 1; flex: 1;
} }
......
...@@ -53,11 +53,11 @@ module Clusters ...@@ -53,11 +53,11 @@ module Clusters
end end
def upgrade_command(values) def upgrade_command(values)
::Gitlab::Kubernetes::Helm::UpgradeCommand.new( ::Gitlab::Kubernetes::Helm::InstallCommand.new(
name, name: name,
version: VERSION, version: VERSION,
chart: chart,
rbac: cluster.platform_kubernetes_rbac?, rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
files: files_with_replaced_values(values) files: files_with_replaced_values(values)
) )
end end
......
...@@ -20,7 +20,7 @@ module Clusters ...@@ -20,7 +20,7 @@ module Clusters
state :update_errored, value: 6 state :update_errored, value: 6
event :make_scheduled do event :make_scheduled do
transition [:installable, :errored] => :scheduled transition [:installable, :errored, :installed, :updated, :update_errored] => :scheduled
end end
event :make_installing do event :make_installing do
...@@ -29,16 +29,19 @@ module Clusters ...@@ -29,16 +29,19 @@ module Clusters
event :make_installed do event :make_installed do
transition [:installing] => :installed transition [:installing] => :installed
transition [:updating] => :updated
end end
event :make_errored do event :make_errored do
transition any => :errored transition any - [:updating] => :errored
transition [:updating] => :update_errored
end end
event :make_updating do event :make_updating do
transition [:installed, :updated, :update_errored] => :updating transition [:installed, :updated, :update_errored, :scheduled] => :updating
end end
# Deprecated
event :make_updated do event :make_updated do
transition [:updating] => :updated transition [:updating] => :updated
end end
...@@ -74,6 +77,10 @@ module Clusters ...@@ -74,6 +77,10 @@ module Clusters
end end
end end
def updateable?
installed? || updated? || update_errored?
end
def available? def available?
installed? || updated? installed? || updated?
end end
......
...@@ -12,6 +12,10 @@ module Clusters ...@@ -12,6 +12,10 @@ module Clusters
end end
end end
end end
def update_available?
version != self.class.const_get(:VERSION)
end
end end
end end
end end
...@@ -8,4 +8,5 @@ class ClusterApplicationEntity < Grape::Entity ...@@ -8,4 +8,5 @@ class ClusterApplicationEntity < Grape::Entity
expose :external_ip, if: -> (e, _) { e.respond_to?(:external_ip) } expose :external_ip, if: -> (e, _) { e.respond_to?(:external_ip) }
expose :hostname, if: -> (e, _) { e.respond_to?(:hostname) } expose :hostname, if: -> (e, _) { e.respond_to?(:hostname) }
expose :email, if: -> (e, _) { e.respond_to?(:email) } expose :email, if: -> (e, _) { e.respond_to?(:email) }
expose :update_available?, as: :update_available, if: -> (e, _) { e.respond_to?(:update_available?) }
end end
...@@ -4,7 +4,7 @@ module Clusters ...@@ -4,7 +4,7 @@ module Clusters
module Applications module Applications
class CheckInstallationProgressService < BaseHelmService class CheckInstallationProgressService < BaseHelmService
def execute def execute
return unless app.installing? return unless operation_in_progress?
case installation_phase case installation_phase
when Gitlab::Kubernetes::Pod::SUCCEEDED when Gitlab::Kubernetes::Pod::SUCCEEDED
...@@ -16,11 +16,16 @@ module Clusters ...@@ -16,11 +16,16 @@ module Clusters
end end
rescue Kubeclient::HttpError => e rescue Kubeclient::HttpError => e
log_error(e) log_error(e)
app.make_errored!("Kubernetes error: #{e.error_code}") unless app.errored?
app.make_errored!("Kubernetes error: #{e.error_code}")
end end
private private
def operation_in_progress?
app.installing? || app.updating?
end
def on_success def on_success
app.make_installed! app.make_installed!
ensure ensure
...@@ -28,13 +33,13 @@ module Clusters ...@@ -28,13 +33,13 @@ module Clusters
end end
def on_failed 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 end
def check_timeout def check_timeout
if timeouted? if timeouted?
begin 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 end
else else
ClusterWaitForAppInstallationWorker.perform_in( ClusterWaitForAppInstallationWorker.perform_in(
...@@ -42,20 +47,24 @@ module Clusters ...@@ -42,20 +47,24 @@ module Clusters
end end
end end
def pod_name
install_command.pod_name
end
def timeouted? def timeouted?
Time.now.utc - app.updated_at.to_time.utc > ClusterWaitForAppInstallationWorker::TIMEOUT Time.now.utc - app.updated_at.to_time.utc > ClusterWaitForAppInstallationWorker::TIMEOUT
end end
def remove_installation_pod def remove_installation_pod
helm_api.delete_pod!(install_command.pod_name) helm_api.delete_pod!(pod_name)
end end
def installation_phase def installation_phase
helm_api.status(install_command.pod_name) helm_api.status(pod_name)
end end
def installation_errors def installation_errors
helm_api.log(install_command.pod_name) helm_api.log(pod_name)
end end
end end
end end
......
...@@ -10,6 +10,18 @@ module Clusters ...@@ -10,6 +10,18 @@ module Clusters
end end
def execute 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! application.make_scheduled!
ClusterInstallAppWorker.perform_async(application.name, application.id) ClusterInstallAppWorker.perform_async(application.name, application.id)
......
# 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
...@@ -23,6 +23,7 @@ ...@@ -23,6 +23,7 @@
- cronjob:prune_web_hook_logs - cronjob:prune_web_hook_logs
- gcp_cluster:cluster_install_app - gcp_cluster:cluster_install_app
- gcp_cluster:cluster_upgrade_app
- gcp_cluster:cluster_provision - gcp_cluster:cluster_provision
- gcp_cluster:cluster_wait_for_app_installation - gcp_cluster:cluster_wait_for_app_installation
- gcp_cluster:wait_for_cluster_creation - gcp_cluster:wait_for_cluster_creation
......
# 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
---
title: Added ability to upgrade cluster applications
merge_request: 24789
author:
type: added
...@@ -20,14 +20,7 @@ module Gitlab ...@@ -20,14 +20,7 @@ module Gitlab
kubeclient.create_pod(command.pod_resource) kubeclient.create_pod(command.pod_resource)
end end
def update(command) alias_method :update, :install
namespace.ensure_exists!
update_config_map(command)
delete_pod!(command.pod_name)
kubeclient.create_pod(command.pod_resource)
end
## ##
# Returns Pod phase # Returns Pod phase
...@@ -62,6 +55,8 @@ module Gitlab ...@@ -62,6 +55,8 @@ module Gitlab
def create_config_map(command) def create_config_map(command)
command.config_map_resource.tap do |config_map_resource| command.config_map_resource.tap do |config_map_resource|
break unless config_map_resource
if config_map_exists?(config_map_resource) if config_map_exists?(config_map_resource)
kubeclient.update_config_map(config_map_resource) kubeclient.update_config_map(config_map_resource)
else else
......
...@@ -42,8 +42,17 @@ module Gitlab ...@@ -42,8 +42,17 @@ module Gitlab
'helm repo update' if repository 'helm repo update' if repository
end end
# Uses `helm upgrade --install` which means we can use this for both
# installation and uprade of applications
def install_command 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 command.shelljoin
end end
...@@ -56,17 +65,20 @@ module Gitlab ...@@ -56,17 +65,20 @@ module Gitlab
postinstall.join("\n") if postinstall postinstall.join("\n") if postinstall
end end
def install_command_flags def install_flag
name_flag = ['--name', name] ['--install']
namespace_flag = ['--namespace', Gitlab::Kubernetes::Helm::NAMESPACE] end
value_flag = ['-f', "/data/helm/#{name}/config/values.yaml"]
name_flag + def reset_values_flag
optional_tls_flags + ['--reset-values']
optional_version_flag + end
rbac_create_flag +
namespace_flag + def value_flag
value_flag ['-f', "/data/helm/#{name}/config/values.yaml"]
end
def namespace_flag
['--namespace', Gitlab::Kubernetes::Helm::NAMESPACE]
end end
def rbac_create_flag def rbac_create_flag
......
# 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
...@@ -1554,6 +1554,9 @@ msgstr "" ...@@ -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}." 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 "" msgstr ""
msgid "ClusterIntegration|%{title} upgraded successfully."
msgstr ""
msgid "ClusterIntegration|API URL" msgid "ClusterIntegration|API URL"
msgstr "" msgstr ""
...@@ -1878,6 +1881,9 @@ msgstr "" ...@@ -1878,6 +1881,9 @@ msgstr ""
msgid "ClusterIntegration|Request to begin installing failed" msgid "ClusterIntegration|Request to begin installing failed"
msgstr "" msgstr ""
msgid "ClusterIntegration|Retry upgrade"
msgstr ""
msgid "ClusterIntegration|Save changes" msgid "ClusterIntegration|Save changes"
msgstr "" msgstr ""
...@@ -1920,6 +1926,9 @@ msgstr "" ...@@ -1920,6 +1926,9 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong on our end." msgid "ClusterIntegration|Something went wrong on our end."
msgstr "" 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" msgid "ClusterIntegration|Something went wrong while creating your Kubernetes cluster on Google Kubernetes Engine"
msgstr "" msgstr ""
...@@ -1944,6 +1953,18 @@ msgstr "" ...@@ -1944,6 +1953,18 @@ msgstr ""
msgid "ClusterIntegration|Token" msgid "ClusterIntegration|Token"
msgstr "" msgstr ""
msgid "ClusterIntegration|Upgrade"
msgstr ""
msgid "ClusterIntegration|Upgrade failed"
msgstr ""
msgid "ClusterIntegration|Upgraded"
msgstr ""
msgid "ClusterIntegration|Upgrading"
msgstr ""
msgid "ClusterIntegration|Validating project billing status" msgid "ClusterIntegration|Validating project billing status"
msgstr "" msgstr ""
......
...@@ -34,7 +34,8 @@ ...@@ -34,7 +34,8 @@
"status_reason": { "type": ["string", "null"] }, "status_reason": { "type": ["string", "null"] },
"external_ip": { "type": ["string", "null"] }, "external_ip": { "type": ["string", "null"] },
"hostname": { "type": ["string", "null"] }, "hostname": { "type": ["string", "null"] },
"email": { "type": ["string", "null"] } "email": { "type": ["string", "null"] },
"update_available": { "type": ["boolean", "null"] }
}, },
"required" : [ "name", "status" ] "required" : [ "name", "status" ]
} }
......
...@@ -208,6 +208,144 @@ describe('Application Row', () => { ...@@ -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', () => { describe('Error block', () => {
it('does not show error block when there is no error', () => { it('does not show error block when there is no error', () => {
vm = mountComponent(ApplicationRow, { vm = mountComponent(ApplicationRow, {
......
...@@ -85,6 +85,9 @@ describe('Clusters Store', () => { ...@@ -85,6 +85,9 @@ describe('Clusters Store', () => {
statusReason: mockResponseData.applications[2].status_reason, statusReason: mockResponseData.applications[2].status_reason,
requestStatus: null, requestStatus: null,
requestReason: null, requestReason: null,
version: mockResponseData.applications[2].version,
upgradeAvailable: mockResponseData.applications[2].update_available,
chartRepo: 'https://gitlab.com/charts/gitlab-runner',
}, },
prometheus: { prometheus: {
title: 'Prometheus', title: 'Prometheus',
......
...@@ -171,51 +171,6 @@ describe Gitlab::Kubernetes::Helm::Api do ...@@ -171,51 +171,6 @@ describe Gitlab::Kubernetes::Helm::Api do
end end
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 describe '#status' do
let(:phase) { Gitlab::Kubernetes::Pod::RUNNING } let(:phase) { Gitlab::Kubernetes::Pod::RUNNING }
let(:pod) { Kubeclient::Resource.new(status: { phase: phase }) } # partial representation let(:pod) { Kubeclient::Resource.new(status: { phase: phase }) } # partial representation
......
...@@ -21,6 +21,15 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do ...@@ -21,6 +21,15 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
) )
end 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 } subject { install_command }
it_behaves_like 'helm commands' do it_behaves_like 'helm commands' do
...@@ -36,12 +45,10 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do ...@@ -36,12 +45,10 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
let(:helm_install_comand) do let(:helm_install_comand) do
<<~EOS.squish <<~EOS.squish
helm install chart-name helm upgrade app-name chart-name
--name app-name --install
--tls --reset-values
--tls-ca-cert /data/helm/app-name/config/ca.pem #{tls_flags}
--tls-cert /data/helm/app-name/config/cert.pem
--tls-key /data/helm/app-name/config/key.pem
--version 1.2.3 --version 1.2.3
--set rbac.create\\=false,rbac.enabled\\=false --set rbac.create\\=false,rbac.enabled\\=false
--namespace gitlab-managed-apps --namespace gitlab-managed-apps
...@@ -66,12 +73,10 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do ...@@ -66,12 +73,10 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
let(:helm_install_command) do let(:helm_install_command) do
<<~EOS.squish <<~EOS.squish
helm install chart-name helm upgrade app-name chart-name
--name app-name --install
--tls --reset-values
--tls-ca-cert /data/helm/app-name/config/ca.pem #{tls_flags}
--tls-cert /data/helm/app-name/config/cert.pem
--tls-key /data/helm/app-name/config/key.pem
--version 1.2.3 --version 1.2.3
--set rbac.create\\=true,rbac.enabled\\=true --set rbac.create\\=true,rbac.enabled\\=true
--namespace gitlab-managed-apps --namespace gitlab-managed-apps
...@@ -95,12 +100,10 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do ...@@ -95,12 +100,10 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
let(:helm_install_command) do let(:helm_install_command) do
<<~EOS.squish <<~EOS.squish
helm install chart-name helm upgrade app-name chart-name
--name app-name --install
--tls --reset-values
--tls-ca-cert /data/helm/app-name/config/ca.pem #{tls_flags}
--tls-cert /data/helm/app-name/config/cert.pem
--tls-key /data/helm/app-name/config/key.pem
--version 1.2.3 --version 1.2.3
--set rbac.create\\=false,rbac.enabled\\=false --set rbac.create\\=false,rbac.enabled\\=false
--namespace gitlab-managed-apps --namespace gitlab-managed-apps
...@@ -120,15 +123,22 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do ...@@ -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 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 add app-name https://repository.example.com
helm repo update helm repo update
/bin/date
/bin/true
#{helm_install_command} #{helm_install_command}
EOS EOS
end end
let(:helm_install_command) do let(:helm_install_command) do
<<~EOS.strip <<~EOS.squish
/bin/date helm upgrade app-name chart-name
/bin/true --install
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 --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 EOS
end end
end end
...@@ -145,14 +155,21 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do ...@@ -145,14 +155,21 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
helm repo add app-name https://repository.example.com helm repo add app-name https://repository.example.com
helm repo update helm repo update
#{helm_install_command} #{helm_install_command}
/bin/date
/bin/false
EOS EOS
end end
let(:helm_install_command) do let(:helm_install_command) do
<<~EOS.strip <<~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 --version 1.2.3 --set rbac.create\\=false,rbac.enabled\\=false --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml helm upgrade app-name chart-name
/bin/date --install
/bin/false --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 EOS
end end
end end
...@@ -174,8 +191,9 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do ...@@ -174,8 +191,9 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
let(:helm_install_command) do let(:helm_install_command) do
<<~EOS.squish <<~EOS.squish
helm install chart-name helm upgrade app-name chart-name
--name app-name --install
--reset-values
--version 1.2.3 --version 1.2.3
--set rbac.create\\=false,rbac.enabled\\=false --set rbac.create\\=false,rbac.enabled\\=false
--namespace gitlab-managed-apps --namespace gitlab-managed-apps
...@@ -201,12 +219,10 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do ...@@ -201,12 +219,10 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
let(:helm_install_command) do let(:helm_install_command) do
<<~EOS.squish <<~EOS.squish
helm install chart-name helm upgrade app-name chart-name
--name app-name --install
--tls --reset-values
--tls-ca-cert /data/helm/app-name/config/ca.pem #{tls_flags}
--tls-cert /data/helm/app-name/config/cert.pem
--tls-key /data/helm/app-name/config/key.pem
--set rbac.create\\=false,rbac.enabled\\=false --set rbac.create\\=false,rbac.enabled\\=false
--namespace gitlab-managed-apps --namespace gitlab-managed-apps
-f /data/helm/app-name/config/values.yaml -f /data/helm/app-name/config/values.yaml
......
# 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
...@@ -5,6 +5,7 @@ describe Clusters::Applications::CertManager do ...@@ -5,6 +5,7 @@ describe Clusters::Applications::CertManager do
include_examples 'cluster application core specs', :clusters_applications_cert_managers include_examples 'cluster application core specs', :clusters_applications_cert_managers
include_examples 'cluster application status 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' include_examples 'cluster application initial status specs'
describe '#install_command' do describe '#install_command' do
......
...@@ -7,6 +7,7 @@ describe Clusters::Applications::Ingress do ...@@ -7,6 +7,7 @@ describe Clusters::Applications::Ingress do
include_examples 'cluster application core specs', :clusters_applications_ingress include_examples 'cluster application core specs', :clusters_applications_ingress
include_examples 'cluster application status 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 helm specs', :clusters_applications_ingress
include_examples 'cluster application initial status specs' include_examples 'cluster application initial status specs'
......
...@@ -3,6 +3,7 @@ require 'rails_helper' ...@@ -3,6 +3,7 @@ require 'rails_helper'
describe Clusters::Applications::Jupyter do describe Clusters::Applications::Jupyter do
include_examples 'cluster application core specs', :clusters_applications_jupyter include_examples 'cluster application core specs', :clusters_applications_jupyter
include_examples 'cluster application status 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 include_examples 'cluster application helm specs', :clusters_applications_jupyter
it { is_expected.to belong_to(:oauth_application) } it { is_expected.to belong_to(:oauth_application) }
......
...@@ -9,6 +9,7 @@ describe Clusters::Applications::Knative do ...@@ -9,6 +9,7 @@ describe Clusters::Applications::Knative do
include_examples 'cluster application core specs', :clusters_applications_knative include_examples 'cluster application core specs', :clusters_applications_knative
include_examples 'cluster application status 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 helm specs', :clusters_applications_knative
include_examples 'cluster application version specs', :clusters_applications_knative
include_examples 'cluster application initial status specs' include_examples 'cluster application initial status specs'
before do before do
......
...@@ -5,6 +5,7 @@ describe Clusters::Applications::Prometheus do ...@@ -5,6 +5,7 @@ describe Clusters::Applications::Prometheus do
include_examples 'cluster application core specs', :clusters_applications_prometheus include_examples 'cluster application core specs', :clusters_applications_prometheus
include_examples 'cluster application status 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 helm specs', :clusters_applications_prometheus
include_examples 'cluster application initial status specs' include_examples 'cluster application initial status specs'
...@@ -206,8 +207,8 @@ describe Clusters::Applications::Prometheus do ...@@ -206,8 +207,8 @@ describe Clusters::Applications::Prometheus do
let(:prometheus) { build(:clusters_applications_prometheus) } let(:prometheus) { build(:clusters_applications_prometheus) }
let(:values) { prometheus.values } let(:values) { prometheus.values }
it 'returns an instance of Gitlab::Kubernetes::Helm::GetCommand' do it 'returns an instance of Gitlab::Kubernetes::Helm::InstallCommand' do
expect(prometheus.upgrade_command(values)).to be_an_instance_of(::Gitlab::Kubernetes::Helm::UpgradeCommand) expect(prometheus.upgrade_command(values)).to be_an_instance_of(::Gitlab::Kubernetes::Helm::InstallCommand)
end end
it 'should be initialized with 3 arguments' do it 'should be initialized with 3 arguments' do
......
...@@ -5,6 +5,7 @@ describe Clusters::Applications::Runner do ...@@ -5,6 +5,7 @@ describe Clusters::Applications::Runner do
include_examples 'cluster application core specs', :clusters_applications_runner include_examples 'cluster application core specs', :clusters_applications_runner
include_examples 'cluster application status 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 helm specs', :clusters_applications_runner
include_examples 'cluster application initial status specs' include_examples 'cluster application initial status specs'
......
...@@ -21,6 +21,14 @@ describe ClusterApplicationEntity do ...@@ -21,6 +21,14 @@ describe ClusterApplicationEntity do
expect(subject[:status_reason]).to be_nil expect(subject[:status_reason]).to be_nil
end 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 context 'when application is errored' do
let(:application) { build(:clusters_applications_helm, :errored) } let(:application) { build(:clusters_applications_helm, :errored) }
......
require 'spec_helper' 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 RESCHEDULE_PHASES = Gitlab::Kubernetes::Pod::PHASES - [Gitlab::Kubernetes::Pod::SUCCEEDED, Gitlab::Kubernetes::Pod::FAILED].freeze
let(:application) { create(:clusters_applications_helm, :installing) } let(:application) { create(:clusters_applications_helm, :installing) }
...@@ -21,24 +21,39 @@ describe Clusters::Applications::CheckInstallationProgressService do ...@@ -21,24 +21,39 @@ describe Clusters::Applications::CheckInstallationProgressService do
expect(ClusterWaitForAppInstallationWorker).to receive(:perform_in).once expect(ClusterWaitForAppInstallationWorker).to receive(:perform_in).once
expect(service).not_to receive(:remove_installation_pod) 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 expect(application.status_reason).to be_nil
end end
end end
end
end
context 'when timeouted' do shared_examples 'error logging' do
let(:application) { create(:clusters_applications_helm, :timeouted) } context 'when installation raises a Kubeclient::HttpError' do
let(:cluster) { create(:cluster, :provided_by_user, :project) }
it 'make the application errored' do before do
expect(ClusterWaitForAppInstallationWorker).not_to receive(:perform_in) 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 it 'shows the response code from the error' do
expect(application.status_reason).to eq("Installation timed out. Check pod logs for install-helm for more details.") service.execute
end
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 end
end end
...@@ -48,10 +63,15 @@ describe Clusters::Applications::CheckInstallationProgressService do ...@@ -48,10 +63,15 @@ describe Clusters::Applications::CheckInstallationProgressService do
allow(service).to receive(:remove_installation_pod).and_return(nil) allow(service).to receive(:remove_installation_pod).and_return(nil)
end 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 context 'when installation POD succeeded' do
let(:phase) { Gitlab::Kubernetes::Pod::SUCCEEDED } let(:phase) { Gitlab::Kubernetes::Pod::SUCCEEDED }
before do before do
expect(service).to receive(:installation_phase).once.and_return(phase) expect(service).to receive(:installation_phase).once.and_return(phase)
end end
...@@ -67,7 +87,7 @@ describe Clusters::Applications::CheckInstallationProgressService do ...@@ -67,7 +87,7 @@ describe Clusters::Applications::CheckInstallationProgressService do
service.execute service.execute
expect(application).to be_installed expect(application).to be_updated
expect(application.status_reason).to be_nil expect(application.status_reason).to be_nil
end end
end end
...@@ -83,33 +103,86 @@ describe Clusters::Applications::CheckInstallationProgressService do ...@@ -83,33 +103,86 @@ describe Clusters::Applications::CheckInstallationProgressService do
it 'make the application errored' do it 'make the application errored' do
service.execute service.execute
expect(application).to be_errored expect(application).to be_update_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
end end
RESCHEDULE_PHASES.each { |phase| it_behaves_like 'a not yet terminated installation', phase } context 'when timed out' do
let(:application) { create(:clusters_applications_helm, :timeouted, :updating) }
context 'when installation raises a Kubeclient::HttpError' do before do
let(:cluster) { create(:cluster, :provided_by_user, :project) } 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 before do
application.update!(cluster: cluster) expect(service).to receive(:installation_phase).once.and_return(phase)
end
expect(service).to receive(:installation_phase).and_raise(Kubeclient::HttpError.new(401, 'Unauthorized', nil)) it 'removes the installation POD' do
expect(service).to receive(:remove_installation_pod).once
service.execute
end end
it 'shows the response code from the error' do 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
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 service.execute
expect(application).to be_errored expect(application).to be_errored
expect(application.status_reason).to eq('Kubernetes error: 401') 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) }
before do
expect(service).to receive(:installation_phase).once.and_return(phase)
end end
it 'should log error' do it 'make the application errored' do
expect(service.send(:logger)).to receive(:error) expect(ClusterWaitForAppInstallationWorker).not_to receive(:perform_in)
service.execute service.execute
expect(application).to be_errored
expect(application.status_reason).to eq('Operation timed out. Check pod logs for install-helm for more details.')
end end
end end
end end
......
...@@ -13,6 +13,7 @@ describe Clusters::Applications::CreateService do ...@@ -13,6 +13,7 @@ describe Clusters::Applications::CreateService do
describe '#execute' do describe '#execute' do
before do before do
allow(ClusterInstallAppWorker).to receive(:perform_async) allow(ClusterInstallAppWorker).to receive(:perform_async)
allow(ClusterUpgradeAppWorker).to receive(:perform_async)
end end
subject { service.execute(test_request) } subject { service.execute(test_request) }
...@@ -31,6 +32,22 @@ describe Clusters::Applications::CreateService do ...@@ -31,6 +32,22 @@ describe Clusters::Applications::CreateService do
subject subject
end 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 context 'cert manager application' do
let(:params) do let(:params) do
{ {
......
...@@ -49,5 +49,29 @@ describe Clusters::Applications::ScheduleInstallationService do ...@@ -49,5 +49,29 @@ describe Clusters::Applications::ScheduleInstallationService do
it_behaves_like 'a failing service' it_behaves_like 'a failing service'
end 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
end end
# 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
...@@ -48,6 +48,36 @@ shared_examples 'cluster application status specs' do |application_name| ...@@ -48,6 +48,36 @@ shared_examples 'cluster application status specs' do |application_name|
expect(subject.version).to eq(subject.class.const_get(:VERSION)) expect(subject.version).to eq(subject.class.const_get(:VERSION))
end 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 end
describe '#make_updated' do describe '#make_updated' do
...@@ -90,6 +120,17 @@ shared_examples 'cluster application status specs' do |application_name| ...@@ -90,6 +120,17 @@ shared_examples 'cluster application status specs' do |application_name|
expect(subject).to be_errored expect(subject).to be_errored
expect(subject.status_reason).to eq(reason) expect(subject.status_reason).to eq(reason)
end 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 end
describe '#make_scheduled' do describe '#make_scheduled' do
...@@ -112,6 +153,18 @@ shared_examples 'cluster application status specs' do |application_name| ...@@ -112,6 +153,18 @@ shared_examples 'cluster application status specs' do |application_name|
expect(subject.status_reason).to be_nil expect(subject.status_reason).to be_nil
end end
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
end end
......
# 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
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