Upgrade cluster applications, starting with runner
This commit is contained in:
parent
e2966a6d8c
commit
f67fc23727
39 changed files with 897 additions and 353 deletions
|
@ -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;
|
||||
|
||||
|
|
|
@ -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">
|
||||
×
|
||||
</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 }"
|
||||
|
|
|
@ -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/"
|
||||
>
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -12,6 +12,10 @@ module Clusters
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
def update_available?
|
||||
version != self.class.const_get(:VERSION)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
28
app/services/clusters/applications/upgrade_service.rb
Normal file
28
app/services/clusters/applications/upgrade_service.rb
Normal 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
|
|
@ -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
|
||||
|
|
13
app/workers/cluster_upgrade_app_worker.rb
Normal file
13
app/workers/cluster_upgrade_app_worker.rb
Normal 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
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Added ability to upgrade cluster applications
|
||||
merge_request: 24789
|
||||
author:
|
||||
type: added
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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" ]
|
||||
}
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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) }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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
|
||||
|
|
128
spec/services/clusters/applications/upgrade_service_spec.rb
Normal file
128
spec/services/clusters/applications/upgrade_service_spec.rb
Normal 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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
Loading…
Reference in a new issue