diff options
author | Kamil Trzciński <ayufan@ayufan.eu> | 2018-03-01 17:38:04 +0000 |
---|---|---|
committer | Kamil Trzciński <ayufan@ayufan.eu> | 2018-03-01 17:38:04 +0000 |
commit | ccb080d94aa765d8391f262e25c5ead0764dc2ff (patch) | |
tree | 643e50759d3d92617606850ac987507c0ed59774 | |
parent | 52e133d17627396f29c1875b972534eaba4a837e (diff) | |
parent | ebac9c81ad1e859afe73482dd6f112b141cf9ede (diff) | |
download | gitlab-ce-ccb080d94aa765d8391f262e25c5ead0764dc2ff.tar.gz |
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
27 files changed, 494 insertions, 43 deletions
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index b070a59cf15..01aec4f36af 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -37,10 +37,11 @@ export default class Clusters { clusterStatusReason, helpPath, ingressHelpPath, + ingressDnsHelpPath, } = document.querySelector('.js-edit-cluster-form').dataset; this.store = new ClustersStore(); - this.store.setHelpPaths(helpPath, ingressHelpPath); + this.store.setHelpPaths(helpPath, ingressHelpPath, ingressDnsHelpPath); this.store.setManagePrometheusPath(managePrometheusPath); this.store.updateStatus(clusterStatus); this.store.updateStatusReason(clusterStatusReason); @@ -98,6 +99,7 @@ export default class Clusters { helpPath: this.state.helpPath, ingressHelpPath: this.state.ingressHelpPath, managePrometheusPath: this.state.managePrometheusPath, + ingressDnsHelpPath: this.state.ingressDnsHelpPath, }, }); }, diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue index 50e35bbbba5..c2a35341eb2 100644 --- a/app/assets/javascripts/clusters/components/application_row.vue +++ b/app/assets/javascripts/clusters/components/application_row.vue @@ -36,10 +36,6 @@ type: String, required: false, }, - description: { - type: String, - required: true, - }, status: { type: String, required: false, @@ -148,7 +144,7 @@ class="table-section section-wrap" role="gridcell" > - <div v-html="description"></div> + <slot name="description"></slot> </div> <div class="table-section table-button-footer section-align-top" diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index 978881a4831..35618398468 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -2,10 +2,16 @@ import _ from 'underscore'; import { s__, sprintf } from '../../locale'; import applicationRow from './application_row.vue'; + import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; + import { + APPLICATION_INSTALLED, + INGRESS, + } from '../constants'; export default { components: { applicationRow, + clipboardButton, }, props: { applications: { @@ -23,6 +29,11 @@ required: false, default: '', }, + ingressDnsHelpPath: { + type: String, + required: false, + default: '', + }, managePrometheusPath: { type: String, required: false, @@ -43,19 +54,16 @@ false, ); }, - helmTillerDescription() { - return _.escape(s__( - `ClusterIntegration|Helm streamlines installing and managing Kubernetes applications. - Tiller runs inside of your Kubernetes Cluster, and manages - releases of your charts.`, - )); + ingressId() { + return INGRESS; + }, + ingressInstalled() { + return this.applications.ingress.status === APPLICATION_INSTALLED; + }, + ingressExternalIp() { + return this.applications.ingress.externalIp; }, 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( _.escape(s__( `ClusterIntegration|%{boldNotice} This will add some extra resources @@ -84,9 +92,6 @@ return ` <p> - ${descriptionParagraph} - </p> - <p> ${extraCostParagraph} </p> <p class="settings-message append-bottom-0"> @@ -136,33 +141,121 @@ id="helm" :title="applications.helm.title" title-link="https://docs.helm.sh/" - :description="helmTillerDescription" :status="applications.helm.status" :status-reason="applications.helm.statusReason" :request-status="applications.helm.requestStatus" :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 - id="ingress" + :id="ingressId" :title="applications.ingress.title" title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/" - :description="ingressDescription" :status="applications.ingress.status" :status-reason="applications.ingress.statusReason" :request-status="applications.ingress.requestStatus" :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 id="prometheus" :title="applications.prometheus.title" title-link="https://prometheus.io/docs/introduction/overview/" :manage-link="managePrometheusPath" - :description="prometheusDescription" :status="applications.prometheus.status" :status-reason="applications.prometheus.statusReason" :request-status="applications.prometheus.requestStatus" :request-reason="applications.prometheus.requestReason" - /> + > + <div + slot="description" + v-html="prometheusDescription" + > + </div> + </application-row> <!-- NOTE: Don't forget to update `clusters.scss` min-height for this block and uncomment `application_spec` tests diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js index 93223aefff8..b7179f52bb3 100644 --- a/app/assets/javascripts/clusters/constants.js +++ b/app/assets/javascripts/clusters/constants.js @@ -10,3 +10,4 @@ export const APPLICATION_ERROR = 'errored'; export const REQUEST_LOADING = 'request-loading'; export const REQUEST_SUCCESS = 'request-success'; export const REQUEST_FAILURE = 'request-failure'; +export const INGRESS = 'ingress'; diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js index 904ee5fd475..348bbec3b25 100644 --- a/app/assets/javascripts/clusters/stores/clusters_store.js +++ b/app/assets/javascripts/clusters/stores/clusters_store.js @@ -1,4 +1,5 @@ import { s__ } from '../../locale'; +import { INGRESS } from '../constants'; export default class ClusterStore { constructor() { @@ -21,6 +22,7 @@ export default class ClusterStore { statusReason: null, requestStatus: null, requestReason: null, + externalIp: null, }, runner: { title: s__('ClusterIntegration|GitLab Runner'), @@ -40,9 +42,10 @@ export default class ClusterStore { }; } - setHelpPaths(helpPath, ingressHelpPath) { + setHelpPaths(helpPath, ingressHelpPath, ingressDnsHelpPath) { this.state.helpPath = helpPath; this.state.ingressHelpPath = ingressHelpPath; + this.state.ingressDnsHelpPath = ingressDnsHelpPath; } setManagePrometheusPath(managePrometheusPath) { @@ -64,6 +67,7 @@ export default class ClusterStore { updateStateFromServer(serverState = {}) { this.state.status = serverState.status; this.state.statusReason = serverState.status_reason; + serverState.applications.forEach((serverAppEntry) => { const { name: appId, @@ -76,6 +80,10 @@ export default class ClusterStore { status, statusReason, }; + + if (appId === INGRESS) { + this.state.applications.ingress.externalIp = serverAppEntry.external_ip; + } }); } } diff --git a/app/assets/javascripts/vue_shared/components/clipboard_button.vue b/app/assets/javascripts/vue_shared/components/clipboard_button.vue index e855ec3c098..3b6c2da1664 100644 --- a/app/assets/javascripts/vue_shared/components/clipboard_button.vue +++ b/app/assets/javascripts/vue_shared/components/clipboard_button.vue @@ -28,6 +28,11 @@ required: false, default: false, }, + cssClass: { + type: String, + required: false, + default: 'btn btn-default btn-transparent btn-clipboard', + }, }, }; </script> @@ -35,7 +40,7 @@ <template> <button type="button" - class="btn btn-transparent btn-clipboard" + :class="cssClass" :title="title" :data-clipboard-text="text" v-tooltip diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb index 142e8b6e4bc..aeaba3a0acf 100644 --- a/app/controllers/projects/clusters_controller.rb +++ b/app/controllers/projects/clusters_controller.rb @@ -4,6 +4,7 @@ class Projects::ClustersController < Projects::ApplicationController before_action :authorize_create_cluster!, only: [:new] before_action :authorize_update_cluster!, only: [:update] before_action :authorize_admin_cluster!, only: [:destroy] + before_action :update_applications_status, only: [:status] STATUS_POLLING_INTERVAL = 10_000 @@ -114,4 +115,8 @@ class Projects::ClustersController < Projects::ApplicationController def authorize_admin_cluster! access_denied! unless can?(current_user, :admin_cluster, cluster) end + + def update_applications_status + @cluster.applications.each(&:schedule_status_update) + end end diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb index aa5cf97756f..9f583342c19 100644 --- a/app/models/clusters/applications/ingress.rb +++ b/app/models/clusters/applications/ingress.rb @@ -5,6 +5,7 @@ module Clusters include ::Clusters::Concerns::ApplicationCore include ::Clusters::Concerns::ApplicationStatus + include AfterCommitQueue default_value_for :ingress_type, :nginx default_value_for :version, :nginx @@ -13,6 +14,17 @@ module Clusters 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 'stable/nginx-ingress' end @@ -24,6 +36,13 @@ module Clusters def install_command Gitlab::Kubernetes::Helm::InstallCommand.new(name, chart: chart, chart_values_file: chart_values_file) end + + def schedule_status_update + return unless installed? + return if external_ip + + ClusterWaitForIngressIpAddressWorker.perform_async(name, id) + end end end end diff --git a/app/models/clusters/concerns/application_core.rb b/app/models/clusters/concerns/application_core.rb index a98fa85a5ff..623b836c0ed 100644 --- a/app/models/clusters/concerns/application_core.rb +++ b/app/models/clusters/concerns/application_core.rb @@ -23,6 +23,11 @@ module Clusters def name self.class.application_name end + + def schedule_status_update + # Override if you need extra data synchronized + # from K8s after installation + end end end end diff --git a/app/serializers/cluster_application_entity.rb b/app/serializers/cluster_application_entity.rb index 3f9a275ad08..b22a0b666ef 100644 --- a/app/serializers/cluster_application_entity.rb +++ b/app/serializers/cluster_application_entity.rb @@ -2,4 +2,5 @@ class ClusterApplicationEntity < Grape::Entity expose :name expose :status_name, as: :status expose :status_reason + expose :external_ip, if: -> (e, _) { e.respond_to?(:external_ip) } end diff --git a/app/services/clusters/applications/check_ingress_ip_address_service.rb b/app/services/clusters/applications/check_ingress_ip_address_service.rb new file mode 100644 index 00000000000..e572b1e5d99 --- /dev/null +++ b/app/services/clusters/applications/check_ingress_ip_address_service.rb @@ -0,0 +1,36 @@ +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 diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml index 2b1b23ba198..179c45a9867 100644 --- a/app/views/projects/clusters/show.html.haml +++ b/app/views/projects/clusters/show.html.haml @@ -15,6 +15,7 @@ cluster_status_reason: @cluster.status_reason, 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_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') } } .js-cluster-application-notice diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index a9415410f8a..328db19be29 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -24,6 +24,7 @@ - gcp_cluster:cluster_wait_for_app_installation - gcp_cluster:wait_for_cluster_creation - gcp_cluster:check_gcp_project_billing +- gcp_cluster:cluster_wait_for_ingress_ip_address - github_import_advance_stage - github_importer:github_import_import_diff_note diff --git a/app/workers/cluster_wait_for_ingress_ip_address_worker.rb b/app/workers/cluster_wait_for_ingress_ip_address_worker.rb new file mode 100644 index 00000000000..8ba5951750c --- /dev/null +++ b/app/workers/cluster_wait_for_ingress_ip_address_worker.rb @@ -0,0 +1,11 @@ +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 diff --git a/changelogs/unreleased/42643-persist-external-ip-of-ingress-controller-gke.yml b/changelogs/unreleased/42643-persist-external-ip-of-ingress-controller-gke.yml new file mode 100644 index 00000000000..35457db82f4 --- /dev/null +++ b/changelogs/unreleased/42643-persist-external-ip-of-ingress-controller-gke.yml @@ -0,0 +1,5 @@ +--- +title: Display ingress IP address in the Kubernetes page +merge_request: 17052 +author: +type: added diff --git a/db/migrate/20180212030105_add_external_ip_to_clusters_applications_ingress.rb b/db/migrate/20180212030105_add_external_ip_to_clusters_applications_ingress.rb new file mode 100644 index 00000000000..dbe09a43aa7 --- /dev/null +++ b/db/migrate/20180212030105_add_external_ip_to_clusters_applications_ingress.rb @@ -0,0 +1,9 @@ +class AddExternalIpToClustersApplicationsIngress < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :clusters_applications_ingress, :external_ip, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index a6ccd9dd907..773cf8b4d3f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -570,6 +570,7 @@ ActiveRecord::Schema.define(version: 20180301084653) do t.string "version", null: false t.string "cluster_ip" t.text "status_reason" + t.string "external_ip" end create_table "clusters_applications_prometheus", force: :cascade do |t| diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb index 954fc79f57d..15ce418d0d6 100644 --- a/spec/controllers/projects/clusters_controller_spec.rb +++ b/spec/controllers/projects/clusters_controller_spec.rb @@ -91,6 +91,12 @@ describe Projects::ClustersController do expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('cluster_status') 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 describe 'security' do diff --git a/spec/features/projects/clusters/applications_spec.rb b/spec/features/projects/clusters/applications_spec.rb index 8d1e10b7191..7b2c57aa652 100644 --- a/spec/features/projects/clusters/applications_spec.rb +++ b/spec/features/projects/clusters/applications_spec.rb @@ -22,7 +22,7 @@ feature 'Clusters Applications', :js do scenario 'user is unable to install applications' 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').text).to eq('Install') + expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install') end end end @@ -33,13 +33,13 @@ feature 'Clusters Applications', :js do scenario 'user can install applications' 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')).to have_content('Install') + expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install') end end context 'when user installs Helm' 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.find(:css, '.js-cluster-application-install-button').click @@ -50,18 +50,18 @@ feature 'Clusters Applications', :js do page.within('.js-cluster-application-row-helm') do # 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')).to have_content('Install') + expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install') Clusters::Cluster.last.application_helm.make_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')).to have_content('Installing') + expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing') 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')).to have_content('Installed') + expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installed') end expect(page).to have_content('Helm Tiller was successfully installed on your Kubernetes cluster') @@ -71,11 +71,14 @@ feature 'Clusters Applications', :js do context 'when user installs Ingress' do context 'when user installs application: Ingress' 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) 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 end end @@ -83,19 +86,28 @@ feature 'Clusters Applications', :js do it 'he sees status transition' do page.within('.js-cluster-application-row-ingress') do # 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')).to have_content('Install') + expect(page).to have_css('.js-cluster-application-install-button[disabled]') + expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install') Clusters::Cluster.last.application_ingress.make_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')).to have_content('Installing') + expect(page).to have_css('.js-cluster-application-install-button', exact_text: '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! - 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') + 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 expect(page).to have_content('Ingress was successfully installed on your Kubernetes cluster') diff --git a/spec/fixtures/api/schemas/cluster_status.json b/spec/fixtures/api/schemas/cluster_status.json index 489d563be2b..d27c12e43f2 100644 --- a/spec/fixtures/api/schemas/cluster_status.json +++ b/spec/fixtures/api/schemas/cluster_status.json @@ -30,7 +30,8 @@ ] } }, - "status_reason": { "type": ["string", "null"] } + "status_reason": { "type": ["string", "null"] }, + "external_ip": { "type": ["string", "null"] } }, "required" : [ "name", "status" ] } diff --git a/spec/javascripts/clusters/components/applications_spec.js b/spec/javascripts/clusters/components/applications_spec.js index 15832c38f25..dfb4cc1b9b1 100644 --- a/spec/javascripts/clusters/components/applications_spec.js +++ b/spec/javascripts/clusters/components/applications_spec.js @@ -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); + }); + }); + }); }); diff --git a/spec/javascripts/clusters/services/mock_data.js b/spec/javascripts/clusters/services/mock_data.js index 253b3c45243..6ae7a792329 100644 --- a/spec/javascripts/clusters/services/mock_data.js +++ b/spec/javascripts/clusters/services/mock_data.js @@ -18,6 +18,7 @@ const CLUSTERS_MOCK_DATA = { name: 'ingress', status: APPLICATION_ERROR, status_reason: 'Cannot connect', + external_ip: null, }, { name: 'runner', status: APPLICATION_INSTALLING, diff --git a/spec/javascripts/clusters/stores/clusters_store_spec.js b/spec/javascripts/clusters/stores/clusters_store_spec.js index 726a4ed30de..8028faf2f02 100644 --- a/spec/javascripts/clusters/stores/clusters_store_spec.js +++ b/spec/javascripts/clusters/stores/clusters_store_spec.js @@ -75,6 +75,7 @@ describe('Clusters Store', () => { statusReason: mockResponseData.applications[1].status_reason, requestStatus: null, requestReason: null, + externalIp: null, }, runner: { title: 'GitLab Runner', diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb index 619c088b0bf..a34f4ff2b48 100644 --- a/spec/models/clusters/applications/ingress_spec.rb +++ b/spec/models/clusters/applications/ingress_spec.rb @@ -4,5 +4,52 @@ describe Clusters::Applications::Ingress do it { is_expected.to belong_to(: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 + + 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 diff --git a/spec/serializers/cluster_application_entity_spec.rb b/spec/serializers/cluster_application_entity_spec.rb index b5a55b4ef6e..852b6af9f7f 100644 --- a/spec/serializers/cluster_application_entity_spec.rb +++ b/spec/serializers/cluster_application_entity_spec.rb @@ -26,5 +26,19 @@ describe ClusterApplicationEntity do expect(subject[:status_reason]).to eq(application.status_reason) 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 diff --git a/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb b/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb new file mode 100644 index 00000000000..bf038595a4d --- /dev/null +++ b/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb @@ -0,0 +1,73 @@ +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 diff --git a/spec/workers/cluster_wait_for_ingress_ip_address_worker_spec.rb b/spec/workers/cluster_wait_for_ingress_ip_address_worker_spec.rb new file mode 100644 index 00000000000..2e2e9afd25a --- /dev/null +++ b/spec/workers/cluster_wait_for_ingress_ip_address_worker_spec.rb @@ -0,0 +1,30 @@ +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 |