diff options
74 files changed, 925 insertions, 483 deletions
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 63b1685feda..698570efb07 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -799,7 +799,6 @@ Style/SelfAssignment: Exclude: - 'app/models/concerns/bulk_member_access_load.rb' - 'app/serializers/base_serializer.rb' - - 'spec/features/projects/clusters/interchangeability_spec.rb' - 'spec/support/import_export/configuration_helper.rb' # Offense count: 50 diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index 0a9c56f5625..3b62121eb0d 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -340,6 +340,11 @@ .deprecated-service { cursor: default; + + a { + font-weight: $gl-font-weight-bold; + color: $white-light; + } } .personal-access-tokens-never-expires-label { diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb index 80ee7c35906..ec8077d18e3 100644 --- a/app/controllers/clusters/clusters_controller.rb +++ b/app/controllers/clusters/clusters_controller.rb @@ -128,6 +128,7 @@ class Clusters::ClustersController < Clusters::BaseController :enabled, :name, :environment_scope, + :managed, :base_domain, platform_kubernetes_attributes: [ :api_url, @@ -140,6 +141,7 @@ class Clusters::ClustersController < Clusters::BaseController params.require(:cluster).permit( :enabled, :environment_scope, + :managed, :base_domain, platform_kubernetes_attributes: [ :namespace diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb index d4b50b7ecfb..01ccf163b45 100644 --- a/app/helpers/services_helper.rb +++ b/app/helpers/services_helper.rb @@ -39,7 +39,7 @@ module ServicesHelper end def disable_fields_service?(service) - !current_controller?("admin/services") && service.deprecated? + service.is_a?(KubernetesService) || (!current_controller?("admin/services") && service.deprecated?) end extend self diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index ccc877fb924..8c044c86c47 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -193,21 +193,40 @@ module Clusters platform_kubernetes.kubeclient if kubernetes? end + ## + # This is subtly different to #find_or_initialize_kubernetes_namespace_for_project + # below because it will ignore any namespaces that have not got a service account + # token. This provides a guarantee that any namespace selected here can be used + # for cluster operations - a namespace needs to have a service account configured + # before it it can be used. + # + # This is used for selecting a namespace to use when querying a cluster, or + # generating variables to pass to CI. def kubernetes_namespace_for(project) - find_or_initialize_kubernetes_namespace_for_project(project).namespace + find_or_initialize_kubernetes_namespace_for_project( + project, scope: kubernetes_namespaces.has_service_account_token + ).namespace end - def find_or_initialize_kubernetes_namespace_for_project(project) + ## + # This is subtly different to #kubernetes_namespace_for because it will include + # namespaces that have yet to receive a service account token. This allows + # the namespace configuration process to be repeatable - if a namespace has + # already been created without a token we don't need to create another + # record entirely, just set the token on the pre-existing namespace. + # + # This is used for configuring cluster namespaces. + def find_or_initialize_kubernetes_namespace_for_project(project, scope: kubernetes_namespaces) attributes = { project: project } attributes[:cluster_project] = cluster_project if project_type? - kubernetes_namespaces.find_or_initialize_by(attributes).tap do |namespace| + scope.find_or_initialize_by(attributes).tap do |namespace| namespace.set_defaults end end def allow_user_defined_namespace? - project_type? + project_type? || !managed? end def kube_ingress_domain diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index 8e06156c73d..272861cacf0 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -80,9 +80,18 @@ module Clusters .append(key: 'KUBE_CA_PEM_FILE', value: ca_pem, file: true) end - if kubernetes_namespace = cluster.kubernetes_namespaces.has_service_account_token.find_by(project: project) + if !cluster.managed? + project_namespace = namespace.presence || "#{project.path}-#{project.id}".downcase + + variables + .append(key: 'KUBE_URL', value: api_url) + .append(key: 'KUBE_TOKEN', value: token, public: false, masked: true) + .append(key: 'KUBE_NAMESPACE', value: project_namespace) + .append(key: 'KUBECONFIG', value: kubeconfig(project_namespace), public: false, file: true) + + elsif kubernetes_namespace = cluster.kubernetes_namespaces.has_service_account_token.find_by(project: project) variables.concat(kubernetes_namespace.predefined_variables) - elsif cluster.project_type? || !cluster.managed? + elsif cluster.project_type? # As of 11.11 a user can create a cluster that they manage themselves, # which replicates the existing project-level cluster behaviour. # Once we have marked all project-level clusters that make use of this diff --git a/app/models/project_services/deployment_service.rb b/app/models/project_services/deployment_service.rb deleted file mode 100644 index 80aa2101509..00000000000 --- a/app/models/project_services/deployment_service.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -# Base class for deployment services -# -# These services integrate with a deployment solution like Kubernetes/OpenShift, -# Mesosphere, etc, to provide additional features to environments. -class DeploymentService < Service - default_value_for :category, 'deployment' - - def self.supported_events - %w() - end - - def predefined_variables(project:) - [] - end - - # Environments may have a number of terminals. Should return an array of - # hashes describing them, e.g.: - # - # [{ - # :selectors => {"a" => "b", "foo" => "bar"}, - # :url => "wss://external.example.com/exec", - # :headers => {"Authorization" => "Token xxx"}, - # :subprotocols => ["foo"], - # :ca_pem => "----BEGIN CERTIFICATE...", # optional - # :created_at => Time.now.utc - # }] - # - # Selectors should be a set of values that uniquely identify a particular - # terminal - def terminals(environment) - raise NotImplementedError - end - - def can_test? - false - end -end diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index aa6b4aa1d5e..edf7e886e77 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -5,10 +5,12 @@ # We'll move this class to Clusters::Platforms::Kubernetes, which contains exactly the same logic. # After we've migrated data, we'll remove KubernetesService. This would happen in a few months. # If you're modyfiyng this class, please note that you should update the same change in Clusters::Platforms::Kubernetes. -class KubernetesService < DeploymentService +class KubernetesService < Service include Gitlab::Kubernetes include ReactiveCaching + default_value_for :category, 'deployment' + self.reactive_cache_key = ->(service) { [service.class.model_name.singular, service.project_id] } # Namespace defaults to the project path, but can be overridden in case that @@ -32,7 +34,10 @@ class KubernetesService < DeploymentService before_validation :enforce_namespace_to_lower_case - validate :deprecation_validation, unless: :template? + attr_accessor :skip_deprecation_validation + + validate :deprecation_validation, unless: :skip_deprecation_validation + validates :namespace, allow_blank: true, length: 1..63, @@ -44,6 +49,14 @@ class KubernetesService < DeploymentService after_save :clear_reactive_cache! + def self.supported_events + %w() + end + + def can_test? + false + end + def initialize_properties self.properties = {} if properties.nil? end @@ -56,11 +69,6 @@ class KubernetesService < DeploymentService 'Kubernetes / OpenShift integration' end - def help - 'To enable terminal access to Kubernetes environments, label your ' \ - 'deployments with `app=$CI_ENVIRONMENT_SLUG`' - end - def self.to_param 'kubernetes' end @@ -153,14 +161,25 @@ class KubernetesService < DeploymentService end def deprecated? - !active + true + end + + def editable? + false end def deprecation_message - content = _("Kubernetes service integration has been deprecated. %{deprecated_message_content} your Kubernetes clusters using the new <a href=\"%{url}\"/>Kubernetes Clusters</a> page") % { - deprecated_message_content: deprecated_message_content, - url: Gitlab::Routing.url_helpers.project_clusters_path(project) - } + content = if project + _("Kubernetes service integration has been deprecated. %{deprecated_message_content} your Kubernetes clusters using the new <a href=\"%{url}\"/>Kubernetes Clusters</a> page") % { + deprecated_message_content: deprecated_message_content, + url: Gitlab::Routing.url_helpers.project_clusters_path(project) + } + else + _("The instance-level Kubernetes service integration is deprecated. Your data has been migrated to an <a href=\"%{url}\"/>instance-level cluster</a>.") % { + url: Gitlab::Routing.url_helpers.admin_clusters_path + } + end + content.html_safe end @@ -243,10 +262,6 @@ class KubernetesService < DeploymentService end def deprecated_message_content - if active? - _("Your Kubernetes cluster information on this page is still editable, but you are advised to disable and reconfigure") - else - _("Fields on this page are now uneditable, you can configure") - end + _("Fields on this page are now uneditable, you can configure") end end diff --git a/app/models/project_services/mock_deployment_service.rb b/app/models/project_services/mock_deployment_service.rb index 7ab1687f8ba..1103cb11e73 100644 --- a/app/models/project_services/mock_deployment_service.rb +++ b/app/models/project_services/mock_deployment_service.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true -class MockDeploymentService < DeploymentService +class MockDeploymentService < Service + default_value_for :category, 'deployment' + def title 'Mock deployment' end @@ -17,4 +19,16 @@ class MockDeploymentService < DeploymentService def terminals(environment) [] end + + def self.supported_events + %w() + end + + def predefined_variables(project:) + [] + end + + def can_test? + false + end end diff --git a/app/models/service.rb b/app/models/service.rb index 16fbe6648cc..40033003f3b 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -120,7 +120,7 @@ class Service < ApplicationRecord end def self.event_names - self.supported_events.map { |event| "#{event}_events" } + self.supported_events.map { |event| ServicesHelper.service_event_field_name(event) } end def event_field(event) @@ -152,7 +152,7 @@ class Service < ApplicationRecord end def self.supported_events - %w(push tag_push issue confidential_issue merge_request wiki_page) + %w(commit push tag_push issue confidential_issue merge_request wiki_page) end def execute(data) diff --git a/app/views/admin/services/_deprecated_message.html.haml b/app/views/admin/services/_deprecated_message.html.haml new file mode 100644 index 00000000000..fea9506a4bb --- /dev/null +++ b/app/views/admin/services/_deprecated_message.html.haml @@ -0,0 +1,3 @@ +.flash-container.flash-container-page + .flash-alert.deprecated-service + %span= @service.deprecation_message diff --git a/app/views/admin/services/_form.html.haml b/app/views/admin/services/_form.html.haml index 1798b44bbb7..97373a3c350 100644 --- a/app/views/admin/services/_form.html.haml +++ b/app/views/admin/services/_form.html.haml @@ -6,5 +6,6 @@ = form_for :service, url: admin_application_settings_service_path, method: :put, html: { class: 'fieldset-form' } do |form| = render 'shared/service_settings', form: form, subject: @service - .footer-block.row-content-block - = form.submit 'Save', class: 'btn btn-success' + - unless @service.is_a?(KubernetesService) + .footer-block.row-content-block + = form.submit 'Save', class: 'btn btn-success' diff --git a/app/views/admin/services/edit.html.haml b/app/views/admin/services/edit.html.haml index 512176649e6..79f5ab0d77d 100644 --- a/app/views/admin/services/edit.html.haml +++ b/app/views/admin/services/edit.html.haml @@ -1,4 +1,7 @@ - add_to_breadcrumbs "Service Templates", admin_application_settings_services_path - breadcrumb_title @service.title - page_title @service.title, "Service Templates" + += render 'deprecated_message' if @service.deprecation_message + = render 'form' diff --git a/app/views/clusters/platforms/kubernetes/_form.html.haml b/app/views/clusters/platforms/kubernetes/_form.html.haml index c1727cf9079..f2e44462226 100644 --- a/app/views/clusters/platforms/kubernetes/_form.html.haml +++ b/app/views/clusters/platforms/kubernetes/_form.html.haml @@ -48,7 +48,7 @@ = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') .form-group - = field.check_box :managed, { disabled: true, label: s_('ClusterIntegration|GitLab-managed cluster'), + = field.check_box :managed, { label: s_('ClusterIntegration|GitLab-managed cluster'), label_class: 'label-bold' } .form-text.text-muted = s_('ClusterIntegration|Allow GitLab to manage namespace and service accounts for this cluster.') diff --git a/app/views/projects/merge_requests/_mr_box.html.haml b/app/views/projects/merge_requests/_mr_box.html.haml index 7f2c9dcacfd..4f09f47d795 100644 --- a/app/views/projects/merge_requests/_mr_box.html.haml +++ b/app/views/projects/merge_requests/_mr_box.html.haml @@ -1,10 +1,10 @@ .detail-page-description - %h2.title + %h2.title.qa-title = markdown_field(@merge_request, :title) %div - if @merge_request.description.present? - .description{ class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : '' } + .description.qa-description{ class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : '' } .md = markdown_field(@merge_request, :description) %textarea.hidden.js-task-list-field diff --git a/app/views/shared/projects/_search_form.html.haml b/app/views/shared/projects/_search_form.html.haml index 7c7c0a363ac..4365e3f6877 100644 --- a/app/views/shared/projects/_search_form.html.haml +++ b/app/views/shared/projects/_search_form.html.haml @@ -1,7 +1,7 @@ - form_field_classes = local_assigns[:admin_view] || !Feature.enabled?(:project_list_filter_bar) ? 'input-short js-projects-list-filter' : '' - placeholder = local_assigns[:search_form_placeholder] ? search_form_placeholder : 'Filter by name...' -= form_tag filter_projects_path, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| += form_tag filter_projects_path, method: :get, class: 'project-filter-form qa-project-filter-form', id: 'project-filter-form' do |f| = search_field_tag :name, params[:name], placeholder: placeholder, class: "project-filter-form-field form-control #{form_field_classes}", diff --git a/changelogs/unreleased/55362-refresh-blank-service-account-token.yml b/changelogs/unreleased/55362-refresh-blank-service-account-token.yml new file mode 100644 index 00000000000..3189de97e8b --- /dev/null +++ b/changelogs/unreleased/55362-refresh-blank-service-account-token.yml @@ -0,0 +1,5 @@ +--- +title: Refresh service_account_token for kubernetes_namespaces +merge_request: 29657 +author: +type: fixed diff --git a/changelogs/unreleased/56737-commits-and-mr-events-on-jira-api.yml b/changelogs/unreleased/56737-commits-and-mr-events-on-jira-api.yml new file mode 100644 index 00000000000..ee2ce8acaeb --- /dev/null +++ b/changelogs/unreleased/56737-commits-and-mr-events-on-jira-api.yml @@ -0,0 +1,5 @@ +--- +title: Expose all current events properly on services API +merge_request: 29736 +author: Zsolt Kovari +type: fixed diff --git a/changelogs/unreleased/60617-allow-switching-from-gitlab-managed-to-unmanaged-clusters.yml b/changelogs/unreleased/60617-allow-switching-from-gitlab-managed-to-unmanaged-clusters.yml new file mode 100644 index 00000000000..1127dde4fcf --- /dev/null +++ b/changelogs/unreleased/60617-allow-switching-from-gitlab-managed-to-unmanaged-clusters.yml @@ -0,0 +1,5 @@ +--- +title: Allow switching clusters between managed and unmanaged +merge_request: 29322 +author: +type: added diff --git a/changelogs/unreleased/62685-add-index-invite-email-to-members.yml b/changelogs/unreleased/62685-add-index-invite-email-to-members.yml new file mode 100644 index 00000000000..80bb05e1709 --- /dev/null +++ b/changelogs/unreleased/62685-add-index-invite-email-to-members.yml @@ -0,0 +1,5 @@ +--- +title: Add index on invite_email for members +merge_request: 29768 +author: +type: performance diff --git a/changelogs/unreleased/63079-exclude-k8s-namespaces-with-no-service-account-token.yml b/changelogs/unreleased/63079-exclude-k8s-namespaces-with-no-service-account-token.yml new file mode 100644 index 00000000000..9dc99c8a62f --- /dev/null +++ b/changelogs/unreleased/63079-exclude-k8s-namespaces-with-no-service-account-token.yml @@ -0,0 +1,6 @@ +--- +title: Ensure a Kubernetes namespace is not used for deployments if there is no service + account token associated with it +merge_request: 29643 +author: +type: fixed diff --git a/changelogs/unreleased/migrate_k8s_service_integration.yml b/changelogs/unreleased/migrate_k8s_service_integration.yml new file mode 100644 index 00000000000..57f03e6bdab --- /dev/null +++ b/changelogs/unreleased/migrate_k8s_service_integration.yml @@ -0,0 +1,5 @@ +--- +title: Migrate Kubernetes service integration templates to clusters +merge_request: 28534 +author: +type: added diff --git a/changelogs/unreleased/readonly_k8s_integration.yml b/changelogs/unreleased/readonly_k8s_integration.yml new file mode 100644 index 00000000000..718705e8750 --- /dev/null +++ b/changelogs/unreleased/readonly_k8s_integration.yml @@ -0,0 +1,5 @@ +--- +title: Make Kubernetes service templates readonly +merge_request: 29044 +author: +type: removed diff --git a/db/migrate/20190610142825_add_index_to_members_invite_email.rb b/db/migrate/20190610142825_add_index_to_members_invite_email.rb new file mode 100644 index 00000000000..58157cc5313 --- /dev/null +++ b/db/migrate/20190610142825_add_index_to_members_invite_email.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddIndexToMembersInviteEmail < ActiveRecord::Migration[5.1] + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index :members, [:invite_email] + end + + def down + remove_concurrent_index :members, [:invite_email] + end +end diff --git a/db/post_migrate/20190517153211_migrate_k8s_service_integration.rb b/db/post_migrate/20190517153211_migrate_k8s_service_integration.rb new file mode 100644 index 00000000000..4bd04edb239 --- /dev/null +++ b/db/post_migrate/20190517153211_migrate_k8s_service_integration.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +class MigrateK8sServiceIntegration < ActiveRecord::Migration[5.1] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + class Cluster < ActiveRecord::Base + self.table_name = 'clusters' + + has_one :platform_kubernetes, class_name: 'MigrateK8sServiceIntegration::PlatformsKubernetes' + + accepts_nested_attributes_for :platform_kubernetes + + enum cluster_type: { + instance_type: 1, + group_type: 2, + project_type: 3 + } + + enum platform_type: { + kubernetes: 1 + } + + enum provider_type: { + user: 0, + gcp: 1 + } + end + + class PlatformsKubernetes < ActiveRecord::Base + self.table_name = 'cluster_platforms_kubernetes' + + belongs_to :cluster, class_name: 'MigrateK8sServiceIntegration::Cluster' + + attr_encrypted :token, + mode: :per_attribute_iv, + key: Settings.attr_encrypted_db_key_base_truncated, + algorithm: 'aes-256-cbc' + end + + class Service < ActiveRecord::Base + include EachBatch + + self.table_name = 'services' + self.inheritance_column = :_type_disabled # Disable STI, otherwise KubernetesModel will be looked up + + belongs_to :project, class_name: 'MigrateK8sServiceIntegration::Project', foreign_key: :project_id + + scope :kubernetes_service_templates, -> do + where(category: 'deployment', type: 'KubernetesService', template: true) + end + + def api_url + parsed_properties['api_url'].presence + end + + def ca_pem + parsed_properties['ca_pem'] + end + + def namespace + parsed_properties['namespace'].presence + end + + def token + parsed_properties['token'].presence + end + + private + + def parsed_properties + @parsed_properties ||= JSON.parse(self.properties) + end + end + + def up + has_instance_cluster = Cluster.instance_type.where(enabled: true).exists? + + MigrateK8sServiceIntegration::Service.kubernetes_service_templates.find_each do |service| + next unless service.api_url && service.token + + MigrateK8sServiceIntegration::Cluster.create!( + enabled: !has_instance_cluster && service.active, + managed: false, + name: 'KubernetesService', + cluster_type: 'instance_type', + provider_type: 'user', + platform_type: 'kubernetes', + platform_kubernetes_attributes: { + api_url: service.api_url, + ca_cert: service.ca_pem, + namespace: service.namespace, + token: service.token + } + ) + end + end + + def down + # It is not possible to tell which instance-level clusters were created by + # this migration. The original data is intentionally left intact. + end +end diff --git a/db/schema.rb b/db/schema.rb index d1c13f44ab2..b003fdd5a9b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1850,6 +1850,7 @@ ActiveRecord::Schema.define(version: 20190613030606) do t.boolean "ldap", default: false, null: false t.boolean "override", default: false, null: false t.index ["access_level"], name: "index_members_on_access_level", using: :btree + t.index ["invite_email"], name: "index_members_on_invite_email", using: :btree t.index ["invite_token"], name: "index_members_on_invite_token", unique: true, using: :btree t.index ["requested_at"], name: "index_members_on_requested_at", using: :btree t.index ["source_id", "source_type"], name: "index_members_on_source_id_and_source_type", using: :btree diff --git a/doc/api/graphql/index.md b/doc/api/graphql/index.md index 88e657a5d2f..6c1cce620ca 100644 --- a/doc/api/graphql/index.md +++ b/doc/api/graphql/index.md @@ -46,7 +46,7 @@ curl --data "value=100" --header "PRIVATE-TOKEN: <your_access_token>" https://gi A first iteration of a GraphQL API includes the following queries 1. `project` : Within a project it is also possible to fetch a `mergeRequest` by IID. -1. `group` : Only basic group information is currently supported. +1. `group` : Basic group information and epics **[ULTIMATE]** are currently supported. 1. `namespace` : Within a namespace it is also possible to fetch `projects`. ### Multiplex queries diff --git a/doc/api/services.md b/doc/api/services.md index 898cfad7254..f38f96f64ad 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -22,6 +22,7 @@ Parameters: | --------- | ---- | -------- | ----------- | | `api_key` | string | true | User API token. User must have access to task, all comments will be attributed to this user. | | `restrict_to_branch` | string | false | Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches. | +| `push_events` | boolean | false | Enable notifications for push events | ### Delete Asana service @@ -57,6 +58,7 @@ Parameters: | --------- | ---- | -------- | ----------- | | `token` | string | true | The authentication token | `subdomain` | string | false | The subdomain setting | +| `push_events` | boolean | false | Enable notifications for push events | ### Delete Assembla service @@ -96,6 +98,7 @@ Parameters: | `build_key` | string | true | Bamboo build plan key like KEY | | `username` | string | true | A user with API access, if applicable | | `password` | string | true | Password of the user | +| `push_events` | boolean | false | Enable notifications for push events | ### Delete Atlassian Bamboo CI service @@ -134,6 +137,7 @@ Parameters: | `project_url` | string | true | Project url | | `description` | string | false | Description | | `title` | string | false | Title | +| `push_events` | boolean | false | Enable notifications for push events | ### Delete Bugzilla Service @@ -170,6 +174,7 @@ Parameters: | `token` | string | true | Buildkite project GitLab token | | `project_url` | string | true | `https://buildkite.com/example/project` | | `enable_ssl_verification` | boolean | false | Enable SSL verification | +| `push_events` | boolean | false | Enable notifications for push events | ### Delete Buildkite service @@ -206,6 +211,7 @@ Parameters: | `token` | string | true | Campfire token | | `subdomain` | string | false | Campfire subdomain | | `room` | string | false | Campfire room | +| `push_events` | boolean | false | Enable notifications for push events | ### Delete Campfire service @@ -244,6 +250,7 @@ Parameters: | `project_url` | string | true | Project url | `description` | string | false | Description | `title` | string | false | Title +| `push_events` | boolean | false | Enable notifications for push events | ### Delete Custom Issue Tracker service @@ -280,6 +287,10 @@ Parameters: | `token` | string | true | Drone CI project specific token | | `drone_url` | string | true | `http://drone.example.com` | | `enable_ssl_verification` | boolean | false | Enable SSL verification | +| `push_events` | boolean | false | Enable notifications for push events | +| `merge_requests_events` | boolean | false | Enable notifications for merge request events | +| `tag_push_events` | boolean | false | Enable notifications for tag push events | + ### Delete Drone CI service @@ -316,6 +327,8 @@ Parameters: | `recipients` | string | true | Emails separated by whitespace | | `disable_diffs` | boolean | false | Disable code diffs | | `send_from_committer_email` | boolean | false | Send from committer | +| `push_events` | boolean | false | Enable notifications for push events | +| `tag_push_events` | boolean | false | Enable notifications for tag push events | ### Delete Emails on push service @@ -384,6 +397,7 @@ Parameters: | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `token` | string | true | Flowdock Git source token | +| `push_events` | boolean | false | Enable notifications for push events | ### Delete Flowdock service @@ -471,6 +485,14 @@ Parameters: | `room` | string | false |Room name or ID | | `api_version` | string | false | Leave blank for default (v2) | | `server` | string | false | Leave blank for default. For example, `https://hipchat.example.com`. | +| `push_events` | boolean | false | Enable notifications for push events | +| `issues_events` | boolean | false | Enable notifications for issue events | +| `confidential_issues_events` | boolean | false | Enable notifications for confidential issue events | +| `merge_requests_events` | boolean | false | Enable notifications for merge request events | +| `tag_push_events` | boolean | false | Enable notifications for tag push events | +| `note_events` | boolean | false | Enable notifications for note events | +| `confidental_note_events` | boolean | false | Enable notifications for confidential note events | +| `pipeline_events` | boolean | false | Enable notifications for pipeline events | ### Delete HipChat service @@ -511,6 +533,7 @@ Parameters: | `server_host` | string | false | localhost | | `server_port` | integer | false | 6659 | | `colorize_messages` | boolean | false | Colorize messages | +| `push_events` | boolean | false | Enable notifications for push events | ### Delete Irker (IRC gateway) service @@ -562,6 +585,8 @@ Parameters: | `password` | string | yes | The password of the user created to be used with GitLab/JIRA. | | `active` | boolean | no | Activates or deactivates the service. Defaults to false (deactivated). | | `jira_issue_transition_id` | string | no | The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. | +| `commit_events` | boolean | false | Enable notifications for commit events | +| `merge_requests_events` | boolean | false | Enable notifications for merge request events | ### Delete JIRA service @@ -715,9 +740,14 @@ PUT /projects/:id/services/packagist Parameters: -- `username` (**required**) -- `token` (**required**) -- `server` (optional) +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `username` | string | yes | The username of a Packagist account | +| `token` | string | yes | API token to the Packagist server | +| `server` | boolean | no | URL of the Packagist server. Leave blank for default: https://packagist.org | +| `push_events` | boolean | false | Enable notifications for push events | +| `merge_requests_events` | boolean | false | Enable notifications for merge request events | +| `tag_push_events` | boolean | false | Enable notifications for tag push events | ### Delete Packagist service @@ -755,6 +785,7 @@ Parameters: | `add_pusher` | boolean | no | Add pusher to recipients list | | `notify_only_broken_pipelines` | boolean | no | Notify only broken pipelines | | `notify_only_default_branch` | boolean | no | Send notifications only for the default branch ([introduced in GitLab 12.0](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/28271)) | +| `pipeline_events` | boolean | false | Enable notifications for pipeline events | ### Delete Pipeline-Emails service @@ -790,6 +821,7 @@ Parameters: | --------- | ---- | -------- | ----------- | | `token` | string | true | The PivotalTracker token | | `restrict_to_branch` | boolean | false | Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches. | +| `push_events` | boolean | false | Enable notifications for push events | ### Delete PivotalTracker service @@ -862,6 +894,7 @@ Parameters: | `priority` | string | true | The priority | | `device` | string | false | Leave blank for all active devices | | `sound` | string | false | The sound of the notification | +| `push_events` | boolean | false | Enable notifications for push events | ### Delete Pushover service @@ -899,6 +932,7 @@ Parameters: | `project_url` | string | true | Project url | | `issues_url` | string | true | Issue url | | `description` | string | false | Description | +| `push_events` | boolean | false | Enable notifications for push events | ### Delete Redmine service @@ -989,6 +1023,15 @@ Parameters: | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `webhook` | string | true | The Microsoft Teams webhook. For example, `https://outlook.office.com/webhook/...` | +| `push_events` | boolean | false | Enable notifications for push events | +| `issues_events` | boolean | false | Enable notifications for issue events | +| `confidential_issues_events` | boolean | false | Enable notifications for confidential issue events | +| `merge_requests_events` | boolean | false | Enable notifications for merge request events | +| `tag_push_events` | boolean | false | Enable notifications for tag push events | +| `note_events` | boolean | false | Enable notifications for note events | +| `confidental_note_events` | boolean | false | Enable notifications for confidential note events | +| `pipeline_events` | boolean | false | Enable notifications for pipeline events | +| `wiki_page_events` | boolean | false | Enable notifications for wiki page events | ### Delete Microsoft Teams service @@ -1084,6 +1127,7 @@ Parameters: | `build_type` | string | true | Build configuration ID | | `username` | string | true | A user with permissions to trigger a manual build | | `password` | string | true | The password of the user | +| `push_events` | boolean | false | Enable notifications for push events | ### Delete JetBrains TeamCity CI service @@ -1230,6 +1274,7 @@ Parameters: | `issues_url` | string | true | Issue url | | `project_url` | string | true | Project url | | `description` | string | false | Description | +| `push_events` | boolean | false | Enable notifications for push events | ### Delete YouTrack Service diff --git a/doc/ci/merge_request_pipelines/index.md b/doc/ci/merge_request_pipelines/index.md index d8c1cefc8df..5adb7ebd30d 100644 --- a/doc/ci/merge_request_pipelines/index.md +++ b/doc/ci/merge_request_pipelines/index.md @@ -124,7 +124,7 @@ otherwise pipelines for merged results won't run and your merge requests will be ## Merge Trains **[PREMIUM]** > [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/9186) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.0. -> This feature is disabled by default until we resolve issues with [contention handling](https://gitlab.com/gitlab-org/gitlab-ee/issues/11222), but [can be enabled manually](#enabling-merge-trains). +> This feature is disabled by default, but [can be enabled manually](#enabling-merge-trains). [Pipelines for merged results](#pipelines-for-merged-results-premium) introduces running a build on the result of the merged code prior to merging, as a way to keep master green. @@ -142,9 +142,8 @@ If the button is subsequently pressed in a different MR, instead of creating a n it creates a new pipeline targeting the merge result of the previous MR plus the target branch. Pipelines invalidated through failures are immediately canceled and requeued. -CAUTION: **Warning:** -At this moment, each merge train can generate a merge ref and run a pipeline **one at a time** due to a technical challenge. -We have [an issue](https://gitlab.com/gitlab-org/gitlab-ee/issues/11222) to tackle this problem to make the pipelines for merged results run in parallel. +CAUTION: **Caution:** +At the moment, each merge train can generate a merge ref and run a pipeline **one at a time**. We plan to make the pipelines for merged results [run in parallel](https://gitlab.com/gitlab-org/gitlab-ee/issues/11222) in a future release. ### Enabling Merge Trains @@ -186,11 +185,6 @@ button while the latest pipeline is running. ![Add to merge train when pipeline succeeds](img/merge_train_start_when_pipeline_succeeds.png) -### Merge Train's limitations - -- At this moment, each merge train can generate a merge ref and run a pipeline **one at a time** due to a technical challenge. - We have [an issue](https://gitlab.com/gitlab-org/gitlab-ee/issues/11222) to tackle this problem to make the pipelines for merged results run in parallel. - ## Excluding certain jobs The behavior of the `only: merge_requests` parameter is such that _only_ jobs with diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 4c170056a49..2759f1c5160 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -2620,6 +2620,24 @@ test: - pwd ``` +### Nested paths + +The value of `GIT_CLONE_PATH` is expanded once and nesting variables +within it is not supported. + +For example, you define both the variables below in your +`.gitlab-ci.yml` file: + +```yml +variables: + GOPATH: $CI_BUILDS_DIR/go + GIT_CLONE_PATH: $GOPATH/src/namespace/project +``` + +The value of `GIT_CLONE_PATH` is expanded once into +`$CI_BUILDS_DIR/go/src/namespace/project`, and results in failure +because `$CI_BUILDS_DIR` is not expanded. + ## Special YAML features It's possible to use special YAML features like anchors (`&`), aliases (`*`) diff --git a/doc/user/group/clusters/index.md b/doc/user/group/clusters/index.md index 3c5e820c1ca..26d764fa2cf 100644 --- a/doc/user/group/clusters/index.md +++ b/doc/user/group/clusters/index.md @@ -57,10 +57,6 @@ differentiate the new cluster from the rest. > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22011) in GitLab 11.5. > Became [optional](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/26565) in GitLab 11.11. -NOTE: **Note:** -Only available when creating clusters. Existing clusters not managed by GitLab -cannot become GitLab-managed later. - You can choose to allow GitLab to manage your cluster for you. If your cluster is managed by GitLab, resources for your projects will be automatically created. See the [Access controls](../../project/clusters/index.md#access-controls) section for details on which resources will diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index dc21db603d6..181b20dc710 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -225,10 +225,6 @@ applications running on the cluster. > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22011) in GitLab 11.5. > Became [optional](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/26565) in GitLab 11.11. -NOTE: **Note:** -Only available when creating clusters. Existing clusters not managed by GitLab -cannot become GitLab-managed later. - You can choose to allow GitLab to manage your cluster for you. If your cluster is managed by GitLab, resources for your projects will be automatically created. See the [Access controls](#access-controls) section for details on which resources will diff --git a/doc/workflow/todos.md b/doc/workflow/todos.md index 32907db4f46..3eac79427cf 100644 --- a/doc/workflow/todos.md +++ b/doc/workflow/todos.md @@ -34,6 +34,8 @@ A Todo appears in your Todos dashboard when: - the author, or - have set it to automatically merge once pipeline succeeds. +Todo triggers are not affected by [GitLab Notification Email settings](notifications.md). + NOTE: **Note:** When an user no longer has access to a resource related to a Todo like an issue, merge request, project or group the related Todos, for security reasons, gets deleted within the next hour. The delete is delayed to prevent data loss in case user got their access revoked by mistake. diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 8840accf675..25e9fdd5fce 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -997,7 +997,7 @@ module API class ProjectService < Grape::Entity expose :id, :title, :created_at, :updated_at, :active - expose :push_events, :issues_events, :confidential_issues_events + expose :commit_events, :push_events, :issues_events, :confidential_issues_events expose :merge_requests_events, :tag_push_events, :note_events expose :confidential_note_events, :pipeline_events, :wiki_page_events expose :job_events diff --git a/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb b/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb index dbdc59505ac..531c9ce4256 100644 --- a/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb +++ b/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb @@ -9,7 +9,7 @@ module Gitlab deployment_cluster.present? && deployment_cluster.managed? && !deployment_cluster.project_type? && - kubernetes_namespace.new_record? + (kubernetes_namespace.new_record? || kubernetes_namespace.service_account_token.blank?) end def complete! diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 186c4cb2dba..0cabaeabb9a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -9999,6 +9999,9 @@ msgstr "" msgid "The import will time out after %{timeout}. For repositories that take longer, use a clone/push combination." msgstr "" +msgid "The instance-level Kubernetes service integration is deprecated. Your data has been migrated to an <a href=\"%{url}\"/>instance-level cluster</a>." +msgstr "" + msgid "The invitation could not be accepted." msgstr "" @@ -11943,9 +11946,6 @@ msgstr "" msgid "Your Groups" msgstr "" -msgid "Your Kubernetes cluster information on this page is still editable, but you are advised to disable and reconfigure" -msgstr "" - msgid "Your Primary Email will be used for avatar detection." msgstr "" diff --git a/qa/qa/page/dashboard/projects.rb b/qa/qa/page/dashboard/projects.rb index 7ab8ee39f72..0c23d7cffbb 100644 --- a/qa/qa/page/dashboard/projects.rb +++ b/qa/qa/page/dashboard/projects.rb @@ -5,7 +5,7 @@ module QA module Dashboard class Projects < Page::Base view 'app/views/shared/projects/_search_form.html.haml' do - element :form_filter_by_name, /form_tag.+id: 'project-filter-form'/ # rubocop:disable QA/ElementWithPattern + element :project_filter_form, required: true end def go_to_project(name) @@ -14,10 +14,14 @@ module QA find_link(text: name).click end + def self.path + '/' + end + private def filter_by_name(name) - page.within('form#project-filter-form') do + within_element(:project_filter_form) do fill_in :name, with: name end end diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb index 5aef868a805..6a415b56e50 100644 --- a/qa/qa/page/merge_request/show.rb +++ b/qa/qa/page/merge_request/show.rb @@ -111,6 +111,14 @@ module QA end end + def has_title?(title) + has_element?(:title, text: title) + end + + def has_description?(description) + has_element?(:description, text: description) + end + def merge! # The merge button is disabled on load wait do diff --git a/qa/qa/resource/label.rb b/qa/qa/resource/label.rb index 5a681a5fe9f..3750725c440 100644 --- a/qa/qa/resource/label.rb +++ b/qa/qa/resource/label.rb @@ -46,7 +46,7 @@ module QA end def api_post_path - "/projects/#{project}/labels" + "/projects/#{project.id}/labels" end def api_post_body diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb index d706439a891..e8ea947581a 100644 --- a/qa/qa/resource/project.rb +++ b/qa/qa/resource/project.rb @@ -18,7 +18,11 @@ module QA end attribute :path_with_namespace do - "#{group.sandbox.path}/#{group.path}/#{name}" if group + "#{sandbox_path}#{group.path}/#{name}" if group + end + + def sandbox_path + group.respond_to?('sandbox') ? "#{group.sandbox.path}/" : '' end attribute :repository_ssh_location do diff --git a/qa/qa/resource/project_milestone.rb b/qa/qa/resource/project_milestone.rb index 8ace75f695a..70640eac095 100644 --- a/qa/qa/resource/project_milestone.rb +++ b/qa/qa/resource/project_milestone.rb @@ -31,6 +31,21 @@ module QA milestone_new.click_milestone_create_button end end + + def api_get_path + "/projects/#{project.id}/milestones/#{id}" + end + + def api_post_path + "/projects/#{project.id}/milestones" + end + + def api_post_body + { + description: @description, + title: @title + } + end end end end diff --git a/qa/qa/resource/sandbox.rb b/qa/qa/resource/sandbox.rb index 942eea5cc40..e2b1c4c0831 100644 --- a/qa/qa/resource/sandbox.rb +++ b/qa/qa/resource/sandbox.rb @@ -44,6 +44,10 @@ module QA "/groups/#{path}" end + def api_members_path + "#{api_get_path}/members" + end + def api_post_path '/groups' end diff --git a/qa/qa/runtime/address.rb b/qa/qa/runtime/address.rb index 98d042fb43a..c622051bb6d 100644 --- a/qa/qa/runtime/address.rb +++ b/qa/qa/runtime/address.rb @@ -5,7 +5,7 @@ module QA class Address attr_reader :address - def initialize(instance, page = nil) + def initialize(instance, page) @instance = instance @address = host + (page.is_a?(String) ? page : page&.path) end diff --git a/qa/qa/runtime/browser.rb b/qa/qa/runtime/browser.rb index 3bf4b3bbbfb..ed0779b93cc 100644 --- a/qa/qa/runtime/browser.rb +++ b/qa/qa/runtime/browser.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'rspec/core' +require 'rspec/expectations' require 'capybara/rspec' require 'capybara-screenshot/rspec' require 'selenium-webdriver' @@ -27,13 +28,12 @@ module QA # In case of an address that is a symbol we will try to guess address # based on `Runtime::Scenario#something_address`. # - def visit(address, page = nil, &block) - Browser::Session.new(address, page).perform(&block) + def visit(address, page_class, &block) + Browser::Session.new(address, page_class).perform(&block) end - def self.visit(address, page = nil, &block) - new.visit(address, page, &block) - page.validate_elements_present! + def self.visit(address, page_class, &block) + new.visit(address, page_class, &block) end def self.configure! @@ -128,8 +128,11 @@ module QA class Session include Capybara::DSL - def initialize(instance, page = nil) - @session_address = Runtime::Address.new(instance, page) + attr_reader :page_class + + def initialize(instance, page_class) + @session_address = Runtime::Address.new(instance, page_class) + @page_class = page_class end def url @@ -139,6 +142,8 @@ module QA def perform(&block) visit(url) + page_class.validate_elements_present! + if QA::Runtime::Env.qa_cookies browser = Capybara.current_session.driver.browser QA::Runtime::Env.qa_cookies.each do |cookie| diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb index 82510dfa03c..96f337dc081 100644 --- a/qa/qa/runtime/env.rb +++ b/qa/qa/runtime/env.rb @@ -10,13 +10,26 @@ module QA # The environment variables used to indicate if the environment under test # supports the given feature SUPPORTED_FEATURES = { - git_protocol_v2: 'QA_CAN_TEST_GIT_PROTOCOL_V2' + git_protocol_v2: 'QA_CAN_TEST_GIT_PROTOCOL_V2', + admin: 'QA_CAN_TEST_ADMIN_FEATURES' }.freeze def supported_features SUPPORTED_FEATURES end + def admin_password + ENV['GITLAB_ADMIN_PASSWORD'] + end + + def admin_username + ENV['GITLAB_ADMIN_USERNAME'] + end + + def admin_personal_access_token + ENV['GITLAB_QA_ADMIN_ACCESS_TOKEN'] + end + def debug? enabled?(ENV['QA_DEBUG'], default: false) end @@ -92,14 +105,6 @@ module QA ENV['GITLAB_PASSWORD'] end - def admin_username - ENV['GITLAB_ADMIN_USERNAME'] - end - - def admin_password - ENV['GITLAB_ADMIN_PASSWORD'] - end - def github_username ENV['GITHUB_USERNAME'] end diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb index cd1c7545944..6969f123f95 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb @@ -1,74 +1,64 @@ # frozen_string_literal: true module QA - # Failure issue: https://gitlab.com/gitlab-org/quality/staging/issues/50 - context 'Create', :quarantine do - describe 'Merge request creation' do - it 'user creates a new merge request', :smoke do + context 'Create' do + describe 'Create a new merge request' do + before do Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.act { sign_in_using_credentials } + Page::Main::Login.perform(&:sign_in_using_credentials) - current_project = Resource::Project.fabricate! do |project| - project.name = 'project-with-merge-request' + @project = Resource::Project.fabricate_via_api! do |project| + project.name = 'project' end - merge_request_title = 'This is a merge request' - merge_request_description = 'Great feature' + @merge_request_title = 'One merge request to rule them all' + @merge_request_description = '... to find them, to bring them all, and in the darkness bind them' + end - Resource::MergeRequest.fabricate! do |merge_request| - merge_request.title = merge_request_title - merge_request.description = merge_request_description - merge_request.project = current_project + it 'creates a basic merge request', :smoke do + Resource::MergeRequest.fabricate_via_browser_ui! do |merge_request| + merge_request.project = @project + merge_request.title = @merge_request_title + merge_request.description = @merge_request_description end - expect(page).to have_content(merge_request_title) - expect(page).to have_content(merge_request_description) - expect(page).to have_content('Opened just now') + Page::MergeRequest::Show.perform do |merge_request| + expect(merge_request).to have_title(@merge_request_title) + expect(merge_request).to have_description(@merge_request_description) + end end - it 'user creates a new merge request with a milestone and label' do + it 'creates a merge request with a milestone and label' do gitlab_account_username = "@#{Runtime::User.username}" - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.act { sign_in_using_credentials } - - current_project = Resource::Project.fabricate! do |project| - project.name = 'project-with-merge-request-and-milestone' - end - - current_milestone = Resource::ProjectMilestone.fabricate! do |milestone| - milestone.title = 'unique-milestone' - milestone.project = current_project + milestone = Resource::ProjectMilestone.fabricate_via_api! do |milestone| + milestone.project = @project + milestone.title = 'milestone' end - new_label = Resource::Label.fabricate_via_browser_ui! do |label| - label.project = current_project - label.title = 'qa-mr-test-label' - label.description = 'Merge Request label' + label = Resource::Label.fabricate_via_api! do |label| + label.project = @project + label.title = 'label' end - merge_request_title = 'This is a merge request with a milestone and a label' - merge_request_description = 'Great feature with milestone' - - Resource::MergeRequest.fabricate! do |merge_request| - merge_request.title = merge_request_title - merge_request.description = merge_request_description - merge_request.project = current_project - merge_request.milestone = current_milestone + Resource::MergeRequest.fabricate_via_browser_ui! do |merge_request| + merge_request.title = @merge_request_title + merge_request.description = @merge_request_description + merge_request.project = @project + merge_request.milestone = milestone merge_request.assignee = 'me' - merge_request.labels.push(new_label) + merge_request.labels.push(label) end Page::MergeRequest::Show.perform do |merge_request| - expect(merge_request).to have_content(merge_request_title) - expect(merge_request).to have_content(merge_request_description) - expect(merge_request).to have_content('Opened just now') + expect(merge_request).to have_title(@merge_request_title) + expect(merge_request).to have_description(@merge_request_description) expect(merge_request).to have_assignee(gitlab_account_username) - expect(merge_request).to have_label(new_label.title) + expect(merge_request).to have_label(label.title) end Page::Issuable::Sidebar.perform do |sidebar| - expect(sidebar).to have_milestone(current_milestone.title) + expect(sidebar).to have_milestone(milestone.title) end end end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb index 5bfafdfa041..247cde38e52 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb @@ -1,74 +1,57 @@ # frozen_string_literal: true module QA - # Failure issue: https://gitlab.com/gitlab-org/quality/staging/issues/37 - context 'Create', :quarantine do + context 'Create', :requires_admin do describe 'push after setting the file size limit via admin/application_settings' do - before(:all) do - push = Resource::Repository::ProjectPush.fabricate! do |p| - p.file_name = 'README.md' - p.file_content = '# This is a test project' - p.commit_message = 'Add README.md' + before(:context) do + @project = Resource::Project.fabricate_via_api! do |p| + p.name = 'project-test-push-limit' + p.initialize_with_readme = true end - @project = push.project + @api_client = Runtime::API::Client.new(:gitlab, personal_access_token: Runtime::Env.admin_personal_access_token) end - before do - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.perform(&:sign_in_using_credentials) - end - - after(:all) do + after(:context) do # need to set the default value after test # default value for file size limit is empty - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.perform(&:sign_in_using_credentials) - - set_file_size_limit('') - - Page::Main::Menu.perform(&:sign_out) + set_file_size_limit(nil) end it 'push successful when the file size is under the limit' do set_file_size_limit(5) - expect(page).to have_content("Application settings saved successfully") - push = push_new_file('oversize_file_1.bin', wait_for_push: true) expect(push.output).not_to have_content 'remote: fatal: pack exceeds maximum allowed size' end it 'push fails when the file size is above the limit' do set_file_size_limit(1) - expect(page).to have_content("Application settings saved successfully") - expect { push_new_file('oversize_file_2.bin', wait_for_push: false) } .to raise_error(QA::Git::Repository::RepositoryCommandError, /remote: fatal: pack exceeds maximum allowed size/) end def set_file_size_limit(limit) - Page::Main::Menu.perform(&:click_admin_area) - Page::Admin::Menu.perform(&:go_to_general_settings) + request = Runtime::API::Request.new(@api_client, '/application/settings') + put request.url, receive_max_input_size: limit - Page::Admin::Settings::General.perform do |setting| - setting.expand_account_and_limit do |page| - page.set_max_file_size(limit) - page.save_settings - end - end + expect_status(200) + expect(json_body).to match( + a_hash_including(receive_max_input_size: limit) + ) end def push_new_file(file_name, wait_for_push: true) - @project.visit! - - Resource::Repository::ProjectPush.fabricate! do |p| - p.project = @project + commit_message = 'Adding a new file' + output = Resource::Repository::Push.fabricate! do |p| + p.repository_http_uri = @project.repository_http_location.uri p.file_name = file_name p.file_content = SecureRandom.random_bytes(2000000) - p.commit_message = 'Adding a new file' - p.wait_for_push = wait_for_push + p.commit_message = commit_message p.new_branch = false end + @project.wait_for_push commit_message + + output end end end diff --git a/qa/spec/runtime/env_spec.rb b/qa/spec/runtime/env_spec.rb index 2560695ef2e..caf96a213e1 100644 --- a/qa/spec/runtime/env_spec.rb +++ b/qa/spec/runtime/env_spec.rb @@ -227,6 +227,12 @@ describe QA::Runtime::Env do env_key: 'QA_CAN_TEST_GIT_PROTOCOL_V2', default: true + it_behaves_like 'boolean method with parameter', + method: :can_test?, + param: :admin, + env_key: 'QA_CAN_TEST_ADMIN_FEATURES', + default: true + it 'raises ArgumentError if feature is unknown' do expect { described_class.can_test? :foo }.to raise_error(ArgumentError, 'Unknown feature "foo"') end diff --git a/qa/spec/specs/runner_spec.rb b/qa/spec/specs/runner_spec.rb index 5c86c102105..f94145d148e 100644 --- a/qa/spec/specs/runner_spec.rb +++ b/qa/spec/specs/runner_spec.rb @@ -1,16 +1,22 @@ # frozen_string_literal: true +require 'active_support/core_ext/hash' + describe QA::Specs::Runner do + shared_examples 'excludes orchestrated' do + it 'excludes the orchestrated tag and includes default args' do + expect_rspec_runner_arguments(['--tag', '~orchestrated', *described_class::DEFAULT_TEST_PATH_ARGS]) + + subject.perform + end + end + context '#perform' do before do allow(QA::Runtime::Browser).to receive(:configure!) end - it 'excludes the orchestrated tag by default' do - expect_rspec_runner_arguments(['--tag', '~orchestrated', *described_class::DEFAULT_TEST_PATH_ARGS]) - - subject.perform - end + it_behaves_like 'excludes orchestrated' context 'when tty is set' do subject { described_class.new.tap { |runner| runner.tty = true } } @@ -67,8 +73,6 @@ describe QA::Specs::Runner do allow(QA::Runtime::Env).to receive(:signup_disabled?).and_return(true) end - subject { described_class.new } - it 'includes default args and excludes the skip_signup_disabled tag' do expect_rspec_runner_arguments(['--tag', '~orchestrated', '--tag', '~skip_signup_disabled', *described_class::DEFAULT_TEST_PATH_ARGS]) @@ -76,18 +80,54 @@ describe QA::Specs::Runner do end end - context 'when git protocol v2 is not supported' do - before do - allow(QA::Runtime::Env).to receive(:can_test?).with(:git_protocol_v2).and_return(false) + context 'testable features' do + shared_examples 'one supported feature' do |feature| + before do + QA::Runtime::Env.supported_features.each do |tag, _| + allow(QA::Runtime::Env).to receive(:can_test?).with(tag).and_return(false) + end + + allow(QA::Runtime::Env).to receive(:can_test?).with(feature).and_return(true) unless feature.nil? + end + + it 'includes default args and excludes all unsupported tags' do + expect_rspec_runner_arguments(['--tag', '~orchestrated', *excluded_feature_tags_except(feature), *described_class::DEFAULT_TEST_PATH_ARGS]) + + subject.perform + end end - subject { described_class.new } + context 'when only git protocol 2 is supported' do + it_behaves_like 'one supported feature', :git_protocol_v2 + end - it 'includes default args and excludes the requires_git_protocol_v2 tag' do - expect_rspec_runner_arguments(['--tag', '~orchestrated', '--tag', '~requires_git_protocol_v2', *described_class::DEFAULT_TEST_PATH_ARGS]) + context 'when only admin features are supported' do + it_behaves_like 'one supported feature', :admin + end - subject.perform + context 'when no features are supported' do + it_behaves_like 'one supported feature', nil end + + context 'when all features are supported' do + before do + QA::Runtime::Env.supported_features.each do |tag, _| + allow(QA::Runtime::Env).to receive(:can_test?).with(tag).and_return(true) + end + end + + it_behaves_like 'excludes orchestrated' + end + + context 'when features are not specified' do + it_behaves_like 'excludes orchestrated' + end + end + + def excluded_feature_tags_except(tag) + QA::Runtime::Env.supported_features.except(tag).map do |tag, _| + ['--tag', "~requires_#{tag}"] + end.flatten end def expect_rspec_runner_arguments(arguments) diff --git a/spec/controllers/admin/clusters_controller_spec.rb b/spec/controllers/admin/clusters_controller_spec.rb index 7b77cb186a4..7709f525119 100644 --- a/spec/controllers/admin/clusters_controller_spec.rb +++ b/spec/controllers/admin/clusters_controller_spec.rb @@ -396,6 +396,7 @@ describe Admin::ClustersController do cluster: { enabled: false, name: 'my-new-cluster-name', + managed: false, base_domain: domain } } @@ -409,6 +410,7 @@ describe Admin::ClustersController do expect(flash[:notice]).to eq('Kubernetes cluster was successfully updated.') expect(cluster.enabled).to be_falsey expect(cluster.name).to eq('my-new-cluster-name') + expect(cluster).not_to be_managed expect(cluster.domain).to eq('test-domain.com') end @@ -433,6 +435,7 @@ describe Admin::ClustersController do cluster: { enabled: false, name: 'my-new-cluster-name', + managed: false, domain: domain } } @@ -445,6 +448,7 @@ describe Admin::ClustersController do expect(response).to have_http_status(:no_content) expect(cluster.enabled).to be_falsey expect(cluster.name).to eq('my-new-cluster-name') + expect(cluster).not_to be_managed end end diff --git a/spec/controllers/groups/clusters_controller_spec.rb b/spec/controllers/groups/clusters_controller_spec.rb index 7349cb7094c..2f64c7f3460 100644 --- a/spec/controllers/groups/clusters_controller_spec.rb +++ b/spec/controllers/groups/clusters_controller_spec.rb @@ -463,6 +463,7 @@ describe Groups::ClustersController do cluster: { enabled: false, name: 'my-new-cluster-name', + managed: false, base_domain: domain } } @@ -476,6 +477,7 @@ describe Groups::ClustersController do expect(flash[:notice]).to eq('Kubernetes cluster was successfully updated.') expect(cluster.enabled).to be_falsey expect(cluster.name).to eq('my-new-cluster-name') + expect(cluster).not_to be_managed expect(cluster.domain).to eq('test-domain.com') end @@ -500,6 +502,7 @@ describe Groups::ClustersController do cluster: { enabled: false, name: 'my-new-cluster-name', + managed: false, domain: domain } } @@ -512,6 +515,7 @@ describe Groups::ClustersController do expect(response).to have_http_status(:no_content) expect(cluster.enabled).to be_falsey expect(cluster.name).to eq('my-new-cluster-name') + expect(cluster).not_to be_managed end end diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb index c778b7888dc..cf201c9f735 100644 --- a/spec/controllers/projects/branches_controller_spec.rb +++ b/spec/controllers/projects/branches_controller_spec.rb @@ -123,7 +123,11 @@ describe Projects::BranchesController do expect(response).to redirect_to project_tree_path(project, branch) end - shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do + context 'when user configured kubernetes from CI/CD > Clusters' do + before do + create(:cluster, :provided_by_gcp, projects: [project]) + end + it 'redirects to autodeploy setup page' do result = { status: :success, branch: double(name: branch) } @@ -143,22 +147,6 @@ describe Projects::BranchesController do end end - context 'when user configured kubernetes from Integration > Kubernetes' do - before do - project.services << build(:kubernetes_service) - end - - it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes' - end - - context 'when user configured kubernetes from CI/CD > Clusters' do - before do - create(:cluster, :provided_by_gcp, projects: [project]) - end - - it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes' - end - it 'redirects to autodeploy setup page' do result = { status: :success, branch: double(name: branch) } diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb index 8d37bd82d21..fa49438287f 100644 --- a/spec/controllers/projects/clusters_controller_spec.rb +++ b/spec/controllers/projects/clusters_controller_spec.rb @@ -449,6 +449,7 @@ describe Projects::ClustersController do cluster: { enabled: false, name: 'my-new-cluster-name', + managed: false, platform_kubernetes_attributes: { namespace: 'my-namespace' } @@ -464,6 +465,7 @@ describe Projects::ClustersController do expect(flash[:notice]).to eq('Kubernetes cluster was successfully updated.') expect(cluster.enabled).to be_falsey expect(cluster.name).to eq('my-new-cluster-name') + expect(cluster).not_to be_managed expect(cluster.platform_kubernetes.namespace).to eq('my-namespace') end @@ -475,6 +477,7 @@ describe Projects::ClustersController do cluster: { enabled: false, name: 'my-new-cluster-name', + managed: false, platform_kubernetes_attributes: { namespace: 'my-namespace' } @@ -489,6 +492,7 @@ describe Projects::ClustersController do expect(response).to have_http_status(:no_content) expect(cluster.enabled).to be_falsey expect(cluster.name).to eq('my-new-cluster-name') + expect(cluster).not_to be_managed expect(cluster.platform_kubernetes.namespace).to eq('my-namespace') end end diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb index 3608d175d50..5c7f8d95f82 100644 --- a/spec/controllers/projects/services_controller_spec.rb +++ b/spec/controllers/projects/services_controller_spec.rb @@ -141,20 +141,6 @@ describe Projects::ServicesController do end end - context 'with a deprecated service' do - let(:service) { create(:kubernetes_service, project: project) } - - before do - put :update, - params: { namespace_id: project.namespace, project_id: project, id: service.to_param, service: { namespace: 'updated_namespace' } } - end - - it 'does not update the service' do - service.reload - expect(service.namespace).not_to eq('updated_namespace') - end - end - context 'when activating JIRA service from a template' do let(:template_service) { create(:jira_service, project: project, template: true) } @@ -168,20 +154,10 @@ describe Projects::ServicesController do describe "GET #edit" do before do - get :edit, params: { namespace_id: project.namespace, project_id: project, id: service_id } + get :edit, params: { namespace_id: project.namespace, project_id: project, id: 'jira' } end context 'with approved services' do - let(:service_id) { 'jira' } - - it 'renders edit page' do - expect(response).to be_success - end - end - - context 'with a deprecated service' do - let(:service_id) { 'kubernetes' } - it 'renders edit page' do expect(response).to be_success end diff --git a/spec/factories/clusters/kubernetes_namespaces.rb b/spec/factories/clusters/kubernetes_namespaces.rb index 3b50a57433f..042be7b4c4a 100644 --- a/spec/factories/clusters/kubernetes_namespaces.rb +++ b/spec/factories/clusters/kubernetes_namespaces.rb @@ -16,5 +16,9 @@ FactoryBot.define do trait :with_token do service_account_token { FFaker::Lorem.characters(10) } end + + trait :without_token do + service_account_token nil + end end end diff --git a/spec/factories/services.rb b/spec/factories/services.rb index 0d8c26a2ee9..763909f30bd 100644 --- a/spec/factories/services.rb +++ b/spec/factories/services.rb @@ -24,6 +24,8 @@ FactoryBot.define do api_url: 'https://kubernetes.example.com', token: 'a' * 40 }) + + skip_deprecation_validation true end factory :mock_deployment_service do diff --git a/spec/features/projects/clusters/interchangeability_spec.rb b/spec/features/projects/clusters/interchangeability_spec.rb deleted file mode 100644 index 0033e12b6b1..00000000000 --- a/spec/features/projects/clusters/interchangeability_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -require 'spec_helper' - -describe 'Interchangeability between KubernetesService and Platform::Kubernetes' do - EXCEPT_METHODS = %i[test title description help fields initialize_properties namespace namespace= api_url api_url= deprecated? deprecation_message].freeze - EXCEPT_METHODS_GREP_V = %w[_touched? _changed? _was].freeze - - it 'Clusters::Platform::Kubernetes covers core interfaces in KubernetesService' do - expected_interfaces = KubernetesService.instance_methods(false) - expected_interfaces = expected_interfaces - EXCEPT_METHODS - EXCEPT_METHODS_GREP_V.each do |g| - expected_interfaces = expected_interfaces.grep_v(/#{Regexp.escape(g)}\z/) - end - - expect(expected_interfaces - Clusters::Platforms::Kubernetes.instance_methods).to be_empty - end -end diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb index da4ef6428d4..fbaf12be64e 100644 --- a/spec/features/projects/environments/environment_spec.rb +++ b/spec/features/projects/environments/environment_spec.rb @@ -155,7 +155,10 @@ describe 'Environment' do end context 'with terminal' do - shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do + context 'when user configured kubernetes from CI/CD > Clusters' do + let!(:cluster) { create(:cluster, :project, :provided_by_gcp) } + let(:project) { cluster.project } + context 'for project maintainer' do let(:role) { :maintainer } @@ -191,19 +194,6 @@ describe 'Environment' do end end end - - context 'when user configured kubernetes from Integration > Kubernetes' do - let(:project) { create(:kubernetes_project, :test_repo) } - - it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes' - end - - context 'when user configured kubernetes from CI/CD > Clusters' do - let!(:cluster) { create(:cluster, :project, :provided_by_gcp) } - let(:project) { cluster.project } - - it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes' - end end context 'when environment is available' do diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb index 7b7e45312d9..1b5d9083932 100644 --- a/spec/features/projects/environments/environments_spec.rb +++ b/spec/features/projects/environments/environments_spec.rb @@ -248,7 +248,10 @@ describe 'Environments page', :js do end context 'when kubernetes terminal is available' do - shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do + context 'when user configured kubernetes from CI/CD > Clusters' do + let(:cluster) { create(:cluster, :provided_by_gcp, projects: [create(:project, :repository)]) } + let(:project) { cluster.project } + context 'for project maintainer' do let(:role) { :maintainer } @@ -265,19 +268,6 @@ describe 'Environments page', :js do end end end - - context 'when user configured kubernetes from Integration > Kubernetes' do - let(:project) { create(:kubernetes_project, :test_repo) } - - it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes' - end - - context 'when user configured kubernetes from CI/CD > Clusters' do - let(:cluster) { create(:cluster, :provided_by_gcp, projects: [create(:project, :repository)]) } - let(:project) { cluster.project } - - it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes' - end end end diff --git a/spec/lib/gitlab/ci/build/policy/kubernetes_spec.rb b/spec/lib/gitlab/ci/build/policy/kubernetes_spec.rb index 4884d5f8ba4..4510b82ca9d 100644 --- a/spec/lib/gitlab/ci/build/policy/kubernetes_spec.rb +++ b/spec/lib/gitlab/ci/build/policy/kubernetes_spec.rb @@ -4,24 +4,14 @@ describe Gitlab::Ci::Build::Policy::Kubernetes do let(:pipeline) { create(:ci_pipeline, project: project) } context 'when kubernetes service is active' do - shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do - it 'is satisfied by a kubernetes pipeline' do - expect(described_class.new('active')) - .to be_satisfied_by(pipeline) - end - end - - context 'when user configured kubernetes from Integration > Kubernetes' do - let(:project) { create(:kubernetes_project) } - - it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes' - end - context 'when user configured kubernetes from CI/CD > Clusters' do let!(:cluster) { create(:cluster, :project, :provided_by_gcp) } let(:project) { cluster.project } - it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes' + it 'is satisfied by a kubernetes pipeline' do + expect(described_class.new('active')) + .to be_satisfied_by(pipeline) + end end end diff --git a/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb b/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb index 5387863bd07..5ac5122e800 100644 --- a/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb +++ b/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb @@ -35,9 +35,15 @@ describe Gitlab::Ci::Build::Prerequisite::KubernetesNamespace do end context 'and a namespace is already created for this project' do - let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, cluster: cluster, project: build.project) } + let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, :with_token, cluster: cluster, project: build.project) } it { is_expected.to be_falsey } + + context 'and the service_account_token is blank' do + let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, :without_token, cluster: cluster, project: build.project) } + + it { is_expected.to be_truthy } + end end context 'and cluster is project type' do diff --git a/spec/migrations/migrate_k8s_service_integration_spec.rb b/spec/migrations/migrate_k8s_service_integration_spec.rb new file mode 100644 index 00000000000..4dd0c09632a --- /dev/null +++ b/spec/migrations/migrate_k8s_service_integration_spec.rb @@ -0,0 +1,161 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20190517153211_migrate_k8s_service_integration.rb') + +describe MigrateK8sServiceIntegration, :migration do + context 'template service' do + context 'with namespace' do + let!(:service) do + MigrateK8sServiceIntegration::Service.create!( + active: true, + template: true, + category: 'deployment', + type: 'KubernetesService', + properties: "{\"namespace\":\"prod\",\"api_url\":\"https://sample.kubernetes.com\",\"ca_pem\":\"ca_pem-sample\",\"token\":\"token-sample\"}" + ) + end + + let(:cluster) { MigrateK8sServiceIntegration::Cluster.instance_type.last! } + let(:platform) { cluster.platform_kubernetes } + + it 'migrates the KubernetesService template to Platform::Kubernetes' do + expect { migrate! }.to change { MigrateK8sServiceIntegration::Cluster.count }.by(1) + + expect(cluster).to be_enabled + expect(cluster).to be_user + expect(cluster).not_to be_managed + expect(cluster.environment_scope).to eq('*') + expect(platform.api_url).to eq('https://sample.kubernetes.com') + expect(platform.ca_cert).to eq('ca_pem-sample') + expect(platform.namespace).to eq('prod') + expect(platform.token).to eq('token-sample') + end + end + + context 'without namespace' do + let!(:service) do + MigrateK8sServiceIntegration::Service.create!( + active: true, + template: true, + category: 'deployment', + type: 'KubernetesService', + properties: "{\"namespace\":\"\",\"api_url\":\"https://sample.kubernetes.com\",\"ca_pem\":\"ca_pem-sample\",\"token\":\"token-sample\"}" + ) + end + + let(:cluster) { MigrateK8sServiceIntegration::Cluster.instance_type.last! } + let(:platform) { cluster.platform_kubernetes } + + it 'migrates the KubernetesService template to Platform::Kubernetes' do + expect { migrate! }.to change { MigrateK8sServiceIntegration::Cluster.count }.by(1) + + expect(cluster).to be_enabled + expect(cluster).to be_user + expect(cluster).not_to be_managed + expect(cluster.environment_scope).to eq('*') + expect(platform.api_url).to eq('https://sample.kubernetes.com') + expect(platform.ca_cert).to eq('ca_pem-sample') + expect(platform.namespace).to be_nil + expect(platform.token).to eq('token-sample') + end + end + + context 'with nullified parameters' do + let!(:service) do + MigrateK8sServiceIntegration::Service.create!( + active: true, + template: true, + category: 'deployment', + type: 'KubernetesService', + properties: "{}" + ) + end + + it 'does not migrate the KubernetesService' do + expect { migrate! }.not_to change { MigrateK8sServiceIntegration::Cluster.count } + end + end + + context 'when disabled' do + let!(:service) do + MigrateK8sServiceIntegration::Service.create!( + active: false, + template: true, + category: 'deployment', + type: 'KubernetesService', + properties: "{\"namespace\":\"prod\",\"api_url\":\"https://sample.kubernetes.com\",\"ca_pem\":\"ca_pem-sample\",\"token\":\"token-sample\"}" + ) + end + + let(:cluster) { MigrateK8sServiceIntegration::Cluster.instance_type.last! } + let(:platform) { cluster.platform_kubernetes } + + it 'migrates the KubernetesService template to Platform::Kubernetes' do + expect { migrate! }.to change { MigrateK8sServiceIntegration::Cluster.count }.by(1) + + expect(cluster).not_to be_enabled + expect(cluster).to be_user + expect(cluster).not_to be_managed + expect(cluster.environment_scope).to eq('*') + expect(platform.api_url).to eq('https://sample.kubernetes.com') + expect(platform.ca_cert).to eq('ca_pem-sample') + expect(platform.namespace).to eq('prod') + expect(platform.token).to eq('token-sample') + end + end + + context 'when an instance cluster already exists' do + let!(:service) do + MigrateK8sServiceIntegration::Service.create!( + active: true, + template: true, + category: 'deployment', + type: 'KubernetesService', + properties: "{\"namespace\":\"prod\",\"api_url\":\"https://sample.kubernetes.com\",\"ca_pem\":\"ca_pem-sample\",\"token\":\"token-sample\"}" + ) + end + + let!(:existing_cluster) do + MigrateK8sServiceIntegration::Cluster.create!( + name: 'test-cluster', + cluster_type: :instance_type, + managed: true, + provider_type: :user, + platform_type: :kubernetes + ) + end + let(:new_cluster) { MigrateK8sServiceIntegration::Cluster.instance_type.last! } + let(:platform) { new_cluster.platform_kubernetes } + + it 'migrates the KubernetesService template to disabled Platform::Kubernetes' do + expect { migrate! }.to change { MigrateK8sServiceIntegration::Cluster.count }.by(1) + + expect(new_cluster).not_to be_enabled + expect(new_cluster).to be_user + expect(new_cluster).not_to be_managed + expect(new_cluster.environment_scope).to eq('*') + expect(platform.api_url).to eq('https://sample.kubernetes.com') + expect(platform.ca_cert).to eq('ca_pem-sample') + expect(platform.namespace).to eq('prod') + expect(platform.token).to eq('token-sample') + end + end + end + + context 'non-template service' do + let!(:service) do + MigrateK8sServiceIntegration::Service.create!( + active: true, + template: false, + category: 'deployment', + type: 'KubernetesService', + properties: "{\"namespace\":\"prod\",\"api_url\":\"https://sample.kubernetes.com\",\"ca_pem\":\"ca_pem-sample\",\"token\":\"token-sample\"}" + ) + end + + it 'does not migrate the KubernetesService' do + expect { migrate! }.not_to change { MigrateK8sServiceIntegration::Cluster.count } + end + end +end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index c4e54be673f..6ebc6337d50 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -962,7 +962,11 @@ describe Ci::Pipeline, :mailer do end context 'when kubernetes is active' do - shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do + context 'when user configured kubernetes from CI/CD > Clusters' do + let!(:cluster) { create(:cluster, :project, :provided_by_gcp) } + let(:project) { cluster.project } + let(:pipeline) { build(:ci_pipeline, project: project, config: config) } + it 'returns seeds for kubernetes dependent job' do seeds = pipeline.stage_seeds @@ -971,21 +975,6 @@ describe Ci::Pipeline, :mailer do expect(seeds.dig(1, 0, :name)).to eq 'production' end end - - context 'when user configured kubernetes from Integration > Kubernetes' do - let(:project) { create(:kubernetes_project) } - let(:pipeline) { build(:ci_pipeline, project: project, config: config) } - - it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes' - end - - context 'when user configured kubernetes from CI/CD > Clusters' do - let!(:cluster) { create(:cluster, :project, :provided_by_gcp) } - let(:project) { cluster.project } - let(:pipeline) { build(:ci_pipeline, project: project, config: config) } - - it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes' - end end context 'when kubernetes is not active' do @@ -1679,23 +1668,13 @@ describe Ci::Pipeline, :mailer do describe '#has_kubernetes_active?' do context 'when kubernetes is active' do - shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do - it 'returns true' do - expect(pipeline).to have_kubernetes_active - end - end - - context 'when user configured kubernetes from Integration > Kubernetes' do - let(:project) { create(:kubernetes_project) } - - it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes' - end - context 'when user configured kubernetes from CI/CD > Clusters' do let!(:cluster) { create(:cluster, :project, :provided_by_gcp) } let(:project) { cluster.project } - it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes' + it 'returns true' do + expect(pipeline).to have_kubernetes_active + end end end diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index f206bb41f45..52661178d76 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -514,19 +514,43 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do subject { cluster.allow_user_defined_namespace? } context 'project type cluster' do - it { is_expected.to be_truthy } + context 'gitlab managed' do + it { is_expected.to be_truthy } + end + + context 'not managed' do + let(:cluster) { create(:cluster, :provided_by_gcp, managed: false) } + + it { is_expected.to be_truthy } + end end context 'group type cluster' do - let(:cluster) { create(:cluster, :provided_by_gcp, :group) } + context 'gitlab managed' do + let(:cluster) { create(:cluster, :provided_by_gcp, :group) } - it { is_expected.to be_falsey } + it { is_expected.to be_falsey } + end + + context 'not managed' do + let(:cluster) { create(:cluster, :provided_by_gcp, :group, managed: false) } + + it { is_expected.to be_truthy } + end end context 'instance type cluster' do - let(:cluster) { create(:cluster, :provided_by_gcp, :instance) } + context 'gitlab managed' do + let(:cluster) { create(:cluster, :provided_by_gcp, :instance) } - it { is_expected.to be_falsey } + it { is_expected.to be_falsey } + end + + context 'not managed' do + let(:cluster) { create(:cluster, :provided_by_gcp, :instance, managed: false) } + + it { is_expected.to be_truthy } + end end end @@ -555,6 +579,63 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do end end + describe '#find_or_initialize_kubernetes_namespace_for_project' do + let(:cluster) { create(:cluster, :project, :provided_by_gcp) } + let(:project) { cluster.projects.first } + + subject { cluster.find_or_initialize_kubernetes_namespace_for_project(project) } + + context 'kubernetes namespace exists' do + context 'with no service account token' do + let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, project: project, cluster: cluster) } + + it { is_expected.to eq kubernetes_namespace } + end + + context 'with a service account token' do + let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, :with_token, project: project, cluster: cluster) } + + it { is_expected.to eq kubernetes_namespace } + end + end + + context 'kubernetes namespace does not exist' do + it 'initializes a new namespace and sets default values' do + expect(subject).to be_new_record + expect(subject.project).to eq project + expect(subject.cluster).to eq cluster + expect(subject.namespace).to be_present + expect(subject.service_account_name).to be_present + end + end + + context 'a custom scope is provided' do + let(:scope) { cluster.kubernetes_namespaces.has_service_account_token } + + subject { cluster.find_or_initialize_kubernetes_namespace_for_project(project, scope: scope) } + + context 'kubernetes namespace exists' do + context 'with no service account token' do + let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, project: project, cluster: cluster) } + + it 'initializes a new namespace and sets default values' do + expect(subject).to be_new_record + expect(subject.project).to eq project + expect(subject.cluster).to eq cluster + expect(subject.namespace).to be_present + expect(subject.service_account_name).to be_present + end + end + + context 'with a service account token' do + let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, :with_token, project: project, cluster: cluster) } + + it { is_expected.to eq kubernetes_namespace } + end + end + end + end + describe '#predefined_variables' do subject { cluster.predefined_variables } diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb index c485850c16e..1fb3a8de808 100644 --- a/spec/models/clusters/platforms/kubernetes_spec.rb +++ b/spec/models/clusters/platforms/kubernetes_spec.rb @@ -223,19 +223,33 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching let(:namespace) { 'namespace-123' } it { is_expected.to eq(namespace) } + + context 'kubernetes namespace is present but has no service account token' do + let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, cluster: cluster) } + + it { is_expected.to eq(namespace) } + end end context 'with no namespace assigned' do let(:namespace) { nil } context 'when kubernetes namespace is present' do - let(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, cluster: cluster) } + let(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, :with_token, cluster: cluster) } before do kubernetes_namespace end it { is_expected.to eq(kubernetes_namespace.namespace) } + + context 'kubernetes namespace has no service account token' do + before do + kubernetes_namespace.update!(namespace: 'old-namespace', service_account_token: nil) + end + + it { is_expected.to eq("#{project.path}-#{project.id}") } + end end context 'when kubernetes namespace is not present' do @@ -284,6 +298,46 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching { key: 'KUBE_TOKEN', value: kubernetes_namespace.service_account_token, public: false, masked: true } ) end + + context 'the cluster has been set to unmanaged after the namespace was created' do + before do + cluster.update!(managed: false) + end + + it_behaves_like 'setting variables' + + it 'sets KUBE_TOKEN from the platform' do + expect(subject).to include( + { key: 'KUBE_TOKEN', value: kubernetes.token, public: false, masked: true } + ) + end + + context 'the platform has a custom namespace set' do + before do + kubernetes.update!(namespace: 'custom-namespace') + end + + it 'sets KUBE_NAMESPACE from the platform' do + expect(subject).to include( + { key: 'KUBE_NAMESPACE', value: kubernetes.namespace, public: true, masked: false } + ) + end + end + + context 'there is no namespace specified on the platform' do + let(:project) { cluster.project } + + before do + kubernetes.update!(namespace: nil) + end + + it 'sets KUBE_NAMESPACE to a default for the project' do + expect(subject).to include( + { key: 'KUBE_NAMESPACE', value: "#{project.path}-#{project.id}", public: true, masked: false } + ) + end + end + end end context 'namespace is provided' do diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 7233d2454c6..379dda1f5c4 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -515,29 +515,19 @@ describe Environment do context 'when the environment is available' do context 'with a deployment service' do - shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do - context 'and a deployment' do + context 'when user configured kubernetes from CI/CD > Clusters' do + let!(:cluster) { create(:cluster, :project, :provided_by_gcp) } + let(:project) { cluster.project } + + context 'with deployment' do let!(:deployment) { create(:deployment, :success, environment: environment) } it { is_expected.to be_truthy } end - context 'but no deployments' do + context 'without deployments' do it { is_expected.to be_falsy } end end - - context 'when user configured kubernetes from Integration > Kubernetes' do - let(:project) { create(:kubernetes_project) } - - it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes' - end - - context 'when user configured kubernetes from CI/CD > Clusters' do - let!(:cluster) { create(:cluster, :project, :provided_by_gcp) } - let(:project) { cluster.project } - - it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes' - end end context 'without a deployment service' do @@ -546,8 +536,6 @@ describe Environment do end context 'when the environment is unavailable' do - let(:project) { create(:kubernetes_project) } - before do environment.stop end @@ -590,7 +578,10 @@ describe Environment do allow(environment).to receive(:has_terminals?).and_return(true) end - shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do + context 'when user configured kubernetes from CI/CD > Clusters' do + let!(:cluster) { create(:cluster, :project, :provided_by_gcp) } + let(:project) { cluster.project } + it 'returns the terminals from the deployment service' do expect(environment.deployment_platform) .to receive(:terminals).with(environment) @@ -599,19 +590,6 @@ describe Environment do is_expected.to eq(:fake_terminals) end end - - context 'when user configured kubernetes from Integration > Kubernetes' do - let(:project) { create(:kubernetes_project) } - - it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes' - end - - context 'when user configured kubernetes from CI/CD > Clusters' do - let!(:cluster) { create(:cluster, :project, :provided_by_gcp) } - let(:project) { cluster.project } - - it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes' - end end context 'when the environment does not have terminals' do diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index 2fce120381b..34ee1eafd5c 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -17,6 +17,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do context 'when service is active' do before do subject.active = true + subject.skip_deprecation_validation = true end it { is_expected.not_to validate_presence_of(:namespace) } @@ -67,6 +68,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do before do kubernetes_service.update_attribute(:active, false) + kubernetes_service.skip_deprecation_validation = false kubernetes_service.properties['namespace'] = "foo" end @@ -80,19 +82,11 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do end end - context 'with a non-deprecated service' do - let(:kubernetes_service) { create(:kubernetes_service) } - - it 'updates attributes' do - kubernetes_service.properties['namespace'] = 'foo' - expect(kubernetes_service.save).to be_truthy - end - end - context 'with an active and deprecated service' do let(:kubernetes_service) { create(:kubernetes_service) } before do + kubernetes_service.skip_deprecation_validation = false kubernetes_service.active = false kubernetes_service.properties['namespace'] = 'foo' kubernetes_service.save @@ -110,19 +104,6 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do expect(kubernetes_service.properties['namespace']).to eq("foo") end end - - context 'with a template service' do - let(:kubernetes_service) { create(:kubernetes_service, template: true, active: false) } - - before do - kubernetes_service.properties['namespace'] = 'foo' - end - - it 'updates attributes' do - expect(kubernetes_service.save).to be_truthy - expect(kubernetes_service.properties['namespace']).to eq('foo') - end - end end describe '#initialize_properties' do @@ -393,17 +374,8 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do describe "#deprecated?" do let(:kubernetes_service) { create(:kubernetes_service) } - context 'with an active kubernetes service' do - it 'returns false' do - expect(kubernetes_service.deprecated?).to be_falsy - end - end - - context 'with a inactive kubernetes service' do - it 'returns true' do - kubernetes_service.update_attribute(:active, false) - expect(kubernetes_service.deprecated?).to be_truthy - end + it 'returns true' do + expect(kubernetes_service.deprecated?).to be_truthy end end @@ -414,12 +386,6 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do expect(kubernetes_service.deprecation_message).to match(/Kubernetes service integration has been deprecated/) end - context 'if the services is active' do - it 'returns a message' do - expect(kubernetes_service.deprecation_message).to match(/Your Kubernetes cluster information on this page is still editable/) - end - end - context 'if the service is not active' do it 'returns a message' do kubernetes_service.update_attribute(:active, false) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 269d2bb90d3..20b98b5eb85 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -2652,7 +2652,10 @@ describe Project do end context 'when project has a deployment service' do - shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do + context 'when user configured kubernetes from CI/CD > Clusters and KubernetesNamespace migration has not been executed' do + let!(:cluster) { create(:cluster, :project, :provided_by_gcp) } + let(:project) { cluster.project } + it 'returns variables from this service' do expect(project.deployment_variables).to include( { key: 'KUBE_TOKEN', value: project.deployment_platform.token, public: false, masked: true } @@ -2660,19 +2663,6 @@ describe Project do end end - context 'when user configured kubernetes from Integration > Kubernetes' do - let(:project) { create(:kubernetes_project) } - - it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes' - end - - context 'when user configured kubernetes from CI/CD > Clusters and KubernetesNamespace migration has not been executed' do - let!(:cluster) { create(:cluster, :project, :provided_by_gcp) } - let(:project) { cluster.project } - - it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes' - end - context 'when user configured kubernetes from CI/CD > Clusters and KubernetesNamespace migration has been executed' do let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, :with_token) } let!(:cluster) { kubernetes_namespace.cluster } diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index c9439b0846d..d442c73c118 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -82,7 +82,7 @@ describe Service do context 'when template is invalid' do it 'sets service template to inactive when template is invalid' do project = create(:project) - template = KubernetesService.new(template: true, active: true) + template = build(:prometheus_service, template: true, active: true, properties: {}) template.save(validate: false) service = described_class.build_from_template(project.id, template) @@ -309,10 +309,10 @@ describe Service do end describe '.find_by_template' do - let!(:kubernetes_service) { create(:kubernetes_service, template: true) } + let!(:service) { create(:service, template: true) } it 'returns service template' do - expect(KubernetesService.find_by_template).to eq(kubernetes_service) + expect(described_class.find_by_template).to eq(service) end end diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb index e260aa21e25..3f79e332b90 100644 --- a/spec/requests/api/services_spec.rb +++ b/spec/requests/api/services_spec.rb @@ -10,7 +10,10 @@ describe API::Services do end Service.available_services_names.each do |service| - describe "PUT /projects/:id/services/#{service.dasherize}" do + # TODO: Remove below `if: (service != "kubernetes")` in the next release + # KubernetesService was deprecated and it can't be updated. Right now it's + # only readable. It should be completely removed in the next iteration. + describe "PUT /projects/:id/services/#{service.dasherize}", if: (service != "kubernetes") do include_context service it "updates #{service} settings" do @@ -19,13 +22,22 @@ describe API::Services do expect(response).to have_gitlab_http_status(200) current_service = project.services.first - event = current_service.event_names.empty? ? "foo" : current_service.event_names.first - state = current_service[event] || false + events = current_service.event_names.empty? ? ["foo"].freeze : current_service.event_names + query_strings = [] + events.each do |event| + query_strings << "#{event}=#{!current_service[event]}" + end + query_strings = query_strings.join('&') - put api("/projects/#{project.id}/services/#{dashed_service}?#{event}=#{!state}", user), params: service_attrs + put api("/projects/#{project.id}/services/#{dashed_service}?#{query_strings}", user), params: service_attrs expect(response).to have_gitlab_http_status(200) - expect(project.services.first[event]).not_to eq(state) unless event == "foo" + events.each do |event| + next if event == "foo" + + expect(project.services.first[event]).not_to eq(current_service[event]), + "expected #{!current_service[event]} for event #{event} for service #{current_service.title}, got #{current_service[event]}" + end end it "returns if required fields missing" do @@ -50,7 +62,10 @@ describe API::Services do end end - describe "DELETE /projects/:id/services/#{service.dasherize}" do + # TODO: Remove below `if: (service != "kubernetes")` in the next release + # KubernetesService was deprecated and it can't be updated. Right now it's + # only readable. It should be completely removed in the next iteration. + describe "DELETE /projects/:id/services/#{service.dasherize}", if: (service != "kubernetes") do include_context service before do diff --git a/spec/serializers/environment_entity_spec.rb b/spec/serializers/environment_entity_spec.rb index c2312734042..906449f470b 100644 --- a/spec/serializers/environment_entity_spec.rb +++ b/spec/serializers/environment_entity_spec.rb @@ -59,15 +59,5 @@ describe EnvironmentEntity do expect(subject[:cluster_type]).to eq('project_type') end end - - context 'when deployment platform is a Kubernetes Service' do - before do - create(:kubernetes_service, project: project) - end - - it 'does not include cluster_type' do - expect(subject).not_to include(:cluster_type) - end - end end end diff --git a/spec/support/prometheus/additional_metrics_shared_examples.rb b/spec/support/prometheus/additional_metrics_shared_examples.rb index 8044b061ca5..de21e808932 100644 --- a/spec/support/prometheus/additional_metrics_shared_examples.rb +++ b/spec/support/prometheus/additional_metrics_shared_examples.rb @@ -44,7 +44,9 @@ RSpec.shared_examples 'additional metrics query' do end describe 'project has Kubernetes service' do - shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do + context 'when user configured kubernetes from CI/CD > Clusters' do + let!(:cluster) { create(:cluster, :project, :provided_by_gcp) } + let(:project) { cluster.project } let(:environment) { create(:environment, slug: 'environment-slug', project: project) } let(:kube_namespace) { project.deployment_platform.kubernetes_namespace_for(project) } @@ -56,19 +58,6 @@ RSpec.shared_examples 'additional metrics query' do subject.query(*query_params) end end - - context 'when user configured kubernetes from Integration > Kubernetes' do - let(:project) { create(:kubernetes_project) } - - it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes' - end - - context 'when user configured kubernetes from CI/CD > Clusters' do - let!(:cluster) { create(:cluster, :project, :provided_by_gcp) } - let(:project) { cluster.project } - - it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes' - end end describe 'project without Kubernetes service' do diff --git a/spec/support/shared_contexts/services_shared_context.rb b/spec/support/shared_contexts/services_shared_context.rb index 089f1798cd2..0c3a24d206f 100644 --- a/spec/support/shared_contexts/services_shared_context.rb +++ b/spec/support/shared_contexts/services_shared_context.rb @@ -37,8 +37,7 @@ Service.available_services_names.each do |service| def initialize_service(service) service_item = project.find_or_initialize_service(service) service_item.properties = service_attrs - service_item.active = true if service == "kubernetes" - service_item.save + service_item.save! service_item end end diff --git a/spec/workers/reactive_caching_worker_spec.rb b/spec/workers/reactive_caching_worker_spec.rb index 2395e6ec947..b8ca6063ccd 100644 --- a/spec/workers/reactive_caching_worker_spec.rb +++ b/spec/workers/reactive_caching_worker_spec.rb @@ -6,16 +6,6 @@ describe ReactiveCachingWorker do let(:service) { project.deployment_platform } describe '#perform' do - context 'when user configured kubernetes from Integration > Kubernetes' do - let(:project) { create(:kubernetes_project) } - - it 'calls #exclusively_update_reactive_cache!' do - expect_any_instance_of(KubernetesService).to receive(:exclusively_update_reactive_cache!) - - described_class.new.perform("KubernetesService", service.id) - end - end - context 'when user configured kubernetes from CI/CD > Clusters' do let!(:cluster) { create(:cluster, :project, :provided_by_gcp) } let(:project) { cluster.project } |