Commit ccb080d9 authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch '42643-persist-external-ip-of-ingress-controller-gke' into 'master'

Display ingress IP address in the Kubernetes page

See merge request gitlab-org/gitlab-ce!17052
parents 52e133d1 ebac9c81
...@@ -37,10 +37,11 @@ export default class Clusters { ...@@ -37,10 +37,11 @@ export default class Clusters {
clusterStatusReason, clusterStatusReason,
helpPath, helpPath,
ingressHelpPath, ingressHelpPath,
ingressDnsHelpPath,
} = document.querySelector('.js-edit-cluster-form').dataset; } = document.querySelector('.js-edit-cluster-form').dataset;
this.store = new ClustersStore(); this.store = new ClustersStore();
this.store.setHelpPaths(helpPath, ingressHelpPath); this.store.setHelpPaths(helpPath, ingressHelpPath, ingressDnsHelpPath);
this.store.setManagePrometheusPath(managePrometheusPath); this.store.setManagePrometheusPath(managePrometheusPath);
this.store.updateStatus(clusterStatus); this.store.updateStatus(clusterStatus);
this.store.updateStatusReason(clusterStatusReason); this.store.updateStatusReason(clusterStatusReason);
...@@ -98,6 +99,7 @@ export default class Clusters { ...@@ -98,6 +99,7 @@ export default class Clusters {
helpPath: this.state.helpPath, helpPath: this.state.helpPath,
ingressHelpPath: this.state.ingressHelpPath, ingressHelpPath: this.state.ingressHelpPath,
managePrometheusPath: this.state.managePrometheusPath, managePrometheusPath: this.state.managePrometheusPath,
ingressDnsHelpPath: this.state.ingressDnsHelpPath,
}, },
}); });
}, },
......
...@@ -36,10 +36,6 @@ ...@@ -36,10 +36,6 @@
type: String, type: String,
required: false, required: false,
}, },
description: {
type: String,
required: true,
},
status: { status: {
type: String, type: String,
required: false, required: false,
...@@ -148,7 +144,7 @@ ...@@ -148,7 +144,7 @@
class="table-section section-wrap" class="table-section section-wrap"
role="gridcell" role="gridcell"
> >
<div v-html="description"></div> <slot name="description"></slot>
</div> </div>
<div <div
class="table-section table-button-footer section-align-top" class="table-section table-button-footer section-align-top"
......
...@@ -2,10 +2,16 @@ ...@@ -2,10 +2,16 @@
import _ from 'underscore'; import _ from 'underscore';
import { s__, sprintf } from '../../locale'; import { s__, sprintf } from '../../locale';
import applicationRow from './application_row.vue'; import applicationRow from './application_row.vue';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
import {
APPLICATION_INSTALLED,
INGRESS,
} from '../constants';
export default { export default {
components: { components: {
applicationRow, applicationRow,
clipboardButton,
}, },
props: { props: {
applications: { applications: {
...@@ -23,6 +29,11 @@ ...@@ -23,6 +29,11 @@
required: false, required: false,
default: '', default: '',
}, },
ingressDnsHelpPath: {
type: String,
required: false,
default: '',
},
managePrometheusPath: { managePrometheusPath: {
type: String, type: String,
required: false, required: false,
...@@ -43,19 +54,16 @@ ...@@ -43,19 +54,16 @@
false, false,
); );
}, },
helmTillerDescription() { ingressId() {
return _.escape(s__( return INGRESS;
`ClusterIntegration|Helm streamlines installing and managing Kubernetes applications. },
Tiller runs inside of your Kubernetes Cluster, and manages ingressInstalled() {
releases of your charts.`, return this.applications.ingress.status === APPLICATION_INSTALLED;
)); },
ingressExternalIp() {
return this.applications.ingress.externalIp;
}, },
ingressDescription() { ingressDescription() {
const descriptionParagraph = _.escape(s__(
`ClusterIntegration|Ingress gives you a way to route requests to services based on the
request host or path, centralizing a number of services into a single entrypoint.`,
));
const extraCostParagraph = sprintf( const extraCostParagraph = sprintf(
_.escape(s__( _.escape(s__(
`ClusterIntegration|%{boldNotice} This will add some extra resources `ClusterIntegration|%{boldNotice} This will add some extra resources
...@@ -83,9 +91,6 @@ ...@@ -83,9 +91,6 @@
); );
return ` return `
<p>
${descriptionParagraph}
</p>
<p> <p>
${extraCostParagraph} ${extraCostParagraph}
</p> </p>
...@@ -136,33 +141,121 @@ ...@@ -136,33 +141,121 @@
id="helm" id="helm"
:title="applications.helm.title" :title="applications.helm.title"
title-link="https://docs.helm.sh/" title-link="https://docs.helm.sh/"
:description="helmTillerDescription"
:status="applications.helm.status" :status="applications.helm.status"
:status-reason="applications.helm.statusReason" :status-reason="applications.helm.statusReason"
:request-status="applications.helm.requestStatus" :request-status="applications.helm.requestStatus"
:request-reason="applications.helm.requestReason" :request-reason="applications.helm.requestReason"
/> >
<div slot="description">
{{ s__(`ClusterIntegration|Helm streamlines installing
and managing Kubernetes applications.
Tiller runs inside of your Kubernetes Cluster,
and manages releases of your charts.`) }}
</div>
</application-row>
<application-row <application-row
id="ingress" :id="ingressId"
:title="applications.ingress.title" :title="applications.ingress.title"
title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/" title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/"
:description="ingressDescription"
:status="applications.ingress.status" :status="applications.ingress.status"
:status-reason="applications.ingress.statusReason" :status-reason="applications.ingress.statusReason"
:request-status="applications.ingress.requestStatus" :request-status="applications.ingress.requestStatus"
:request-reason="applications.ingress.requestReason" :request-reason="applications.ingress.requestReason"
/> >
<div slot="description">
<p>
{{ s__(`ClusterIntegration|Ingress gives you a way to route
requests to services based on the request host or path,
centralizing a number of services into a single entrypoint.`) }}
</p>
<template v-if="ingressInstalled">
<div class="form-group">
<label for="ingress-ip-address">
{{ s__('ClusterIntegration|Ingress IP Address') }}
</label>
<div
v-if="ingressExternalIp"
class="input-group"
>
<input
type="text"
id="ingress-ip-address"
class="form-control js-ip-address"
:value="ingressExternalIp"
readonly
/>
<span class="input-group-btn">
<clipboard-button
:text="ingressExternalIp"
:title="s__('ClusterIntegration|Copy Ingress IP Address to clipboard')"
css-class="btn btn-default js-clipboard-btn"
/>
</span>
</div>
<input
v-else
type="text"
class="form-control js-ip-address"
readonly
value="?"
/>
</div>
<p
v-if="!ingressExternalIp"
class="settings-message js-no-ip-message"
>
{{ s__(`ClusterIntegration|The IP address is in
the process of being assigned. Please check your Kubernetes
cluster or Quotas on GKE if it takes a long time.`) }}
<a
:href="ingressHelpPath"
target="_blank"
rel="noopener noreferrer"
>
{{ __('More information') }}
</a>
</p>
<p>
{{ s__(`ClusterIntegration|Point a wildcard DNS to this
generated IP address in order to access
your application after it has been deployed.`) }}
<a
:href="ingressDnsHelpPath"
target="_blank"
rel="noopener noreferrer"
>
{{ __('More information') }}
</a>
</p>
</template>
<div
v-else
v-html="ingressDescription"
>
</div>
</div>
</application-row>
<application-row <application-row
id="prometheus" id="prometheus"
:title="applications.prometheus.title" :title="applications.prometheus.title"
title-link="https://prometheus.io/docs/introduction/overview/" title-link="https://prometheus.io/docs/introduction/overview/"
:manage-link="managePrometheusPath" :manage-link="managePrometheusPath"
:description="prometheusDescription"
:status="applications.prometheus.status" :status="applications.prometheus.status"
:status-reason="applications.prometheus.statusReason" :status-reason="applications.prometheus.statusReason"
:request-status="applications.prometheus.requestStatus" :request-status="applications.prometheus.requestStatus"
:request-reason="applications.prometheus.requestReason" :request-reason="applications.prometheus.requestReason"
/> >
<div
slot="description"
v-html="prometheusDescription"
>
</div>
</application-row>
<!-- <!--
NOTE: Don't forget to update `clusters.scss` NOTE: Don't forget to update `clusters.scss`
min-height for this block and uncomment `application_spec` tests min-height for this block and uncomment `application_spec` tests
......
...@@ -10,3 +10,4 @@ export const APPLICATION_ERROR = 'errored'; ...@@ -10,3 +10,4 @@ export const APPLICATION_ERROR = 'errored';
export const REQUEST_LOADING = 'request-loading'; export const REQUEST_LOADING = 'request-loading';
export const REQUEST_SUCCESS = 'request-success'; export const REQUEST_SUCCESS = 'request-success';
export const REQUEST_FAILURE = 'request-failure'; export const REQUEST_FAILURE = 'request-failure';
export const INGRESS = 'ingress';
import { s__ } from '../../locale'; import { s__ } from '../../locale';
import { INGRESS } from '../constants';
export default class ClusterStore { export default class ClusterStore {
constructor() { constructor() {
...@@ -21,6 +22,7 @@ export default class ClusterStore { ...@@ -21,6 +22,7 @@ export default class ClusterStore {
statusReason: null, statusReason: null,
requestStatus: null, requestStatus: null,
requestReason: null, requestReason: null,
externalIp: null,
}, },
runner: { runner: {
title: s__('ClusterIntegration|GitLab Runner'), title: s__('ClusterIntegration|GitLab Runner'),
...@@ -40,9 +42,10 @@ export default class ClusterStore { ...@@ -40,9 +42,10 @@ export default class ClusterStore {
}; };
} }
setHelpPaths(helpPath, ingressHelpPath) { setHelpPaths(helpPath, ingressHelpPath, ingressDnsHelpPath) {
this.state.helpPath = helpPath; this.state.helpPath = helpPath;
this.state.ingressHelpPath = ingressHelpPath; this.state.ingressHelpPath = ingressHelpPath;
this.state.ingressDnsHelpPath = ingressDnsHelpPath;
} }
setManagePrometheusPath(managePrometheusPath) { setManagePrometheusPath(managePrometheusPath) {
...@@ -64,6 +67,7 @@ export default class ClusterStore { ...@@ -64,6 +67,7 @@ export default class ClusterStore {
updateStateFromServer(serverState = {}) { updateStateFromServer(serverState = {}) {
this.state.status = serverState.status; this.state.status = serverState.status;
this.state.statusReason = serverState.status_reason; this.state.statusReason = serverState.status_reason;
serverState.applications.forEach((serverAppEntry) => { serverState.applications.forEach((serverAppEntry) => {
const { const {
name: appId, name: appId,
...@@ -76,6 +80,10 @@ export default class ClusterStore { ...@@ -76,6 +80,10 @@ export default class ClusterStore {
status, status,
statusReason, statusReason,
}; };
if (appId === INGRESS) {
this.state.applications.ingress.externalIp = serverAppEntry.external_ip;
}
}); });
} }
} }
...@@ -28,6 +28,11 @@ ...@@ -28,6 +28,11 @@
required: false, required: false,
default: false, default: false,
}, },
cssClass: {
type: String,
required: false,
default: 'btn btn-default btn-transparent btn-clipboard',
},
}, },
}; };
</script> </script>
...@@ -35,7 +40,7 @@ ...@@ -35,7 +40,7 @@
<template> <template>
<button <button
type="button" type="button"
class="btn btn-transparent btn-clipboard" :class="cssClass"
:title="title" :title="title"
:data-clipboard-text="text" :data-clipboard-text="text"
v-tooltip v-tooltip
......
...@@ -4,6 +4,7 @@ class Projects::ClustersController < Projects::ApplicationController ...@@ -4,6 +4,7 @@ class Projects::ClustersController < Projects::ApplicationController
before_action :authorize_create_cluster!, only: [:new] before_action :authorize_create_cluster!, only: [:new]
before_action :authorize_update_cluster!, only: [:update] before_action :authorize_update_cluster!, only: [:update]
before_action :authorize_admin_cluster!, only: [:destroy] before_action :authorize_admin_cluster!, only: [:destroy]
before_action :update_applications_status, only: [:status]
STATUS_POLLING_INTERVAL = 10_000 STATUS_POLLING_INTERVAL = 10_000
...@@ -114,4 +115,8 @@ class Projects::ClustersController < Projects::ApplicationController ...@@ -114,4 +115,8 @@ class Projects::ClustersController < Projects::ApplicationController
def authorize_admin_cluster! def authorize_admin_cluster!
access_denied! unless can?(current_user, :admin_cluster, cluster) access_denied! unless can?(current_user, :admin_cluster, cluster)
end end
def update_applications_status
@cluster.applications.each(&:schedule_status_update)
end
end end
...@@ -5,6 +5,7 @@ module Clusters ...@@ -5,6 +5,7 @@ module Clusters
include ::Clusters::Concerns::ApplicationCore include ::Clusters::Concerns::ApplicationCore
include ::Clusters::Concerns::ApplicationStatus include ::Clusters::Concerns::ApplicationStatus
include AfterCommitQueue
default_value_for :ingress_type, :nginx default_value_for :ingress_type, :nginx
default_value_for :version, :nginx default_value_for :version, :nginx
...@@ -13,6 +14,17 @@ module Clusters ...@@ -13,6 +14,17 @@ module Clusters
nginx: 1 nginx: 1
} }
FETCH_IP_ADDRESS_DELAY = 30.seconds
state_machine :status do
before_transition any => [:installed] do |application|
application.run_after_commit do
ClusterWaitForIngressIpAddressWorker.perform_in(
FETCH_IP_ADDRESS_DELAY, application.name, application.id)
end
end
end
def chart def chart
'stable/nginx-ingress' 'stable/nginx-ingress'
end end
...@@ -24,6 +36,13 @@ module Clusters ...@@ -24,6 +36,13 @@ module Clusters
def install_command def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new(name, chart: chart, chart_values_file: chart_values_file) Gitlab::Kubernetes::Helm::InstallCommand.new(name, chart: chart, chart_values_file: chart_values_file)
end end
def schedule_status_update
return unless installed?
return if external_ip
ClusterWaitForIngressIpAddressWorker.perform_async(name, id)
end
end end
end end
end end
...@@ -23,6 +23,11 @@ module Clusters ...@@ -23,6 +23,11 @@ module Clusters
def name def name
self.class.application_name self.class.application_name
end end
def schedule_status_update
# Override if you need extra data synchronized
# from K8s after installation
end
end end
end end
end end
......
...@@ -2,4 +2,5 @@ class ClusterApplicationEntity < Grape::Entity ...@@ -2,4 +2,5 @@ class ClusterApplicationEntity < Grape::Entity
expose :name expose :name
expose :status_name, as: :status expose :status_name, as: :status
expose :status_reason expose :status_reason
expose :external_ip, if: -> (e, _) { e.respond_to?(:external_ip) }
end end
module Clusters
module Applications
class CheckIngressIpAddressService < BaseHelmService
include Gitlab::Utils::StrongMemoize
Error = Class.new(StandardError)
LEASE_TIMEOUT = 15.seconds.to_i
def execute
return if app.external_ip
return unless try_obtain_lease
app.update!(external_ip: ingress_ip) if ingress_ip
end
private
def try_obtain_lease
Gitlab::ExclusiveLease
.new("check_ingress_ip_address_service:#{app.id}", timeout: LEASE_TIMEOUT)
.try_obtain
end
def ingress_ip
service.status.loadBalancer.ingress&.first&.ip
end
def service
strong_memoize(:ingress_service) do
kubeclient.get_service('ingress-nginx-ingress-controller', Gitlab::Kubernetes::Helm::NAMESPACE)
end
end
end
end
end
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
cluster_status_reason: @cluster.status_reason, cluster_status_reason: @cluster.status_reason,
help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications'), help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications'),
ingress_help_path: help_page_path('user/project/clusters/index.md', anchor: 'getting-the-external-ip-address'), ingress_help_path: help_page_path('user/project/clusters/index.md', anchor: 'getting-the-external-ip-address'),
ingress_dns_help_path: help_page_path('topics/autodevops/quick_start_guide.md', anchor: 'point-dns-at-cluster-ip'),
manage_prometheus_path: edit_project_service_path(@cluster.project, 'prometheus') } } manage_prometheus_path: edit_project_service_path(@cluster.project, 'prometheus') } }
.js-cluster-application-notice .js-cluster-application-notice
......
...@@ -24,6 +24,7 @@ ...@@ -24,6 +24,7 @@
- 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
- gcp_cluster:check_gcp_project_billing - gcp_cluster:check_gcp_project_billing
- gcp_cluster:cluster_wait_for_ingress_ip_address
- github_import_advance_stage - github_import_advance_stage
- github_importer:github_import_import_diff_note - github_importer:github_import_import_diff_note
......
class ClusterWaitForIngressIpAddressWorker
include ApplicationWorker
include ClusterQueue
include ClusterApplications
def perform(app_name, app_id)
find_application(app_name, app_id) do |app|
Clusters::Applications::CheckIngressIpAddressService.new(app).execute
end
end
end
---
title: Display ingress IP address in the Kubernetes page
merge_request: 17052
author:
type: added
class AddExternalIpToClustersApplicationsIngress < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :clusters_applications_ingress, :external_ip, :string
end
end
...@@ -570,6 +570,7 @@ ActiveRecord::Schema.define(version: 20180301084653) do ...@@ -570,6 +570,7 @@ ActiveRecord::Schema.define(version: 20180301084653) do
t.string "version", null: false t.string "version", null: false
t.string "cluster_ip" t.string "cluster_ip"
t.text "status_reason" t.text "status_reason"
t.string "external_ip"
end end
create_table "clusters_applications_prometheus", force: :cascade do |t| create_table "clusters_applications_prometheus", force: :cascade do |t|
......
...@@ -91,6 +91,12 @@ describe Projects::ClustersController do ...@@ -91,6 +91,12 @@ describe Projects::ClustersController do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('cluster_status') expect(response).to match_response_schema('cluster_status')
end end
it 'invokes schedule_status_update on each application' do
expect_any_instance_of(Clusters::Applications::Ingress).to receive(:schedule_status_update)
go
end
end end
describe 'security' do describe 'security' do
......
...@@ -22,7 +22,7 @@ feature 'Clusters Applications', :js do ...@@ -22,7 +22,7 @@ feature 'Clusters Applications', :js do
scenario 'user is unable to install applications' do scenario 'user is unable to install applications' do
page.within('.js-cluster-application-row-helm') do page.within('.js-cluster-application-row-helm') do
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true') expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
expect(page.find(:css, '.js-cluster-application-install-button').text).to eq('Install') expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install')
end end
end end
end end
...@@ -33,13 +33,13 @@ feature 'Clusters Applications', :js do ...@@ -33,13 +33,13 @@ feature 'Clusters Applications', :js do
scenario 'user can install applications' do scenario 'user can install applications' do
page.within('.js-cluster-application-row-helm') do page.within('.js-cluster-application-row-helm') do
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to be_nil expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to be_nil
expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install') expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install')
end end
end end
context 'when user installs Helm' do context 'when user installs Helm' do
before do before do
allow(ClusterInstallAppWorker).to receive(:perform_async).and_return(nil) allow(ClusterInstallAppWorker).to receive(:perform_async)
page.within('.js-cluster-application-row-helm') do page.within('.js-cluster-application-row-helm') do
page.find(:css, '.js-cluster-application-install-button').click page.find(:css, '.js-cluster-application-install-button').click
...@@ -50,18 +50,18 @@ feature 'Clusters Applications', :js do ...@@ -50,18 +50,18 @@ feature 'Clusters Applications', :js do
page.within('.js-cluster-application-row-helm') do page.within('.js-cluster-application-row-helm') do
# FE sends request and gets the response, then the buttons is "Install" # FE sends request and gets the response, then the buttons is "Install"
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true') expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install') expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install')
Clusters::Cluster.last.application_helm.make_installing! Clusters::Cluster.last.application_helm.make_installing!
# FE starts polling and update the buttons to "Installing" # FE starts polling and update the buttons to "Installing"
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true') expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installing') expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing')
Clusters::Cluster.last.application_helm.make_installed! Clusters::Cluster.last.application_helm.make_installed!
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true') expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installed') expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installed')
end end
expect(page).to have_content('Helm Tiller was successfully installed on your Kubernetes cluster') expect(page).to have_content('Helm Tiller was successfully installed on your Kubernetes cluster')
...@@ -71,11 +71,14 @@ feature 'Clusters Applications', :js do ...@@ -71,11 +71,14 @@ feature 'Clusters Applications', :js do
context 'when user installs Ingress' do context 'when user installs Ingress' do
context 'when user installs application: Ingress' do context 'when user installs application: Ingress' do
before do before do
allow(ClusterInstallAppWorker).to receive(:perform_async).and_return(nil) allow(ClusterInstallAppWorker).to receive(:perform_async)
allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in)
allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
create(:clusters_applications_helm, :installed, cluster: cluster) create(:clusters_applications_helm, :installed, cluster: cluster)
page.within('.js-cluster-application-row-ingress') do page.within('.js-cluster-application-row-ingress') do
expect(page).to have_css('.js-cluster-application-install-button:not([disabled])')
page.find(:css, '.js-cluster-application-install-button').click page.find(:css, '.js-cluster-application-install-button').click
end end
end end
...@@ -83,19 +86,28 @@ feature 'Clusters Applications', :js do ...@@ -83,19 +86,28 @@ feature 'Clusters Applications', :js do
it 'he sees status transition' do it 'he sees status transition' do
page.within('.js-cluster-application-row-ingress') do page.within('.js-cluster-application-row-ingress') do
# FE sends request and gets the response, then the buttons is "Install" # FE sends request and gets the response, then the buttons is "Install"
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true') expect(page).to have_css('.js-cluster-application-install-button[disabled]')
expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install') expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install')
Clusters::Cluster.last.application_ingress.make_installing! Clusters::Cluster.last.application_ingress.make_installing!
# FE starts polling and update the buttons to "Installing" # FE starts polling and update the buttons to "Installing"
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true') expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing')
expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installing') expect(page).to have_css('.js-cluster-application-install-button[disabled]')
# The application becomes installed but we keep waiting for external IP address
Clusters::Cluster.last.application_ingress.make_installed! Clusters::Cluster.last.application_ingress.make_installed!
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true') expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installed')
expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installed') expect(page).to have_css('.js-cluster-application-install-button[disabled]')
expect(page).to have_selector('.js-no-ip-message')
expect(page.find('.js-ip-address').value).to eq('?')
# We receive the external IP address and display
Clusters::Cluster.last.application_ingress.update!(external_ip: '192.168.1.100')
expect(page).not_to have_selector('.js-no-ip-message')
expect(page.find('.js-ip-address').value).to eq('192.168.1.100')
end end
expect(page).to have_content('Ingress was successfully installed on your Kubernetes cluster') expect(page).to have_content('Ingress was successfully installed on your Kubernetes cluster')
......
...@@ -30,7 +30,8 @@ ...@@ -30,7 +30,8 @@
] ]
} }
}, },
"status_reason": { "type": ["string", "null"] } "status_reason": { "type": ["string", "null"] },
"external_ip": { "type": ["string", "null"] }
}, },
"required" : [ "name", "status" ] "required" : [ "name", "status" ]
} }
......
...@@ -44,4 +44,71 @@ describe('Applications', () => { ...@@ -44,4 +44,71 @@ describe('Applications', () => {
}); });
/* */ /* */
}); });
describe('Ingress application', () => {
describe('when installed', () => {
describe('with ip address', () => {
it('renders ip address with a clipboard button', () => {
vm = mountComponent(Applications, {
applications: {
ingress: {
title: 'Ingress',
status: 'installed',
externalIp: '0.0.0.0',
},
helm: { title: 'Helm Tiller' },
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
},
});
expect(
vm.$el.querySelector('.js-ip-address').value,
).toEqual('0.0.0.0');
expect(
vm.$el.querySelector('.js-clipboard-btn').getAttribute('data-clipboard-text'),
).toEqual('0.0.0.0');
});
});
describe('without ip address', () => {
it('renders an input text with a question mark and an alert text', () => {
vm = mountComponent(Applications, {
applications: {
ingress: {
title: 'Ingress',
status: 'installed',
},
helm: { title: 'Helm Tiller' },
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
},
});
expect(
vm.$el.querySelector('.js-ip-address').value,
).toEqual('?');
expect(vm.$el.querySelector('.js-no-ip-message')).not.toBe(null);
});
});
});
describe('before installing', () => {
it('does not render the IP address', () => {
vm = mountComponent(Applications, {
applications: {
helm: { title: 'Helm Tiller' },
ingress: { title: 'Ingress' },
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
},
});
expect(vm.$el.textContent).not.toContain('Ingress IP Address');
expect(vm.$el.querySelector('.js-ip-address')).toBe(null);
});
});
});
}); });
...@@ -18,6 +18,7 @@ const CLUSTERS_MOCK_DATA = { ...@@ -18,6 +18,7 @@ const CLUSTERS_MOCK_DATA = {
name: 'ingress', name: 'ingress',
status: APPLICATION_ERROR, status: APPLICATION_ERROR,
status_reason: 'Cannot connect', status_reason: 'Cannot connect',
external_ip: null,
}, { }, {
name: 'runner', name: 'runner',
status: APPLICATION_INSTALLING, status: APPLICATION_INSTALLING,
......
...@@ -75,6 +75,7 @@ describe('Clusters Store', () => { ...@@ -75,6 +75,7 @@ describe('Clusters Store', () => {
statusReason: mockResponseData.applications[1].status_reason, statusReason: mockResponseData.applications[1].status_reason,
requestStatus: null, requestStatus: null,
requestReason: null, requestReason: null,
externalIp: null,
}, },
runner: { runner: {
title: 'GitLab Runner', title: 'GitLab Runner',
......
...@@ -4,5 +4,52 @@ describe Clusters::Applications::Ingress do ...@@ -4,5 +4,52 @@ describe Clusters::Applications::Ingress do
it { is_expected.to belong_to(:cluster) } it { is_expected.to belong_to(:cluster) }
it { is_expected.to validate_presence_of(:cluster) } it { is_expected.to validate_presence_of(:cluster) }
before do
allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in)
allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
end
include_examples 'cluster application specs', described_class include_examples 'cluster application specs', described_class
describe '#make_installed!' do
before do
application.make_installed!
end
let(:application) { create(:clusters_applications_ingress, :installing) }
it 'schedules a ClusterWaitForIngressIpAddressWorker' do
expect(ClusterWaitForIngressIpAddressWorker).to have_received(:perform_in)
.with(Clusters::Applications::Ingress::FETCH_IP_ADDRESS_DELAY, 'ingress', application.id)
end
end
describe '#schedule_status_update' do
let(:application) { create(:clusters_applications_ingress, :installed) }
before do
application.schedule_status_update
end
it 'schedules a ClusterWaitForIngressIpAddressWorker' do
expect(ClusterWaitForIngressIpAddressWorker).to have_received(:perform_async)
.with('ingress', application.id)
end
context 'when the application is not installed' do
let(:application) { create(:clusters_applications_ingress, :installing) }
it 'does not schedule a ClusterWaitForIngressIpAddressWorker' do
expect(ClusterWaitForIngressIpAddressWorker).not_to have_received(:perform_async)
end
end
context 'when there is already an external_ip' do
let(:application) { create(:clusters_applications_ingress, :installed, external_ip: '111.222.222.111') }
it 'does not schedule a ClusterWaitForIngressIpAddressWorker' do
expect(ClusterWaitForIngressIpAddressWorker).not_to have_received(:perform_in)
end
end
end
end end
...@@ -26,5 +26,19 @@ describe ClusterApplicationEntity do ...@@ -26,5 +26,19 @@ describe ClusterApplicationEntity do
expect(subject[:status_reason]).to eq(application.status_reason) expect(subject[:status_reason]).to eq(application.status_reason)
end end
end end
context 'for ingress application' do
let(:application) do
build(
:clusters_applications_ingress,
:installed,
external_ip: '111.222.111.222'
)
end
it 'includes external_ip' do
expect(subject[:external_ip]).to eq('111.222.111.222')
end
end
end end
end end
require 'spec_helper'
describe Clusters::Applications::CheckIngressIpAddressService do
let(:application) { create(:clusters_applications_ingress, :installed) }
let(:service) { described_class.new(application) }
let(:kubeclient) { double(::Kubeclient::Client, get_service: kube_service) }
let(:ingress) { [{ ip: '111.222.111.222' }] }
let(:exclusive_lease) { instance_double(Gitlab::ExclusiveLease, try_obtain: true) }
let(:kube_service) do
::Kubeclient::Resource.new(
{
status: {
loadBalancer: {
ingress: ingress
}
}
}
)
end
subject { service.execute }
before do
allow(application.cluster).to receive(:kubeclient).and_return(kubeclient)
allow(Gitlab::ExclusiveLease)
.to receive(:new)
.with("check_ingress_ip_address_service:#{application.id}", timeout: 15.seconds.to_i)
.and_return(exclusive_lease)
end
describe '#execute' do
context 'when the ingress ip address is available' do
it 'updates the external_ip for the app' do
subject
expect(application.external_ip).to eq('111.222.111.222')
end
end
context 'when the ingress ip address is not available' do
let(:ingress) { nil }
it 'does not error' do
subject
end
end
context 'when the exclusive lease cannot be obtained' do
before do
allow(exclusive_lease)
.to receive(:try_obtain)
.and_return(false)
end
it 'does not call kubeclient' do
subject
expect(kubeclient).not_to have_received(:get_service)
end
end
context 'when there is already an external_ip' do
let(:application) { create(:clusters_applications_ingress, :installed, external_ip: '001.111.002.111') }
it 'does not call kubeclient' do
subject
expect(kubeclient).not_to have_received(:get_service)
end
end
end
end
require 'spec_helper'
describe ClusterWaitForIngressIpAddressWorker do
describe '#perform' do
let(:service) { instance_double(Clusters::Applications::CheckIngressIpAddressService, execute: true) }
let(:application) { instance_double(Clusters::Applications::Ingress) }
let(:worker) { described_class.new }
before do
allow(worker)
.to receive(:find_application)
.with('ingress', 117)
.and_yield(application)
allow(Clusters::Applications::CheckIngressIpAddressService)
.to receive(:new)
.with(application)
.and_return(service)
allow(described_class)
.to receive(:perform_in)
end
it 'finds the application and calls CheckIngressIpAddressService#execute' do
worker.perform('ingress', 117)
expect(service).to have_received(:execute)
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