diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index b6da572b201..27959898fb7 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -458,7 +458,6 @@ export default { Applications::Knative }.freeze APPLICATIONS = { Applications::Helm.application_name => Applications::Helm, @@ -18,7 +17,8 @@ module Clusters Applications::CertManager.application_name => Applications::CertManager, Applications::Prometheus.application_name => Applications::Prometheus, Applications::Runner.application_name => Applications::Runner, - Applications::Jupyter.application_name => Applications::Jupyter + Applications::Jupyter.application_name => Applications::Jupyter, + Applications::Knative.application_name => Applications::Knative }.merge(PROJECT_ONLY_APPLICATIONS).freeze DEFAULT_ENVIRONMENT = '*' KUBE_INGRESS_BASE_DOMAIN = 'KUBE_INGRESS_BASE_DOMAIN' diff --git a/changelogs/unreleased/instance-group-level-knative.yml b/changelogs/unreleased/instance-group-level-knative.yml new file mode 100644 index 00000000000..5108334a3ea --- /dev/null +++ b/changelogs/unreleased/instance-group-level-knative.yml @@ -0,0 +1,5 @@ +--- +title: Allow Knative to be installed on group and instance level clusters +merge_request: 32128 +author: +type: added diff --git a/doc/user/clusters/applications.md b/doc/user/clusters/applications.md index 83ddcf61664..f922b8911ae 100644 --- a/doc/user/clusters/applications.md +++ b/doc/user/clusters/applications.md @@ -188,7 +188,8 @@ You can clone repositories from the files tab in Jupyter: ### Knative -> Available for project-level clusters since GitLab 11.5. +> - Available for project-level clusters since GitLab 11.5. +> - Available for group-level and instance-level clusters since GitLab 12.3. [Knative](https://cloud.google.com/knative) provides a platform to create, deploy, and manage serverless workloads from a Kubernetes diff --git a/spec/features/admin/clusters/applications_spec.rb b/spec/features/admin/clusters/applications_spec.rb new file mode 100644 index 00000000000..3d35a27b559 --- /dev/null +++ b/spec/features/admin/clusters/applications_spec.rb @@ -0,0 +1,240 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Instance-level Cluster Applications', :js do + include GoogleApi::CloudPlatformHelpers + + let(:user) { create(:admin) } + + before do + sign_in(user) + end + + describe 'Installing applications' do + before do + visit admin_cluster_path(cluster) + end + + context 'when cluster is being created' do + let(:cluster) { create(:cluster, :providing_by_gcp, :instance) } + + it 'user is unable to install applications' do + expect(page).not_to have_css('.js-cluster-application-row-helm') + expect(page).not_to have_css('.js-cluster-application-install-button') + end + end + + context 'when cluster is created' do + let(:cluster) { create(:cluster, :provided_by_gcp, :instance) } + + it 'user can install applications' do + wait_for_requests + + page.within('.js-cluster-application-row-helm') do + expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to be_nil + 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) + + page.within('.js-cluster-application-row-helm') do + page.find(:css, '.js-cluster-application-install-button').click + end + + wait_for_requests + end + + it 'they see status transition' do + page.within('.js-cluster-application-row-helm') do + # FE sends request and gets the response, then the buttons is "Installing" + expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing') + + Clusters::Cluster.last.application_helm.make_installing! + + # FE starts polling and update the buttons to "Installing" + expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing') + + Clusters::Cluster.last.application_helm.make_installed! + + expect(page).not_to have_css('.js-cluster-application-install-button') + expect(page).to have_css('.js-cluster-application-uninstall-button:not([disabled])', exact_text: 'Uninstall') + end + + expect(page).to have_content('Helm Tiller was successfully installed on your Kubernetes cluster') + end + end + + context 'when user installs Knative' do + before do + create(:clusters_applications_helm, :installed, cluster: cluster) + end + + context 'on an abac cluster' do + let(:cluster) { create(:cluster, :provided_by_gcp, :instance, :rbac_disabled) } + + it 'shows info block and not be installable' do + page.within('.js-cluster-application-row-knative') do + expect(page).to have_css('.rbac-notice') + expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true') + end + end + end + + context 'on an rbac cluster' do + let(:cluster) { create(:cluster, :provided_by_gcp, :instance) } + + it 'does not show callout block and be installable' do + page.within('.js-cluster-application-row-knative') do + expect(page).not_to have_css('.rbac-notice') + expect(page).to have_css('.js-cluster-application-install-button:not([disabled])') + end + end + + describe 'when user clicks install button' do + def domainname_form_value + page.find('.js-knative-domainname').value + end + + before do + allow(ClusterInstallAppWorker).to receive(:perform_async) + allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in) + allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async) + + page.within('.js-cluster-application-row-knative') do + expect(page).to have_css('.js-cluster-application-install-button:not([disabled])') + + page.find('.js-knative-domainname').set("domain.example.org") + + click_button 'Install' + + wait_for_requests + + expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing') + + Clusters::Cluster.last.application_knative.make_installing! + Clusters::Cluster.last.application_knative.make_installed! + Clusters::Cluster.last.application_knative.update_attribute(:external_ip, '127.0.0.1') + end + end + + it 'shows status transition' do + page.within('.js-cluster-application-row-knative') do + expect(domainname_form_value).to eq('domain.example.org') + expect(page).to have_css('.js-cluster-application-uninstall-button', exact_text: 'Uninstall') + end + + expect(page).to have_content('Knative was successfully installed on your Kubernetes cluster') + expect(page).to have_css('.js-knative-save-domain-button'), exact_text: 'Save changes' + end + + it 'can then update the domain' do + page.within('.js-cluster-application-row-knative') do + expect(ClusterPatchAppWorker).to receive(:perform_async) + + expect(domainname_form_value).to eq('domain.example.org') + + page.find('.js-knative-domainname').set("new.domain.example.org") + + click_button 'Save changes' + + wait_for_requests + + expect(domainname_form_value).to eq('new.domain.example.org') + end + end + end + end + end + + context 'when user installs Cert Manager' do + before do + 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-cert_manager') do + click_button 'Install' + end + end + + it 'shows status transition' do + def email_form_value + page.find('.js-email').value + end + + page.within('.js-cluster-application-row-cert_manager') do + expect(email_form_value).to eq(cluster.user.email) + expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing') + + page.find('.js-email').set("new_email@example.org") + Clusters::Cluster.last.application_cert_manager.make_installing! + + expect(email_form_value).to eq('new_email@example.org') + expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing') + + Clusters::Cluster.last.application_cert_manager.make_installed! + + expect(email_form_value).to eq('new_email@example.org') + expect(page).to have_css('.js-cluster-application-uninstall-button', exact_text: 'Uninstall') + end + + expect(page).to have_content('Cert-Manager was successfully installed on your Kubernetes cluster') + end + end + + context 'when user installs Ingress' do + context 'when user installs application: Ingress' do + before do + 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 + + wait_for_requests + end + end + + it 'they see status transition' do + page.within('.js-cluster-application-row-ingress') do + # FE sends request and gets the response, then the buttons is "Installing" + expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing') + + Clusters::Cluster.last.application_ingress.make_installing! + + # FE starts polling and update the buttons to "Installing" + expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing') + + # The application becomes installed but we keep waiting for external IP address + Clusters::Cluster.last.application_ingress.make_installed! + + expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installed') + expect(page).to have_selector('.js-no-endpoint-message') + expect(page).to have_selector('.js-ingress-ip-loading-icon') + + # 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_css('.js-cluster-application-install-button') + expect(page).to have_css('.js-cluster-application-uninstall-button:not([disabled])', exact_text: 'Uninstall') + expect(page).not_to have_selector('.js-no-endpoint-message') + expect(page.find('.js-endpoint').value).to eq('192.168.1.100') + end + + expect(page).to have_content('Ingress was successfully installed on your Kubernetes cluster') + end + end + end + end + end +end diff --git a/spec/features/groups/clusters/applications_spec.rb b/spec/features/groups/clusters/applications_spec.rb new file mode 100644 index 00000000000..5ab8d8048e2 --- /dev/null +++ b/spec/features/groups/clusters/applications_spec.rb @@ -0,0 +1,242 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Group-level Cluster Applications', :js do + include GoogleApi::CloudPlatformHelpers + + let(:group) { create(:group) } + let(:user) { create(:user) } + + before do + group.add_maintainer(user) + sign_in(user) + end + + describe 'Installing applications' do + before do + visit group_cluster_path(group, cluster) + end + + context 'when cluster is being created' do + let(:cluster) { create(:cluster_for_group, :providing_by_gcp, groups: [group]) } + + it 'user is unable to install applications' do + expect(page).not_to have_css('.js-cluster-application-row-helm') + expect(page).not_to have_css('.js-cluster-application-install-button') + end + end + + context 'when cluster is created' do + let(:cluster) { create(:cluster_for_group, groups: [group]) } + + it 'user can install applications' do + wait_for_requests + + page.within('.js-cluster-application-row-helm') do + expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to be_nil + 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) + + page.within('.js-cluster-application-row-helm') do + page.find(:css, '.js-cluster-application-install-button').click + end + + wait_for_requests + end + + it 'they see status transition' do + page.within('.js-cluster-application-row-helm') do + # FE sends request and gets the response, then the buttons is "Installing" + expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing') + + Clusters::Cluster.last.application_helm.make_installing! + + # FE starts polling and update the buttons to "Installing" + expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing') + + Clusters::Cluster.last.application_helm.make_installed! + + expect(page).not_to have_css('.js-cluster-application-install-button') + expect(page).to have_css('.js-cluster-application-uninstall-button:not([disabled])', exact_text: 'Uninstall') + end + + expect(page).to have_content('Helm Tiller was successfully installed on your Kubernetes cluster') + end + end + + context 'when user installs Knative' do + before do + create(:clusters_applications_helm, :installed, cluster: cluster) + end + + context 'on an abac cluster' do + let(:cluster) { create(:cluster_for_group, :rbac_disabled, groups: [group]) } + + it 'shows info block and not be installable' do + page.within('.js-cluster-application-row-knative') do + expect(page).to have_css('.rbac-notice') + expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true') + end + end + end + + context 'on an rbac cluster' do + let(:cluster) { create(:cluster_for_group, groups: [group]) } + + it 'does not show callout block and be installable' do + page.within('.js-cluster-application-row-knative') do + expect(page).not_to have_css('.rbac-notice') + expect(page).to have_css('.js-cluster-application-install-button:not([disabled])') + end + end + + describe 'when user clicks install button' do + def domainname_form_value + page.find('.js-knative-domainname').value + end + + before do + allow(ClusterInstallAppWorker).to receive(:perform_async) + allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in) + allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async) + + page.within('.js-cluster-application-row-knative') do + expect(page).to have_css('.js-cluster-application-install-button:not([disabled])') + + page.find('.js-knative-domainname').set("domain.example.org") + + click_button 'Install' + + wait_for_requests + + expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing') + + Clusters::Cluster.last.application_knative.make_installing! + Clusters::Cluster.last.application_knative.make_installed! + Clusters::Cluster.last.application_knative.update_attribute(:external_ip, '127.0.0.1') + end + end + + it 'shows status transition' do + page.within('.js-cluster-application-row-knative') do + expect(domainname_form_value).to eq('domain.example.org') + expect(page).to have_css('.js-cluster-application-uninstall-button', exact_text: 'Uninstall') + end + + expect(page).to have_content('Knative was successfully installed on your Kubernetes cluster') + expect(page).to have_css('.js-knative-save-domain-button'), exact_text: 'Save changes' + end + + it 'can then update the domain' do + page.within('.js-cluster-application-row-knative') do + expect(ClusterPatchAppWorker).to receive(:perform_async) + + expect(domainname_form_value).to eq('domain.example.org') + + page.find('.js-knative-domainname').set("new.domain.example.org") + + click_button 'Save changes' + + wait_for_requests + + expect(domainname_form_value).to eq('new.domain.example.org') + end + end + end + end + end + + context 'when user installs Cert Manager' do + before do + 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-cert_manager') do + click_button 'Install' + end + end + + it 'shows status transition' do + def email_form_value + page.find('.js-email').value + end + + page.within('.js-cluster-application-row-cert_manager') do + expect(email_form_value).to eq(cluster.user.email) + expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing') + + page.find('.js-email').set("new_email@example.org") + Clusters::Cluster.last.application_cert_manager.make_installing! + + expect(email_form_value).to eq('new_email@example.org') + expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing') + + Clusters::Cluster.last.application_cert_manager.make_installed! + + expect(email_form_value).to eq('new_email@example.org') + expect(page).to have_css('.js-cluster-application-uninstall-button', exact_text: 'Uninstall') + end + + expect(page).to have_content('Cert-Manager was successfully installed on your Kubernetes cluster') + end + end + + context 'when user installs Ingress' do + context 'when user installs application: Ingress' do + before do + 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 + + wait_for_requests + end + end + + it 'they see status transition' do + page.within('.js-cluster-application-row-ingress') do + # FE sends request and gets the response, then the buttons is "Installing" + expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing') + + Clusters::Cluster.last.application_ingress.make_installing! + + # FE starts polling and update the buttons to "Installing" + expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing') + + # The application becomes installed but we keep waiting for external IP address + Clusters::Cluster.last.application_ingress.make_installed! + + expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installed') + expect(page).to have_selector('.js-no-endpoint-message') + expect(page).to have_selector('.js-ingress-ip-loading-icon') + + # 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_css('.js-cluster-application-install-button') + expect(page).to have_css('.js-cluster-application-uninstall-button:not([disabled])', exact_text: 'Uninstall') + expect(page).not_to have_selector('.js-no-endpoint-message') + expect(page.find('.js-endpoint').value).to eq('192.168.1.100') + end + + expect(page).to have_content('Ingress was successfully installed on your Kubernetes cluster') + end + end + end + end + end +end diff --git a/spec/frontend/clusters/components/applications_spec.js b/spec/frontend/clusters/components/applications_spec.js index 1d8984cea0a..fbcab078993 100644 --- a/spec/frontend/clusters/components/applications_spec.js +++ b/spec/frontend/clusters/components/applications_spec.js @@ -89,7 +89,7 @@ describe('Applications', () => { }); it('renders a row for Knative', () => { - expect(vm.$el.querySelector('.js-cluster-application-row-knative')).toBeNull(); + expect(vm.$el.querySelector('.js-cluster-application-row-knative')).not.toBeNull(); }); }); @@ -126,7 +126,7 @@ describe('Applications', () => { }); it('renders a row for Knative', () => { - expect(vm.$el.querySelector('.js-cluster-application-row-knative')).toBeNull(); + expect(vm.$el.querySelector('.js-cluster-application-row-knative')).not.toBeNull(); }); });