diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index d386960f3b6ef7f72f6760723228d02b4aacb8e7..7ea8901ecbb7ab4fa88c6db51b2798fe824d034b 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -41,6 +41,8 @@ export default class Clusters { managePrometheusPath, clusterEnvironmentsPath, hasRbac, + providerType, + preInstalledKnative, clusterType, clusterStatus, clusterStatusReason, @@ -50,6 +52,7 @@ export default class Clusters { environmentsHelpPath, clustersHelpPath, deployBoardsHelpPath, + cloudRunHelpPath, clusterId, } = document.querySelector('.js-edit-cluster-form').dataset; @@ -65,10 +68,13 @@ export default class Clusters { environmentsHelpPath, clustersHelpPath, deployBoardsHelpPath, + cloudRunHelpPath, ); this.store.setManagePrometheusPath(managePrometheusPath); this.store.updateStatus(clusterStatus); this.store.updateStatusReason(clusterStatusReason); + this.store.updateProviderType(providerType); + this.store.updatePreInstalledKnative(preInstalledKnative); this.store.updateRbac(hasRbac); this.service = new ClustersService({ endpoint: statusPath, @@ -153,6 +159,9 @@ export default class Clusters { ingressHelpPath: this.state.ingressHelpPath, managePrometheusPath: this.state.managePrometheusPath, ingressDnsHelpPath: this.state.ingressDnsHelpPath, + cloudRunHelpPath: this.state.cloudRunHelpPath, + providerType: this.state.providerType, + preInstalledKnative: this.state.preInstalledKnative, rbac: this.state.rbac, }, }); diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue index 64364092016d6f92f719e6a1886411e359eecd44..c6c8dc6352c4f087696dbb0b45c9c2f83a9f8367 100644 --- a/app/assets/javascripts/clusters/components/application_row.vue +++ b/app/assets/javascripts/clusters/components/application_row.vue @@ -78,6 +78,10 @@ export default { required: false, default: false, }, + installedVia: { + type: String, + required: false, + }, version: { type: String, required: false, @@ -311,6 +315,11 @@ export default { > <span v-else class="js-cluster-application-title">{{ title }}</span> </strong> + <span + v-if="installedVia" + class="js-cluster-application-installed-via" + v-html="installedVia" + ></span> <slot name="description"></slot> <div v-if="hasError" class="cluster-application-error text-danger prepend-top-10"> <p class="js-cluster-application-general-error-message append-bottom-0"> diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index 27959898fb79491920302a59faf8381b743f82f8..4d3e759d8d4857f4ee212bef1d9b20dce7d7a698 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -16,7 +16,7 @@ import { s__, sprintf } from '../../locale'; import applicationRow from './application_row.vue'; import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; import KnativeDomainEditor from './knative_domain_editor.vue'; -import { CLUSTER_TYPE, APPLICATION_STATUS, INGRESS } from '../constants'; +import { CLUSTER_TYPE, PROVIDER_TYPE, APPLICATION_STATUS, INGRESS } from '../constants'; import LoadingButton from '~/vue_shared/components/loading_button.vue'; import eventHub from '~/clusters/event_hub'; @@ -54,11 +54,26 @@ export default { required: false, default: '', }, + cloudRunHelpPath: { + type: String, + required: false, + default: '', + }, managePrometheusPath: { type: String, required: false, default: '', }, + providerType: { + type: String, + required: false, + default: '', + }, + preInstalledKnative: { + type: Boolean, + required: false, + default: false, + }, rbac: { type: Boolean, required: false, @@ -156,6 +171,25 @@ export default { knative() { return this.applications.knative; }, + cloudRun() { + return this.providerType === PROVIDER_TYPE.GCP && this.preInstalledKnative; + }, + installedVia() { + if (this.cloudRun) { + return sprintf( + _.escape(s__(`ClusterIntegration|installed via %{installed_via}`)), + { + installed_via: `<a href="${ + this.cloudRunHelpPath + }" target="_blank" rel="noopener noreferrer">${_.escape( + s__('ClusterIntegration|Cloud Run'), + )}</a>`, + }, + false, + ); + } + return null; + }, }, created() { this.helmInstallIllustration = helmInstallIllustration; @@ -468,6 +502,7 @@ export default { :installed="applications.knative.installed" :install-failed="applications.knative.installFailed" :install-application-request-params="{ hostname: applications.knative.hostname }" + :installed-via="installedVia" :uninstallable="applications.knative.uninstallable" :uninstall-successful="applications.knative.uninstallSuccessful" :uninstall-failed="applications.knative.uninstallFailed" @@ -499,7 +534,7 @@ export default { </p> <knative-domain-editor - v-if="knative.installed || (helmInstalled && rbac)" + v-if="(knative.installed || (helmInstalled && rbac)) && !preInstalledKnative" :knative="knative" :ingress-dns-help-path="ingressDnsHelpPath" @save="saveKnativeDomain" diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js index 8fd752092c982e7f3103917894ac9396054afe94..c6e4b7951cf90b0c6b05303bc3bae354471a1ee6 100644 --- a/app/assets/javascripts/clusters/constants.js +++ b/app/assets/javascripts/clusters/constants.js @@ -5,6 +5,11 @@ export const CLUSTER_TYPE = { PROJECT: 'project_type', }; +// These need to match the available providers in app/models/clusters/providers/ +export const PROVIDER_TYPE = { + GCP: 'gcp', +}; + // These need to match what is returned from the server export const APPLICATION_STATUS = { NO_STATUS: null, @@ -19,6 +24,7 @@ export const APPLICATION_STATUS = { UNINSTALLING: 'uninstalling', UNINSTALL_ERRORED: 'uninstall_errored', ERROR: 'errored', + PRE_INSTALLED: 'pre_installed', }; /* @@ -29,6 +35,7 @@ export const APPLICATION_INSTALLED_STATUSES = [ APPLICATION_STATUS.INSTALLED, APPLICATION_STATUS.UPDATING, APPLICATION_STATUS.UNINSTALLING, + APPLICATION_STATUS.PRE_INSTALLED, ]; // These are only used client-side diff --git a/app/assets/javascripts/clusters/services/application_state_machine.js b/app/assets/javascripts/clusters/services/application_state_machine.js index 6e632519d8a2b28979cee0df1aeb8ed8e6c18cdc..6bc4be7b93ae0d73d7144aea8bf0bbf636b82492 100644 --- a/app/assets/javascripts/clusters/services/application_state_machine.js +++ b/app/assets/javascripts/clusters/services/application_state_machine.js @@ -13,6 +13,7 @@ const { UPDATE_ERRORED, UNINSTALLING, UNINSTALL_ERRORED, + PRE_INSTALLED, } = APPLICATION_STATUS; const applicationStateMachine = { @@ -63,6 +64,9 @@ const applicationStateMachine = { uninstallFailed: true, }, }, + [PRE_INSTALLED]: { + target: PRE_INSTALLED, + }, }, }, [NOT_INSTALLABLE]: { @@ -123,6 +127,27 @@ const applicationStateMachine = { }, }, }, + [PRE_INSTALLED]: { + on: { + [UPDATE_EVENT]: { + target: UPDATING, + effects: { + updateFailed: false, + updateSuccessful: false, + }, + }, + [NOT_INSTALLABLE]: { + target: NOT_INSTALLABLE, + }, + [UNINSTALL_EVENT]: { + target: UNINSTALLING, + effects: { + uninstallFailed: false, + uninstallSuccessful: false, + }, + }, + }, + }, [UPDATING]: { on: { [UPDATED]: { diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js index 5cddb4cc0982bfd22c8b7cf61c3fd75fb2bc4d82..6464461ea0c2736ee6beae3bf2f3b7c1f4704205 100644 --- a/app/assets/javascripts/clusters/stores/clusters_store.js +++ b/app/assets/javascripts/clusters/stores/clusters_store.js @@ -35,7 +35,10 @@ export default class ClusterStore { environmentsHelpPath: null, clustersHelpPath: null, deployBoardsHelpPath: null, + cloudRunHelpPath: null, status: null, + providerType: null, + preInstalledKnative: false, rbac: false, statusReason: null, applications: { @@ -95,6 +98,7 @@ export default class ClusterStore { environmentsHelpPath, clustersHelpPath, deployBoardsHelpPath, + cloudRunHelpPath, ) { this.state.helpPath = helpPath; this.state.ingressHelpPath = ingressHelpPath; @@ -102,6 +106,7 @@ export default class ClusterStore { this.state.environmentsHelpPath = environmentsHelpPath; this.state.clustersHelpPath = clustersHelpPath; this.state.deployBoardsHelpPath = deployBoardsHelpPath; + this.state.cloudRunHelpPath = cloudRunHelpPath; } setManagePrometheusPath(managePrometheusPath) { @@ -112,6 +117,14 @@ export default class ClusterStore { this.state.status = status; } + updateProviderType(providerType) { + this.state.providerType = providerType; + } + + updatePreInstalledKnative(preInstalledKnative) { + this.state.preInstalledKnative = parseBoolean(preInstalledKnative); + } + updateRbac(rbac) { this.state.rbac = parseBoolean(rbac); } diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb index 15f1e8284ffd8a700f43c1c92651622c58595117..993aba661f3f83b8c2647b795332c97da1ab00b4 100644 --- a/app/controllers/clusters/clusters_controller.rb +++ b/app/controllers/clusters/clusters_controller.rb @@ -170,6 +170,7 @@ def create_gcp_cluster_params :zone, :num_nodes, :machine_type, + :cloud_run, :legacy_abac ]).merge( provider_type: :gcp, diff --git a/app/models/clusters/applications/helm.rb b/app/models/clusters/applications/helm.rb index 455cf200fbc2ee67a210ab4a5a5e58531f357215..261f6ce898705959f241818331f6a7fc848f403d 100644 --- a/app/models/clusters/applications/helm.rb +++ b/app/models/clusters/applications/helm.rb @@ -27,7 +27,7 @@ def issue_client_cert def set_initial_status return unless not_installable? - self.status = 'installable' if cluster&.platform_kubernetes_active? + self.status = status_states[:installable] if cluster&.platform_kubernetes_active? end # It can only be uninstalled if there are no other applications installed diff --git a/app/models/clusters/applications/jupyter.rb b/app/models/clusters/applications/jupyter.rb index ec65482a846fffe9090228c708187f0f1c3f08b0..ca93bc15be08cd4ee5c1dac68ad367cfee579791 100644 --- a/app/models/clusters/applications/jupyter.rb +++ b/app/models/clusters/applications/jupyter.rb @@ -23,7 +23,7 @@ def set_initial_status return unless cluster&.application_ingress_available? ingress = cluster.application_ingress - self.status = 'installable' if ingress.external_ip_or_hostname? + self.status = status_states[:installable] if ingress.external_ip_or_hostname? end def chart diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb index a9b9374622dda7e2fb47e379dc789351ccdba0f4..f2a3695d2eb84bc5aedc31b45a063e9f84e6647b 100644 --- a/app/models/clusters/applications/knative.rb +++ b/app/models/clusters/applications/knative.rb @@ -21,7 +21,7 @@ def set_initial_status return unless not_installable? return unless verify_cluster? - self.status = 'installable' + self.status = status_states[:installable] end state_machine :status do @@ -47,6 +47,10 @@ def values { "domain" => hostname }.to_yaml end + def allowed_to_uninstall? + !pre_installed? + end + def install_command Gitlab::Kubernetes::Helm::InstallCommand.new( name: name, diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 6a5b98a46768b0352bd004fdb23fdf5e270c1a40..3459c566ff3a2cd6469705cbdf924ba9fb6eb7f2 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -194,6 +194,10 @@ def predefined_variables end end + def knative_pre_installed? + provider&.knative_pre_installed? + end + private def instance_domain diff --git a/app/models/clusters/concerns/application_core.rb b/app/models/clusters/concerns/application_core.rb index d1b57a21a7d79c710ef8303f55ba6d9978033281..e748c0a855d0d76869a6a614597ece444c23ac13 100644 --- a/app/models/clusters/concerns/application_core.rb +++ b/app/models/clusters/concerns/application_core.rb @@ -15,7 +15,7 @@ module ApplicationCore def set_initial_status return unless not_installable? - self.status = 'installable' if cluster&.application_helm_available? + self.status = status_states[:installable] if cluster&.application_helm_available? end def can_uninstall? diff --git a/app/models/clusters/concerns/application_status.rb b/app/models/clusters/concerns/application_status.rb index 342d766f7230a8ddb0de6643872445257b314734..b63a596dfeecce530587c2a553221c50082a919f 100644 --- a/app/models/clusters/concerns/application_status.rb +++ b/app/models/clusters/concerns/application_status.rb @@ -28,6 +28,13 @@ module ApplicationStatus state :uninstalling, value: 7 state :uninstall_errored, value: 8 + # Used for applications that are pre-installed by the cluster, + # e.g. Knative in GCP Cloud Run enabled clusters + # Because we cannot upgrade or uninstall Knative in these clusters, + # we define only one simple state transition to enter the `pre_installed` state, + # and no exit transitions. + state :pre_installed, value: 9 + event :make_scheduled do transition [:installable, :errored, :installed, :updated, :update_errored, :uninstall_errored] => :scheduled end @@ -41,6 +48,10 @@ module ApplicationStatus transition [:updating] => :updated end + event :make_pre_installed do + transition any => :pre_installed + end + event :make_errored do transition any - [:updating, :uninstalling] => :errored transition [:updating] => :update_errored @@ -90,12 +101,18 @@ module ApplicationStatus end end + def status_states + self.class.state_machines[:status].states.each_with_object({}) do |state, states| + states[state.name] = state.value + end + end + def updateable? installed? || updated? || update_errored? end def available? - installed? || updated? + pre_installed? || installed? || updated? end def update_in_progress? diff --git a/app/models/clusters/providers/gcp.rb b/app/models/clusters/providers/gcp.rb index 390748bf252b83446551cce622bdacdf843be415..043765f79ac5f2d8ffd9b8c5bc44b6c4e976a1ad 100644 --- a/app/models/clusters/providers/gcp.rb +++ b/app/models/clusters/providers/gcp.rb @@ -10,6 +10,9 @@ class Gcp < ApplicationRecord default_value_for :zone, 'us-central1-a' default_value_for :num_nodes, 3 default_value_for :machine_type, 'n1-standard-2' + default_value_for :cloud_run, false + + scope :cloud_run, -> { where(cloud_run: true) } attr_encrypted :access_token, mode: :per_attribute_iv, @@ -77,6 +80,10 @@ def api_client @api_client ||= GoogleApi::CloudPlatform::Client.new(access_token, nil) end + + def knative_pre_installed? + cloud_run? + end end end end diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb index c5cde8319647f8af869c1dbc8e5110fa402cbb5c..0aff1bcc8b90fc4c9e99af05c1161893453448ba 100644 --- a/app/services/clusters/gcp/finalize_creation_service.rb +++ b/app/services/clusters/gcp/finalize_creation_service.rb @@ -11,6 +11,7 @@ def execute(provider) configure_provider create_gitlab_service_account! configure_kubernetes + configure_pre_installed_knative if provider.knative_pre_installed? cluster.save! rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e log_service_error(e.class.name, provider.id, e.message) @@ -48,6 +49,13 @@ def configure_kubernetes token: request_kubernetes_token) end + def configure_pre_installed_knative + knative = cluster.build_application_knative( + hostname: 'example.com' + ) + knative.make_pre_installed! + end + def request_kubernetes_token Clusters::Kubernetes::FetchKubernetesTokenService.new( kube_client, diff --git a/app/services/clusters/gcp/provision_service.rb b/app/services/clusters/gcp/provision_service.rb index 80040511ec2c2c39bc39d5267b36cc5d1880e5fd..7dc2d3c32f13b84a5ce6274676ac997a797c23cd 100644 --- a/app/services/clusters/gcp/provision_service.rb +++ b/app/services/clusters/gcp/provision_service.rb @@ -3,6 +3,8 @@ module Clusters module Gcp class ProvisionService + CLOUD_RUN_ADDONS = %i[http_load_balancing istio_config cloud_run_config].freeze + attr_reader :provider def execute(provider) @@ -22,13 +24,16 @@ def execute(provider) private def get_operation_id + enable_addons = provider.cloud_run? ? CLOUD_RUN_ADDONS : [] + operation = provider.api_client.projects_zones_clusters_create( provider.gcp_project_id, provider.zone, provider.cluster.name, provider.num_nodes, machine_type: provider.machine_type, - legacy_abac: provider.legacy_abac + legacy_abac: provider.legacy_abac, + enable_addons: enable_addons ) unless operation.status == 'PENDING' || operation.status == 'RUNNING' diff --git a/app/views/clusters/clusters/gcp/_form.html.haml b/app/views/clusters/clusters/gcp/_form.html.haml index 4d3e3359ea0f223d3564d1f340bd2db853d51780..196ad422766928787f5b42cba614b8906c2e0925 100644 --- a/app/views/clusters/clusters/gcp/_form.html.haml +++ b/app/views/clusters/clusters/gcp/_form.html.haml @@ -65,6 +65,13 @@ %p.form-text.text-muted = s_('ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}.').html_safe % { help_link_start_machine_type: help_link_start % { url: machine_type_link_url }, help_link_start_pricing: help_link_start % { url: pricing_link_url }, help_link_end: help_link_end } + .form-group + = provider_gcp_field.check_box :cloud_run, { label: s_('ClusterIntegration|Enable Cloud Run on GKE (beta)'), + label_class: 'label-bold' } + .form-text.text-muted + = s_('ClusterIntegration|Uses the Cloud Run, Istio, and HTTP Load Balancing addons for this cluster.') + = link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'cloud-run-on-gke'), target: '_blank' + .form-group = field.check_box :managed, { label: s_('ClusterIntegration|GitLab-managed cluster'), label_class: 'label-bold' } diff --git a/app/views/clusters/clusters/show.html.haml b/app/views/clusters/clusters/show.html.haml index 00fdd5e9562aae09ceb144294c29475d0bd1d471..859a07d05b15d2f6974dc4de0fd243d64f09a9e3 100644 --- a/app/views/clusters/clusters/show.html.haml +++ b/app/views/clusters/clusters/show.html.haml @@ -22,12 +22,15 @@ cluster_type: @cluster.cluster_type, cluster_status: @cluster.status_name, cluster_status_reason: @cluster.status_reason, + provider_type: @cluster.provider_type, + pre_installed_knative: @cluster.knative_pre_installed? ? 'true': 'false', 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-endpoint'), ingress_dns_help_path: help_page_path('user/project/clusters/index.md', anchor: 'manually-determining-the-external-endpoint'), environments_help_path: help_page_path('ci/environments', anchor: 'defining-environments'), clusters_help_path: help_page_path('user/project/clusters/index.md', anchor: 'deploying-to-a-kubernetes-cluster'), deploy_boards_help_path: help_page_path('user/project/deploy_boards.html', anchor: 'enabling-deploy-boards'), + cloud_run_help_path: help_page_path('user/project/clusters/index.md', anchor: 'cloud-run-on-gke'), manage_prometheus_path: manage_prometheus_path, cluster_id: @cluster.id } } diff --git a/changelogs/unreleased/27502-enable-cloud-run-on-gke.yml b/changelogs/unreleased/27502-enable-cloud-run-on-gke.yml new file mode 100644 index 0000000000000000000000000000000000000000..77b365f17dbff78e4ef4c73d8704be47b093544d --- /dev/null +++ b/changelogs/unreleased/27502-enable-cloud-run-on-gke.yml @@ -0,0 +1,5 @@ +--- +title: Enable Cloud Run on GKE cluster creation +merge_request: 16566 +author: +type: added diff --git a/config/initializers/google_api_client.rb b/config/initializers/google_api_client.rb new file mode 100644 index 0000000000000000000000000000000000000000..611726a20c7d168a891a9ba3be6fc3f857f4a0ec --- /dev/null +++ b/config/initializers/google_api_client.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true +# +# google-api-client >= 0.26.0 supports enabling CloudRun and Istio during +# cluster creation, but fog-google currently hard deps on '~> 0.23.0', which +# prevents us from upgrading. We are injecting these options as hashes below +# as a workaround until this is resolved. +# +# This can be removed once fog-google and google-api-client can be upgraded. +# See https://gitlab.com/gitlab-org/gitlab-ce/issues/66630 for more details. +# + +require 'google/apis/container_v1beta1' + +Google::Apis::ContainerV1beta1::AddonsConfig::Representation.tap do |representation| + representation.hash :cloud_run_config, as: 'cloudRunConfig' + representation.hash :istio_config, as: 'istioConfig' +end diff --git a/db/migrate/20190905140605_add_cloud_run_to_clusters_providers_gcp.rb b/db/migrate/20190905140605_add_cloud_run_to_clusters_providers_gcp.rb new file mode 100644 index 0000000000000000000000000000000000000000..e7ffd7cd4d366a85c6ce206926b8d13210d5fa6c --- /dev/null +++ b/db/migrate/20190905140605_add_cloud_run_to_clusters_providers_gcp.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddCloudRunToClustersProvidersGcp < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column_with_default(:cluster_providers_gcp, :cloud_run, :boolean, default: false) + end + + def down + remove_column(:cluster_providers_gcp, :cloud_run) + end +end diff --git a/db/migrate/20190919162036_add_index_to_clusters_providers_gcp_on_cloud_run.rb b/db/migrate/20190919162036_add_index_to_clusters_providers_gcp_on_cloud_run.rb new file mode 100644 index 0000000000000000000000000000000000000000..8e0bde97cc1909f4b235b1cce0e5df60fc75fe59 --- /dev/null +++ b/db/migrate/20190919162036_add_index_to_clusters_providers_gcp_on_cloud_run.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddIndexToClustersProvidersGcpOnCloudRun < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index(:cluster_providers_gcp, :cloud_run) + end + + def down + remove_concurrent_index(:cluster_providers_gcp, :cloud_run) + end +end diff --git a/db/schema.rb b/db/schema.rb index b60593132f5fb7d12a3397877cc012f7c7117a15..392db66f5b66dceb6d29e85112df272e66140c14 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_09_18_104222) do +ActiveRecord::Schema.define(version: 2019_09_19_162036) do # These are extensions that must be enabled in order to support this database enable_extension "pg_trgm" @@ -965,6 +965,8 @@ t.text "encrypted_access_token" t.string "encrypted_access_token_iv" t.boolean "legacy_abac", default: false, null: false + t.boolean "cloud_run", default: false, null: false + t.index ["cloud_run"], name: "index_cluster_providers_gcp_on_cloud_run" t.index ["cluster_id"], name: "index_cluster_providers_gcp_on_cluster_id", unique: true end diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index 97fa973d3e3583c9cc99275a31f1d040e26378bc..49878978154eb7ce44cd6d20f2c42850babd927c 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -154,6 +154,7 @@ new Kubernetes cluster to your project: - **Number of nodes** - Enter the number of nodes you wish the cluster to have. - **Machine type** - The [machine type](https://cloud.google.com/compute/docs/machine-types) of the Virtual Machine instance that the cluster will be based on. + - **Enable Cloud Run on GKE (beta)** - Check this if you want to use Cloud Run on GKE for this cluster. See the [Cloud Run on GKE section](#cloud-run-on-gke) for more information. - **GitLab-managed cluster** - Leave this checked if you want GitLab to manage namespaces and service accounts for this cluster. See the [Managed clusters section](#gitlab-managed-clusters) for more information. 1. Finally, click the **Create Kubernetes cluster** button. @@ -339,6 +340,15 @@ functionalities needed to successfully build and deploy a containerized application. Bear in mind that the same credentials are used for all the applications running on the cluster. +### Cloud Run on GKE + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/16566) in GitLab 12.4. + +You can choose to use Cloud Run on GKE in place of installing Knative and Istio +separately after the cluster has been created. This means that Cloud Run +(Knative), Istio, and HTTP Load Balancing will be enabled on the cluster at +create time and cannot be [installed or uninstalled](../../clusters/applications.md) separately. + ### GitLab-managed clusters > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/22011) in GitLab 11.5. diff --git a/lib/google_api/cloud_platform/client.rb b/lib/google_api/cloud_platform/client.rb index 9f01a3f97cee3adcc4d24fa3e5c026b4034a5335..eaf94708282b631e7e581b82f349195d76815404 100644 --- a/lib/google_api/cloud_platform/client.rb +++ b/lib/google_api/cloud_platform/client.rb @@ -2,6 +2,7 @@ require 'google/apis/compute_v1' require 'google/apis/container_v1' +require 'google/apis/container_v1beta1' require 'google/apis/cloudbilling_v1' require 'google/apis/cloudresourcemanager_v1' @@ -53,30 +54,13 @@ def projects_zones_clusters_get(project_id, zone, cluster_id) service.get_zone_cluster(project_id, zone, cluster_id, options: user_agent_header) end - def projects_zones_clusters_create(project_id, zone, cluster_name, cluster_size, machine_type:, legacy_abac:) - service = Google::Apis::ContainerV1::ContainerService.new + def projects_zones_clusters_create(project_id, zone, cluster_name, cluster_size, machine_type:, legacy_abac:, enable_addons: []) + service = Google::Apis::ContainerV1beta1::ContainerService.new service.authorization = access_token - request_body = Google::Apis::ContainerV1::CreateClusterRequest.new( - { - "cluster": { - "name": cluster_name, - "initial_node_count": cluster_size, - "node_config": { - "machine_type": machine_type - }, - "master_auth": { - "username": CLUSTER_MASTER_AUTH_USERNAME, - "client_certificate_config": { - issue_client_certificate: true - } - }, - "legacy_abac": { - "enabled": legacy_abac - } - } - } - ) + cluster_options = make_cluster_options(cluster_name, cluster_size, machine_type, legacy_abac, enable_addons) + + request_body = Google::Apis::ContainerV1beta1::CreateClusterRequest.new(cluster_options) service.create_cluster(project_id, zone, request_body, options: user_agent_header) end @@ -95,6 +79,30 @@ def parse_operation_id(self_link) private + def make_cluster_options(cluster_name, cluster_size, machine_type, legacy_abac, enable_addons) + { + cluster: { + name: cluster_name, + initial_node_count: cluster_size, + node_config: { + machine_type: machine_type + }, + master_auth: { + username: CLUSTER_MASTER_AUTH_USERNAME, + client_certificate_config: { + issue_client_certificate: true + } + }, + legacy_abac: { + enabled: legacy_abac + }, + addons_config: enable_addons.each_with_object({}) do |addon, hash| + hash[addon] = { disabled: false } + end + } + } + end + def token_life_time(expires_at) DateTime.strptime(expires_at, '%s').to_time.utc - Time.now.utc end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 9be6cbca00c86e2172ef165af1695cd227b88a07..a9dabed430bfedfbbd880a24b3e960010852cc3f 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3325,6 +3325,9 @@ msgstr "" msgid "ClusterIntegration|Choose which of your environments will use this cluster." msgstr "" +msgid "ClusterIntegration|Cloud Run" +msgstr "" + msgid "ClusterIntegration|Cluster health" msgstr "" @@ -3364,6 +3367,9 @@ msgstr "" msgid "ClusterIntegration|Did you know?" msgstr "" +msgid "ClusterIntegration|Enable Cloud Run on GKE (beta)" +msgstr "" + msgid "ClusterIntegration|Enable or disable GitLab's connection to your Kubernetes cluster." msgstr "" @@ -3724,6 +3730,9 @@ msgstr "" msgid "ClusterIntegration|Update failed. Please check the logs and try again." msgstr "" +msgid "ClusterIntegration|Uses the Cloud Run, Istio, and HTTP Load Balancing addons for this cluster." +msgstr "" + msgid "ClusterIntegration|Validating project billing status" msgstr "" @@ -3760,6 +3769,9 @@ msgstr "" msgid "ClusterIntegration|help page" msgstr "" +msgid "ClusterIntegration|installed via %{installed_via}" +msgstr "" + msgid "ClusterIntegration|meets the requirements" msgstr "" diff --git a/spec/factories/clusters/clusters.rb b/spec/factories/clusters/clusters.rb index d294e6d055e72f976e486ce8c4b84b7b55014cfb..29aea5e403e72d280c7b8987aa14d04cd780a723 100644 --- a/spec/factories/clusters/clusters.rb +++ b/spec/factories/clusters/clusters.rb @@ -58,6 +58,10 @@ platform_kubernetes factory: [:cluster_platform_kubernetes, :configured, :rbac_disabled] end + trait :cloud_run_enabled do + provider_gcp factory: [:cluster_provider_gcp, :created, :cloud_run_enabled] + end + trait :disabled do enabled false end diff --git a/spec/factories/clusters/providers/gcp.rb b/spec/factories/clusters/providers/gcp.rb index 22462651b6af1052798618ee47259309945d3199..7fdcdebad346dbd2b9c7e4faffbf10beb4023968 100644 --- a/spec/factories/clusters/providers/gcp.rb +++ b/spec/factories/clusters/providers/gcp.rb @@ -34,5 +34,9 @@ trait :abac_enabled do legacy_abac true end + + trait :cloud_run_enabled do + cloud_run true + end end end diff --git a/spec/frontend/clusters/stores/clusters_store_spec.js b/spec/frontend/clusters/stores/clusters_store_spec.js index ee3b7d8aa90a30837d5a1d2e38eb90ea8efbbace..5ee06eb44c94ad049028e425ee263fbcbe0efb02 100644 --- a/spec/frontend/clusters/stores/clusters_store_spec.js +++ b/spec/frontend/clusters/stores/clusters_store_spec.js @@ -54,8 +54,11 @@ describe('Clusters Store', () => { environmentsHelpPath: null, clustersHelpPath: null, deployBoardsHelpPath: null, + cloudRunHelpPath: null, status: mockResponseData.status, statusReason: mockResponseData.status_reason, + providerType: null, + preInstalledKnative: false, rbac: false, applications: { helm: { diff --git a/spec/initializers/google_api_client_spec.rb b/spec/initializers/google_api_client_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..44a1bc0836cdceaa2ebd2ed45a2bccf8817791a9 --- /dev/null +++ b/spec/initializers/google_api_client_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe './config/initializers/google_api_client.rb' do + subject { Google::Apis::ContainerV1beta1 } + + it 'is needed' do |example| + is_expected.not_to be_const_defined(:CloudRunConfig), + <<-MSG.strip_heredoc + The google-api-client gem has been upgraded! + Remove: + #{example.example_group.description} + #{example.file_path} + MSG + end +end diff --git a/spec/lib/google_api/cloud_platform/client_spec.rb b/spec/lib/google_api/cloud_platform/client_spec.rb index c24998d32f876e568308d1d42448c09fcfc67be8..2253feb376dfb421bd3d49c8242b319f2d0801ce 100644 --- a/spec/lib/google_api/cloud_platform/client_spec.rb +++ b/spec/lib/google_api/cloud_platform/client_spec.rb @@ -68,7 +68,7 @@ describe '#projects_zones_clusters_create' do subject do client.projects_zones_clusters_create( - project_id, zone, cluster_name, cluster_size, machine_type: machine_type, legacy_abac: legacy_abac) + project_id, zone, cluster_name, cluster_size, machine_type: machine_type, legacy_abac: legacy_abac, enable_addons: enable_addons) end let(:project_id) { 'project-123' } @@ -77,39 +77,51 @@ let(:cluster_size) { 1 } let(:machine_type) { 'n1-standard-2' } let(:legacy_abac) { true } - let(:create_cluster_request_body) { double('Google::Apis::ContainerV1::CreateClusterRequest') } + let(:enable_addons) { [] } + + let(:addons_config) do + enable_addons.each_with_object({}) do |addon, hash| + hash[addon] = { disabled: false } + end + end + + let(:cluster_options) do + { + cluster: { + name: cluster_name, + initial_node_count: cluster_size, + node_config: { + machine_type: machine_type + }, + master_auth: { + username: 'admin', + client_certificate_config: { + issue_client_certificate: true + } + }, + legacy_abac: { + enabled: legacy_abac + }, + addons_config: addons_config + } + } + end + + let(:create_cluster_request_body) { double('Google::Apis::ContainerV1beta1::CreateClusterRequest') } let(:operation) { double } before do - allow_any_instance_of(Google::Apis::ContainerV1::ContainerService) + allow_any_instance_of(Google::Apis::ContainerV1beta1::ContainerService) .to receive(:create_cluster).with(any_args) .and_return(operation) end it 'sets corresponded parameters' do - expect_any_instance_of(Google::Apis::ContainerV1::ContainerService) + expect_any_instance_of(Google::Apis::ContainerV1beta1::ContainerService) .to receive(:create_cluster).with(project_id, zone, create_cluster_request_body, options: user_agent_options) - expect(Google::Apis::ContainerV1::CreateClusterRequest) - .to receive(:new).with( - { - "cluster": { - "name": cluster_name, - "initial_node_count": cluster_size, - "node_config": { - "machine_type": machine_type - }, - "master_auth": { - "username": "admin", - "client_certificate_config": { - issue_client_certificate: true - } - }, - "legacy_abac": { - "enabled": true - } - } - } ).and_return(create_cluster_request_body) + expect(Google::Apis::ContainerV1beta1::CreateClusterRequest) + .to receive(:new).with(cluster_options).and_return(create_cluster_request_body) expect(subject).to eq operation end @@ -118,29 +130,25 @@ let(:legacy_abac) { false } it 'sets corresponded parameters' do - expect_any_instance_of(Google::Apis::ContainerV1::ContainerService) + expect_any_instance_of(Google::Apis::ContainerV1beta1::ContainerService) + .to receive(:create_cluster).with(project_id, zone, create_cluster_request_body, options: user_agent_options) + + expect(Google::Apis::ContainerV1beta1::CreateClusterRequest) + .to receive(:new).with(cluster_options).and_return(create_cluster_request_body) + + expect(subject).to eq operation + end + end + + context 'create with enable_addons for cloud_run' do + let(:enable_addons) { [:http_load_balancing, :istio_config, :cloud_run_config] } + + it 'sets corresponded parameters' do + expect_any_instance_of(Google::Apis::ContainerV1beta1::ContainerService) .to receive(:create_cluster).with(project_id, zone, create_cluster_request_body, options: user_agent_options) - expect(Google::Apis::ContainerV1::CreateClusterRequest) - .to receive(:new).with( - { - "cluster": { - "name": cluster_name, - "initial_node_count": cluster_size, - "node_config": { - "machine_type": machine_type - }, - "master_auth": { - "username": "admin", - "client_certificate_config": { - issue_client_certificate: true - } - }, - "legacy_abac": { - "enabled": false - } - } - } ).and_return(create_cluster_request_body) + expect(Google::Apis::ContainerV1beta1::CreateClusterRequest) + .to receive(:new).with(cluster_options).and_return(create_cluster_request_body) expect(subject).to eq operation end diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb index 3825994b733f33c0a721e11fb3ec3b8be382bb04..16247026e342475f7950428c456e5d79122019d0 100644 --- a/spec/models/clusters/applications/knative_spec.rb +++ b/spec/models/clusters/applications/knative_spec.rb @@ -16,6 +16,13 @@ allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async) end + describe 'when cloud run is enabled' do + let(:cluster) { create(:cluster, :provided_by_gcp, :cloud_run_enabled) } + let(:knative_cloud_run) { create(:clusters_applications_knative, cluster: cluster) } + + it { expect(knative_cloud_run).to be_not_installable } + end + describe 'when rbac is not enabled' do let(:cluster) { create(:cluster, :provided_by_gcp, :rbac_disabled) } let(:knative_no_rbac) { create(:clusters_applications_knative, cluster: cluster) } diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index 9afbe6328cafb3060ce4e1bd4979a41b81aeb629..c09814ebc95ca0fdc36deaaa7d711aa767d8e916 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -747,4 +747,26 @@ end end end + + describe '#knative_pre_installed?' do + subject { cluster.knative_pre_installed? } + + context 'with a GCP provider without cloud_run' do + let(:cluster) { create(:cluster, :provided_by_gcp) } + + it { is_expected.to be_falsey } + end + + context 'with a GCP provider with cloud_run' do + let(:cluster) { create(:cluster, :provided_by_gcp, :cloud_run_enabled) } + + it { is_expected.to be_truthy } + end + + context 'with a user provider' do + let(:cluster) { create(:cluster, :provided_by_user) } + + it { is_expected.to be_falsey } + end + end end diff --git a/spec/models/clusters/providers/gcp_spec.rb b/spec/models/clusters/providers/gcp_spec.rb index 785db4febe083df3c106fae42800c4c6bc9cc0c8..7ac1bbfafd85930d1cd848f4ffc36f57adc509c7 100644 --- a/spec/models/clusters/providers/gcp_spec.rb +++ b/spec/models/clusters/providers/gcp_spec.rb @@ -166,6 +166,22 @@ end end + describe '#knative_pre_installed?' do + subject { gcp.knative_pre_installed? } + + context 'when cluster is cloud_run' do + let(:gcp) { create(:cluster_provider_gcp) } + + it { is_expected.to be_falsey } + end + + context 'when cluster is not cloud_run' do + let(:gcp) { create(:cluster_provider_gcp, :cloud_run_enabled) } + + it { is_expected.to be_truthy } + end + end + describe '#api_client' do subject { gcp.api_client } diff --git a/spec/services/clusters/gcp/finalize_creation_service_spec.rb b/spec/services/clusters/gcp/finalize_creation_service_spec.rb index 5f91acb8e844f8d76a5d4a4efd1e06e63f6163dc..43dbea959a2ef17d8f403b8e5c992cd8dfc39456 100644 --- a/spec/services/clusters/gcp/finalize_creation_service_spec.rb +++ b/spec/services/clusters/gcp/finalize_creation_service_spec.rb @@ -107,6 +107,9 @@ namespace: 'default' } ) + + stub_kubeclient_get_cluster_role_binding_error(api_url, 'gitlab-admin') + stub_kubeclient_create_cluster_role_binding(api_url) end end @@ -133,9 +136,6 @@ context 'With an RBAC cluster' do before do provider.legacy_abac = false - - stub_kubeclient_get_cluster_role_binding_error(api_url, 'gitlab-admin') - stub_kubeclient_create_cluster_role_binding(api_url) end include_context 'kubernetes information successfully fetched' @@ -152,4 +152,22 @@ it_behaves_like 'kubernetes information not successfully fetched' end + + context 'With a Cloud Run cluster' do + before do + provider.cloud_run = true + end + + include_context 'kubernetes information successfully fetched' + + it_behaves_like 'success' + + it 'has knative pre-installed' do + subject + cluster.reload + + expect(cluster.application_knative).to be_present + expect(cluster.application_knative).to be_pre_installed + end + end end diff --git a/spec/support/google_api/cloud_platform_helpers.rb b/spec/support/google_api/cloud_platform_helpers.rb index a1328ef0d13a3859619f005d28af68f891e786a9..38ffca8c5ae53f4bdc97555cad1198271e7dfd06 100644 --- a/spec/support/google_api/cloud_platform_helpers.rb +++ b/spec/support/google_api/cloud_platform_helpers.rb @@ -65,7 +65,7 @@ def cloud_platform_get_zone_cluster_url(project_id, zone, cluster_id) end def cloud_platform_create_cluster_url(project_id, zone) - "https://container.googleapis.com/v1/projects/#{project_id}/zones/#{zone}/clusters" + "https://container.googleapis.com/v1beta1/projects/#{project_id}/zones/#{zone}/clusters" end def cloud_platform_get_zone_operation_url(project_id, zone, operation_id) diff --git a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb index 5341aacb445b200541272f4ac325cf4e39057523..6f06d323a82b4ad613ec5c41af328812129e0eda 100644 --- a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb +++ b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb @@ -11,6 +11,20 @@ end end + describe '#status_states' do + let(:cluster) { create(:cluster, :provided_by_gcp) } + + subject { described_class.new(cluster: cluster) } + + it 'returns a hash of state values' do + expect(subject.status_states).to include(:installed) + end + + it 'returns an integer for installed state value' do + expect(subject.status_states[:installed]).to eq(3) + end + end + describe '.available' do subject { described_class.available }