diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2019-09-23 00:06:29 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2019-09-23 00:06:29 +0000 |
commit | 98dbb0a488d7b0093f352938210d9578b0f7a8a6 (patch) | |
tree | 25654204f8de2672556a696199fa209b8f8ff1b3 | |
parent | 9ce26d3dfdf4194f32c470cd3102b4376a53ef2f (diff) | |
download | gitlab-ce-98dbb0a488d7b0093f352938210d9578b0f7a8a6.tar.gz |
Add latest changes from gitlab-org/gitlab@master
43 files changed, 621 insertions, 82 deletions
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index d386960f3b6..7ea8901ecbb 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 64364092016..c6c8dc6352c 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 27959898fb7..4d3e759d8d4 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 8fd752092c9..c6e4b7951cf 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 6e632519d8a..6bc4be7b93a 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 5cddb4cc098..6464461ea0c 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 15f1e8284ff..993aba661f3 100644 --- a/app/controllers/clusters/clusters_controller.rb +++ b/app/controllers/clusters/clusters_controller.rb @@ -170,6 +170,7 @@ class Clusters::ClustersController < Clusters::BaseController :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 455cf200fbc..261f6ce8987 100644 --- a/app/models/clusters/applications/helm.rb +++ b/app/models/clusters/applications/helm.rb @@ -27,7 +27,7 @@ module Clusters 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 ec65482a846..ca93bc15be0 100644 --- a/app/models/clusters/applications/jupyter.rb +++ b/app/models/clusters/applications/jupyter.rb @@ -23,7 +23,7 @@ module Clusters 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 a9b9374622d..f2a3695d2eb 100644 --- a/app/models/clusters/applications/knative.rb +++ b/app/models/clusters/applications/knative.rb @@ -21,7 +21,7 @@ module Clusters 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 @@ module Clusters { "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 231cadfae05..2df30e8ac36 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -194,6 +194,10 @@ module Clusters 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 d1b57a21a7d..e748c0a855d 100644 --- a/app/models/clusters/concerns/application_core.rb +++ b/app/models/clusters/concerns/application_core.rb @@ -15,7 +15,7 @@ module Clusters 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 342d766f723..b63a596dfee 100644 --- a/app/models/clusters/concerns/application_status.rb +++ b/app/models/clusters/concerns/application_status.rb @@ -28,6 +28,13 @@ module Clusters 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 Clusters 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 Clusters 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 390748bf252..043765f79ac 100644 --- a/app/models/clusters/providers/gcp.rb +++ b/app/models/clusters/providers/gcp.rb @@ -10,6 +10,9 @@ module Clusters 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 @@ module Clusters @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/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb index 10eb1141f59..37a74cd1b00 100644 --- a/app/services/boards/issues/list_service.rb +++ b/app/services/boards/issues/list_service.rb @@ -11,9 +11,11 @@ module Boards # rubocop: disable CodeReuse/ActiveRecord def metadata + issues = Issue.arel_table keys = metadata_fields.keys - columns = metadata_fields.values_at(*keys).join(', ') - results = Issue.where(id: fetch_issues.select('issues.id')).pluck(columns) + # TODO: eliminate need for SQL literal fragment + columns = Arel.sql(metadata_fields.values_at(*keys).join(', ')) + results = Issue.where(id: fetch_issues.select(issues[:id])).pluck(columns) Hash[keys.zip(results.flatten)] end diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb index c5cde831964..0aff1bcc8b9 100644 --- a/app/services/clusters/gcp/finalize_creation_service.rb +++ b/app/services/clusters/gcp/finalize_creation_service.rb @@ -11,6 +11,7 @@ module Clusters 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 @@ module Clusters 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 80040511ec2..7dc2d3c32f1 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 @@ module Clusters 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 4d3e3359ea0..196ad422766 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 cccba48624b..8b9844bcfc1 100644 --- a/app/views/clusters/clusters/show.html.haml +++ b/app/views/clusters/clusters/show.html.haml @@ -23,12 +23,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 00000000000..77b365f17db --- /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 00000000000..611726a20c7 --- /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/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index f37cd518d48..6b0741667bd 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -117,3 +117,4 @@ - [jira_connect, 1] - [update_external_pull_requests, 3] - [refresh_license_compliance_checks, 2] + - [design_management_new_version, 1] 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 00000000000..e7ffd7cd4d3 --- /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 00000000000..8e0bde97cc1 --- /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 b60593132f5..392db66f5b6 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 @@ ActiveRecord::Schema.define(version: 2019_09_18_104222) do 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 97fa973d3e3..49878978154 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/gitlab/utils/inline_hash.rb b/lib/gitlab/utils/inline_hash.rb new file mode 100644 index 00000000000..5985e4da845 --- /dev/null +++ b/lib/gitlab/utils/inline_hash.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module Gitlab + module Utils + module InlineHash + extend self + + # Transforms a Hash into an inline Hash by merging its nested keys. + # + # Input + # + # { + # 'root_param' => 'Root', + # nested_param: { + # key: 'Value' + # }, + # 'very' => { + # 'deep' => { + # 'nested' => { + # 'param' => 'Deep nested value' + # } + # } + # } + # } + # + # + # Result + # + # { + # 'root_param' => 'Root', + # 'nested_param.key' => 'Value', + # 'very.deep.nested.param' => 'Deep nested value' + # } + # + def merge_keys(hash, prefix: nil, connector: '.') + result = {} + base_prefix = prefix ? "#{prefix}#{connector}" : '' + pairs = hash.map { |key, value| ["#{base_prefix}#{key}", value] } + + until pairs.empty? + key, value = pairs.shift + + if value.is_a?(Hash) + value.each { |k, v| pairs.unshift ["#{key}#{connector}#{k}", v] } + else + result[key] = value + end + end + + result + end + end + end +end diff --git a/lib/gitlab/utils/safe_inline_hash.rb b/lib/gitlab/utils/safe_inline_hash.rb new file mode 100644 index 00000000000..644d87c6876 --- /dev/null +++ b/lib/gitlab/utils/safe_inline_hash.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Gitlab + module Utils + class SafeInlineHash + # Validates the hash size using `Gitlab::Utils::DeepSize` before merging keys using `Gitlab::Utils::InlineHash` + def initialize(hash, prefix: nil, connector: '.') + @hash = hash + end + + def self.merge_keys!(hash, prefix: nil, connector: '.') + new(hash).merge_keys!(prefix: prefix, connector: connector) + end + + def merge_keys!(prefix:, connector:) + raise ArgumentError, 'The Hash is too big' unless valid? + + Gitlab::Utils::InlineHash.merge_keys(hash, prefix: prefix, connector: connector) + end + + private + + attr_reader :hash + + def valid? + Gitlab::Utils::DeepSize.new(hash).valid? + end + end + end +end diff --git a/lib/google_api/cloud_platform/client.rb b/lib/google_api/cloud_platform/client.rb index 9f01a3f97ce..eaf94708282 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 @@ module GoogleApi 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 @@ module GoogleApi 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 089a6dd5b1d..c9e40aca796 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 d294e6d055e..29aea5e403e 100644 --- a/spec/factories/clusters/clusters.rb +++ b/spec/factories/clusters/clusters.rb @@ -58,6 +58,10 @@ FactoryBot.define do 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 22462651b6a..7fdcdebad34 100644 --- a/spec/factories/clusters/providers/gcp.rb +++ b/spec/factories/clusters/providers/gcp.rb @@ -34,5 +34,9 @@ FactoryBot.define do 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 ee3b7d8aa90..5ee06eb44c9 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 00000000000..44a1bc0836c --- /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/gitlab/utils/inline_hash_spec.rb b/spec/lib/gitlab/utils/inline_hash_spec.rb new file mode 100644 index 00000000000..806117eddd0 --- /dev/null +++ b/spec/lib/gitlab/utils/inline_hash_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +describe Gitlab::Utils::InlineHash do + describe '.merge_keys' do + subject { described_class.merge_keys(source) } + + let(:source) do + { + nested_param: { + key: 'Value' + }, + 'root_param' => 'Root', + 'very' => { + 'deep' => { + 'nested' => { + 'param' => 'Deep nested value' + } + } + } + } + end + + it 'transforms a nested hash into a one-level hash' do + is_expected.to eq( + 'nested_param.key' => 'Value', + 'root_param' => 'Root', + 'very.deep.nested.param' => 'Deep nested value' + ) + end + + it 'retains key insertion order' do + expect(subject.keys) + .to eq(%w(nested_param.key root_param very.deep.nested.param)) + end + + context 'with a custom connector' do + subject { described_class.merge_keys(source, connector: '::') } + + it 'uses the connector to merge keys' do + is_expected.to eq( + 'nested_param::key' => 'Value', + 'root_param' => 'Root', + 'very::deep::nested::param' => 'Deep nested value' + ) + end + end + + context 'with a starter prefix' do + subject { described_class.merge_keys(source, prefix: 'options') } + + it 'prefixes all the keys' do + is_expected.to eq( + 'options.nested_param.key' => 'Value', + 'options.root_param' => 'Root', + 'options.very.deep.nested.param' => 'Deep nested value' + ) + end + end + end +end diff --git a/spec/lib/gitlab/utils/safe_inline_hash_spec.rb b/spec/lib/gitlab/utils/safe_inline_hash_spec.rb new file mode 100644 index 00000000000..8d8163f3baa --- /dev/null +++ b/spec/lib/gitlab/utils/safe_inline_hash_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +describe Gitlab::Utils::SafeInlineHash do + context '.merge_keys!' do + let(:source) { { 'foo' => { 'bar' => 'baz' } } } + let(:validator) { instance_double(Gitlab::Utils::DeepSize, valid?: valid) } + + subject { described_class.merge_keys!(source, prefix: 'safe', connector: '::') } + + before do + allow(Gitlab::Utils::DeepSize) + .to receive(:new) + .with(source) + .and_return(validator) + end + + context 'when hash is too big' do + let(:valid) { false } + + it 'raises an exception' do + expect { subject }.to raise_error ArgumentError, 'The Hash is too big' + end + end + + context 'when hash has an acceptaable size' do + let(:valid) { true } + + it 'returns a result of InlineHash' do + is_expected.to eq('safe::foo::bar' => 'baz') + end + end + 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 c24998d32f8..2253feb376d 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 GoogleApi::CloudPlatform::Client do 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 @@ describe GoogleApi::CloudPlatform::Client do 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 @@ describe GoogleApi::CloudPlatform::Client do 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 3825994b733..16247026e34 100644 --- a/spec/models/clusters/applications/knative_spec.rb +++ b/spec/models/clusters/applications/knative_spec.rb @@ -16,6 +16,13 @@ describe Clusters::Applications::Knative do 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 b8a6fd6f914..df6263eeed2 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -756,4 +756,26 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do 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 785db4febe0..7ac1bbfafd8 100644 --- a/spec/models/clusters/providers/gcp_spec.rb +++ b/spec/models/clusters/providers/gcp_spec.rb @@ -166,6 +166,22 @@ describe Clusters::Providers::Gcp do 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 5f91acb8e84..43dbea959a2 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 @@ describe Clusters::Gcp::FinalizeCreationService, '#execute' do 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 @@ describe Clusters::Gcp::FinalizeCreationService, '#execute' do 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 @@ describe Clusters::Gcp::FinalizeCreationService, '#execute' do 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 a1328ef0d13..38ffca8c5ae 100644 --- a/spec/support/google_api/cloud_platform_helpers.rb +++ b/spec/support/google_api/cloud_platform_helpers.rb @@ -65,7 +65,7 @@ module GoogleApi 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 5341aacb445..6f06d323a82 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 @@ shared_examples 'cluster application status specs' do |application_name| 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 } |