diff options
101 files changed, 2242 insertions, 449 deletions
@@ -431,6 +431,3 @@ gem 'flipper-active_support_cache_store', '~> 0.13.0' # Structured logging gem 'lograge', '~> 0.5' gem 'grape_logging', '~> 1.7' - -# Asset synchronization -gem 'asset_sync', '~> 2.4' diff --git a/Gemfile.lock b/Gemfile.lock index d9aae0b5297..50e3ddef1e1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -58,11 +58,6 @@ GEM asciidoctor (1.5.6.2) asciidoctor-plantuml (0.0.8) asciidoctor (~> 1.5) - asset_sync (2.4.0) - activemodel (>= 4.1.0) - fog-core - mime-types (>= 2.99) - unf ast (2.4.0) atomic (1.1.99) attr_encrypted (3.1.0) @@ -938,7 +933,6 @@ DEPENDENCIES asana (~> 0.6.0) asciidoctor (~> 1.5.6) asciidoctor-plantuml (= 0.0.8) - asset_sync (~> 2.4) attr_encrypted (~> 3.1.0) awesome_print babosa (~> 1.0.2) diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock index b656365103f..181f2db95b0 100644 --- a/Gemfile.rails5.lock +++ b/Gemfile.rails5.lock @@ -61,11 +61,6 @@ GEM asciidoctor (1.5.6.2) asciidoctor-plantuml (0.0.8) asciidoctor (~> 1.5) - asset_sync (2.4.0) - activemodel (>= 4.1.0) - fog-core - mime-types (>= 2.99) - unf ast (2.4.0) atomic (1.1.99) attr_encrypted (3.1.0) @@ -947,7 +942,6 @@ DEPENDENCIES asana (~> 0.6.0) asciidoctor (~> 1.5.6) asciidoctor-plantuml (= 0.0.8) - asset_sync (~> 2.4) attr_encrypted (~> 3.1.0) awesome_print babosa (~> 1.0.2) diff --git a/app/assets/images/cluster_app_logos/knative.png b/app/assets/images/cluster_app_logos/knative.png Binary files differnew file mode 100644 index 00000000000..0a2510c8549 --- /dev/null +++ b/app/assets/images/cluster_app_logos/knative.png diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index ebf76af5966..02dfe1c7d6f 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -28,6 +28,7 @@ export default class Clusters { installIngressPath, installRunnerPath, installJupyterPath, + installKnativePath, installPrometheusPath, managePrometheusPath, clusterStatus, @@ -49,6 +50,7 @@ export default class Clusters { installRunnerEndpoint: installRunnerPath, installPrometheusEndpoint: installPrometheusPath, installJupyterEndpoint: installJupyterPath, + installKnativeEndpoint: installKnativePath, }); this.installApplication = this.installApplication.bind(this); diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index 6d7f45a35d8..c7ffb470d4d 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -7,6 +7,7 @@ import helmLogo from 'images/cluster_app_logos/helm.png'; import jeagerLogo from 'images/cluster_app_logos/jeager.png'; import jupyterhubLogo from 'images/cluster_app_logos/jupyterhub.png'; import kubernetesLogo from 'images/cluster_app_logos/kubernetes.png'; +import knativeLogo from 'images/cluster_app_logos/knative.png'; import meltanoLogo from 'images/cluster_app_logos/meltano.png'; import prometheusLogo from 'images/cluster_app_logos/prometheus.png'; import { s__, sprintf } from '../../locale'; @@ -53,6 +54,7 @@ export default { jeagerLogo, jupyterhubLogo, kubernetesLogo, + knativeLogo, meltanoLogo, prometheusLogo, }), @@ -136,6 +138,9 @@ export default { jupyterHostname() { return this.applications.jupyter.hostname; }, + knativeInstalled() { + return this.applications.knative.status === APPLICATION_STATUS.INSTALLED; + }, }, created() { this.helmInstallIllustration = helmInstallIllustration; @@ -321,7 +326,6 @@ export default { :request-reason="applications.jupyter.requestReason" :install-application-request-params="{ hostname: applications.jupyter.hostname }" :disabled="!helmInstalled" - class="hide-bottom-border rounded-bottom" title-link="https://jupyterhub.readthedocs.io/en/stable/" > <div slot="description"> @@ -371,6 +375,58 @@ export default { </template> </div> </application-row> + <application-row + id="knative" + :logo-url="knativeLogo" + :title="applications.knative.title" + :status="applications.knative.status" + :status-reason="applications.knative.statusReason" + :request-status="applications.knative.requestStatus" + :request-reason="applications.knative.requestReason" + :install-application-request-params="{ hostname: applications.knative.hostname}" + :disabled="!helmInstalled" + class="hide-bottom-border rounded-bottom" + title-link="https://github.com/knative/docs" + > + <div slot="description"> + <p> + {{ s__(`ClusterIntegration|A Knative build extends Kubernetes + and utilizes existing Kubernetes primitives to provide you with + the ability to run on-cluster container builds from source. + For example, you can write a build that uses Kubernetes-native + resources to obtain your source code from a repository, + build it into container a image, and then run that image.`) }} + </p> + + <template v-if="knativeInstalled"> + <div class="form-group"> + <label for="knative-domainname"> + {{ s__('ClusterIntegration|Knative Domain Name:') }} + </label> + <input + id="knative-domainname" + v-model="applications.knative.hostname" + type="text" + class="form-control js-domainname" + readonly + /> + </div> + </template> + <template v-else> + <div class="form-group"> + <label for="knative-domainname"> + {{ s__('ClusterIntegration|Knative Domain Name:') }} + </label> + <input + id="knative-domainname" + v-model="applications.knative.hostname" + type="text" + class="form-control js-domainname" + /> + </div> + </template> + </div> + </application-row> </div> </section> </template> diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js index 24a49624583..d707420c845 100644 --- a/app/assets/javascripts/clusters/constants.js +++ b/app/assets/javascripts/clusters/constants.js @@ -16,3 +16,4 @@ export const REQUEST_SUCCESS = 'request-success'; export const REQUEST_FAILURE = 'request-failure'; export const INGRESS = 'ingress'; export const JUPYTER = 'jupyter'; +export const KNATIVE = 'knative'; diff --git a/app/assets/javascripts/clusters/services/clusters_service.js b/app/assets/javascripts/clusters/services/clusters_service.js index a7d82292ba9..da562b09ee5 100644 --- a/app/assets/javascripts/clusters/services/clusters_service.js +++ b/app/assets/javascripts/clusters/services/clusters_service.js @@ -9,6 +9,7 @@ export default class ClusterService { runner: this.options.installRunnerEndpoint, prometheus: this.options.installPrometheusEndpoint, jupyter: this.options.installJupyterEndpoint, + knative: this.options.installKnativeEndpoint, }; } diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js index 106ac3cb516..e45da967392 100644 --- a/app/assets/javascripts/clusters/stores/clusters_store.js +++ b/app/assets/javascripts/clusters/stores/clusters_store.js @@ -1,5 +1,5 @@ import { s__ } from '../../locale'; -import { INGRESS, JUPYTER } from '../constants'; +import { INGRESS, JUPYTER, KNATIVE } from '../constants'; export default class ClusterStore { constructor() { @@ -46,6 +46,14 @@ export default class ClusterStore { requestReason: null, hostname: null, }, + knative: { + title: s__('ClusterIntegration|Knative'), + status: null, + statusReason: null, + requestStatus: null, + requestReason: null, + hostname: null, + }, }, }; } @@ -93,6 +101,9 @@ export default class ClusterStore { (this.state.applications.ingress.externalIp ? `jupyter.${this.state.applications.ingress.externalIp}.nip.io` : ''); + } else if (appId === KNATIVE) { + this.state.applications.knative.hostname = + serverAppEntry.hostname || this.state.applications.knative.hostname; } }); } diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue index 29b5aff0fb1..a5b87dfc2d9 100644 --- a/app/assets/javascripts/diffs/components/compare_versions.vue +++ b/app/assets/javascripts/diffs/components/compare_versions.vue @@ -36,7 +36,7 @@ export default { }, computed: { ...mapState('diffs', ['commit', 'showTreeList']), - ...mapGetters('diffs', ['isInlineView', 'isParallelView', 'areAllFilesCollapsed']), + ...mapGetters('diffs', ['isInlineView', 'isParallelView', 'hasCollapsedFile']), comparableDiffs() { return this.mergeRequestDiffs.slice(1); }, @@ -113,8 +113,8 @@ export default { class="inline-parallel-buttons d-none d-md-flex ml-auto" > <a - v-if="areAllFilesCollapsed" - class="btn btn-default" + v-show="hasCollapsedFile" + class="btn btn-default append-right-8" @click="expandAllFiles" > {{ __('Expand all') }} diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js index 2bf0ad99c22..bf490f9d78a 100644 --- a/app/assets/javascripts/diffs/store/getters.js +++ b/app/assets/javascripts/diffs/store/getters.js @@ -5,7 +5,7 @@ export const isParallelView = state => state.diffViewType === PARALLEL_DIFF_VIEW export const isInlineView = state => state.diffViewType === INLINE_DIFF_VIEW_TYPE; -export const areAllFilesCollapsed = state => state.diffFiles.every(file => file.collapsed); +export const hasCollapsedFile = state => state.diffFiles.some(file => file.collapsed); export const commitId = state => (state.commit && state.commit.id ? state.commit.id : null); diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb index f90971bb9f6..d3774746cb8 100644 --- a/app/finders/snippets_finder.rb +++ b/app/finders/snippets_finder.rb @@ -1,138 +1,149 @@ # frozen_string_literal: true -# Snippets Finder +# Finder for retrieving snippets that a user can see, optionally scoped to a +# project or snippets author. # -# Used to filter Snippets collections by a set of params +# Basic usage: # -# Arguments. +# user = User.find(1) # -# current_user - The current user, nil also can be used. -# params: -# visibility (integer) - Individual snippet visibility: Public(20), internal(10) or private(0). -# project (Project) - Project related. -# author (User) - Author related. +# SnippetsFinder.new(user).execute # -# params are optional +# To limit the snippets to a specific project, supply the `project:` option: +# +# user = User.find(1) +# project = Project.find(1) +# +# SnippetsFinder.new(user, project: project).execute +# +# Limiting snippets to an author can be done by supplying the `author:` option: +# +# user = User.find(1) +# project = Project.find(1) +# +# SnippetsFinder.new(user, author: user).execute +# +# To filter snippets using a specific visibility level, you can provide the +# `scope:` option: +# +# user = User.find(1) +# project = Project.find(1) +# +# SnippetsFinder.new(user, author: user, scope: :are_public).execute +# +# Valid `scope:` values are: +# +# * `:are_private` +# * `:are_internal` +# * `:are_public` +# +# Any other value will be ignored. class SnippetsFinder < UnionFinder - include Gitlab::Allowable include FinderMethods - attr_accessor :current_user, :project, :params + attr_accessor :current_user, :project, :author, :scope - def initialize(current_user, params = {}) + def initialize(current_user = nil, params = {}) @current_user = current_user - @params = params @project = params[:project] - end - - def execute - items = init_collection - items = by_author(items) - items = by_visibility(items) - - items.fresh - end - - private - - def init_collection - if project.present? - authorized_snippets_from_project - else - authorized_snippets + @author = params[:author] + @scope = params[:scope].to_s + + if project && author + raise( + ArgumentError, + 'Filtering by both an author and a project is not supported, ' \ + 'as this finder is not optimised for this use case' + ) end end - def authorized_snippets_from_project - if can?(current_user, :read_project_snippet, project) - if project.team.member?(current_user) - project.snippets + def execute + base = + if project + snippets_for_a_single_project else - project.snippets.public_to_user(current_user) + snippets_for_multiple_projects end - else - Snippet.none - end - end - # rubocop: disable CodeReuse/ActiveRecord - def authorized_snippets - # This query was intentionally converted to a raw one to get it work in Rails 5.0. - # In Rails 5.0 and 5.1 there's a bug: https://github.com/rails/arel/issues/531 - # Please convert it back when on rails 5.2 as it works again as expected since 5.2. - Snippet.where("#{feature_available_projects} OR #{not_project_related}") - .public_or_visible_to_user(current_user) + base.with_optional_visibility(visibility_from_scope).fresh end - # rubocop: enable CodeReuse/ActiveRecord - # Returns a collection of projects that is either public or visible to the - # logged in user. + # Produces a query that retrieves snippets from multiple projects. # - # A caller must pass in a block to modify individual parts of - # the query, e.g. to apply .with_feature_available_for_user on top of it. - # This is useful for performance as we can stick those additional filters - # at the bottom of e.g. the UNION. - # rubocop: disable CodeReuse/ActiveRecord - def projects_for_user - return yield(Project.public_to_user) unless current_user - - # If the current_user is allowed to see all projects, - # we can shortcut and just return. - return yield(Project.all) if current_user.full_private_access? - - authorized_projects = yield(Project.where('EXISTS (?)', current_user.authorizations_for_projects)) - - levels = Gitlab::VisibilityLevel.levels_for_user(current_user) - visible_projects = yield(Project.where(visibility_level: levels)) - - # We use a UNION here instead of OR clauses since this results in better - # performance. - Project.from_union([authorized_projects, visible_projects]) - end - # rubocop: enable CodeReuse/ActiveRecord - - def feature_available_projects - # Don't return any project related snippets if the user cannot read cross project - return table[:id].eq(nil).to_sql unless Ability.allowed?(current_user, :read_cross_project) - - projects = projects_for_user do |part| - part.with_feature_available_for_user(:snippets, current_user) - end.select(:id) + # The resulting query will, depending on the user's permissions, include the + # following collections of snippets: + # + # 1. Snippets that don't belong to any project. + # 2. Snippets of projects that are visible to the current user (e.g. snippets + # in public projects). + # 3. Snippets of projects that the current user is a member of. + # + # Each collection is constructed in isolation, allowing for greater control + # over the resulting SQL query. + def snippets_for_multiple_projects + queries = [global_snippets] + + if Ability.allowed?(current_user, :read_cross_project) + queries << snippets_of_visible_projects + queries << snippets_of_authorized_projects if current_user + end - # This query was intentionally converted to a raw one to get it work in Rails 5.0. - # In Rails 5.0 and 5.1 there's a bug: https://github.com/rails/arel/issues/531 - # Please convert it back when on rails 5.2 as it works again as expected since 5.2. - "snippets.project_id IN (#{projects.to_sql})" + find_union(queries, Snippet) end - def not_project_related - table[:project_id].eq(nil).to_sql + def snippets_for_a_single_project + Snippet.for_project_with_user(project, current_user) end - def table - Snippet.arel_table + def global_snippets + snippets_for_author_or_visible_to_user.only_global_snippets end - # rubocop: disable CodeReuse/ActiveRecord - def by_visibility(items) - visibility = params[:visibility] || visibility_from_scope + # Returns the snippets that the current user (logged in or not) can view. + def snippets_of_visible_projects + snippets_for_author_or_visible_to_user + .only_include_projects_visible_to(current_user) + .only_include_projects_with_snippets_enabled + end - return items unless visibility + # Returns the snippets that the currently logged in user has access to by + # being a member of the project the snippets belong to. + # + # This method requires that `current_user` returns a `User` instead of `nil`, + # and is optimised for this specific scenario. + def snippets_of_authorized_projects + base = author ? snippets_for_author : Snippet.all + + base + .only_include_projects_with_snippets_enabled(include_private: true) + .only_include_authorized_projects(current_user) + end - items.where(visibility_level: visibility) + def snippets_for_author_or_visible_to_user + if author + snippets_for_author + elsif current_user + Snippet.visible_to_or_authored_by(current_user) + else + Snippet.public_to_user + end end - # rubocop: enable CodeReuse/ActiveRecord - # rubocop: disable CodeReuse/ActiveRecord - def by_author(items) - return items unless params[:author] + def snippets_for_author + base = author.snippets - items.where(author_id: params[:author].id) + if author == current_user + # If the current user is also the author of all snippets, then we can + # include private snippets. + base + else + base.public_to_user(current_user) + end end - # rubocop: enable CodeReuse/ActiveRecord def visibility_from_scope - case params[:scope].to_s + case scope when 'are_private' Snippet::PRIVATE when 'are_internal' diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 9a1c2a4c9e1..086bb38ce9a 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -217,7 +217,8 @@ module ApplicationSettingsHelper :user_oauth_applications, :version_check_enabled, :web_ide_clientside_preview_enabled, - :diff_max_patch_bytes + :diff_max_patch_bytes, + :commit_email_hostname ] end diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb index 7fc4c1a023f..5906ddabee4 100644 --- a/app/helpers/avatars_helper.rb +++ b/app/helpers/avatars_helper.rb @@ -22,7 +22,7 @@ module AvatarsHelper end def avatar_icon_for_email(email = nil, size = nil, scale = 2, only_path: true) - user = User.find_by_any_email(email.try(:downcase)) + user = User.find_by_any_email(email) if user avatar_icon_for_user(user, size, scale, only_path: only_path) else diff --git a/app/helpers/profiles_helper.rb b/app/helpers/profiles_helper.rb index 55674e37a34..42f9a1213e9 100644 --- a/app/helpers/profiles_helper.rb +++ b/app/helpers/profiles_helper.rb @@ -1,6 +1,20 @@ # frozen_string_literal: true module ProfilesHelper + def commit_email_select_options(user) + private_email = user.private_commit_email + verified_emails = user.verified_emails - [private_email] + + [ + [s_("Profiles|Use a private email - %{email}").html_safe % { email: private_email }, Gitlab::PrivateCommitEmail::TOKEN], + verified_emails + ] + end + + def selected_commit_email(user) + user.read_attribute(:commit_email) || user.commit_email + end + def attribute_provider_label(attribute) user_synced_attributes_metadata = current_user.user_synced_attributes_metadata if user_synced_attributes_metadata&.synced?(attribute) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 704310f53f0..207ffae873a 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -187,6 +187,8 @@ class ApplicationSetting < ActiveRecord::Base validates :user_default_internal_regex, js_regex: true, allow_nil: true + validates :commit_email_hostname, format: { with: /\A[^@]+\z/ } + validates :archive_builds_in_seconds, allow_nil: true, numericality: { only_integer: true, greater_than_or_equal_to: 1.day.seconds } @@ -299,10 +301,15 @@ class ApplicationSetting < ActiveRecord::Base user_default_internal_regex: nil, user_show_add_ssh_key_message: true, usage_stats_set_by_user_id: nil, - diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES + diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES, + commit_email_hostname: default_commit_email_hostname } end + def self.default_commit_email_hostname + "users.noreply.#{Gitlab.config.gitlab.host}" + end + def self.create_from_defaults create(defaults) end @@ -358,6 +365,10 @@ class ApplicationSetting < ActiveRecord::Base Array(read_attribute(:repository_storages)) end + def commit_email_hostname + super.presence || self.class.default_commit_email_hostname + end + def default_project_visibility=(level) super(Gitlab::VisibilityLevel.level_value(level)) end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 25596581d0f..889f8ce27a6 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -211,6 +211,7 @@ module Ci build.deployment&.succeed build.run_after_commit do + BuildSuccessWorker.perform_async(id) PagesWorker.perform_async(:deploy, id) if build.pages_generator? end end @@ -220,9 +221,7 @@ module Ci build.deployment&.drop - next if build.retries_max.zero? - - if build.retries_count < build.retries_max + if build.retry_failure? begin Ci::Build.retry(build, build.user) rescue Gitlab::Access::AccessDeniedError => ex @@ -320,7 +319,17 @@ module Ci end def retries_max - self.options.to_h.fetch(:retry, 0).to_i + normalized_retry.fetch(:max, 0) + end + + def retry_when + normalized_retry.fetch(:when, ['always']) + end + + def retry_failure? + return false if retries_max.zero? || retries_count >= retries_max + + retry_when.include?('always') || retry_when.include?(failure_reason.to_s) end def latest? @@ -885,6 +894,16 @@ module Ci options&.dig(:environment, :url) || persisted_environment&.external_url end + # The format of the retry option changed in GitLab 11.5: Before it was + # integer only, after it is a hash. New builds are created with the new + # format, but builds created before GitLab 11.5 and saved in database still + # have the old integer only format. This method returns the retry option + # normalized as a hash in 11.5+ format. + def normalized_retry + value = options&.dig(:retry) + value.is_a?(Integer) ? { max: value } : value.to_h + end + def build_attributes_from_config return {} unless pipeline.config_processor diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb new file mode 100644 index 00000000000..8adb99fcb04 --- /dev/null +++ b/app/models/clusters/applications/knative.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Clusters + module Applications + class Knative < ActiveRecord::Base + VERSION = '0.1.3'.freeze + REPOSITORY = 'https://storage.googleapis.com/triggermesh-charts'.freeze + + # This is required for helm version <= 2.10.x in order to support + # Setting up CRDs + ISTIO_CRDS = 'https://storage.googleapis.com/triggermesh-charts/istio-crds.yaml'.freeze + + self.table_name = 'clusters_applications_knative' + + include ::Clusters::Concerns::ApplicationCore + include ::Clusters::Concerns::ApplicationStatus + include ::Clusters::Concerns::ApplicationVersion + include ::Clusters::Concerns::ApplicationData + + default_value_for :version, VERSION + + validates :hostname, presence: true, hostname: true + + def chart + 'knative/knative' + end + + def values + { "domain" => hostname }.to_yaml + end + + def install_command + Gitlab::Kubernetes::Helm::InstallCommand.new( + name: name, + version: VERSION, + rbac: cluster.platform_kubernetes_rbac?, + chart: chart, + files: files, + repository: REPOSITORY, + preinstall: install_script + ) + end + + private + + def install_script + ["/usr/bin/kubectl apply -f #{ISTIO_CRDS} >/dev/null"] + end + end + end +end diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index e80d35d0f3c..48d6c0daa0f 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -12,7 +12,8 @@ module Clusters Applications::Ingress.application_name => Applications::Ingress, Applications::Prometheus.application_name => Applications::Prometheus, Applications::Runner.application_name => Applications::Runner, - Applications::Jupyter.application_name => Applications::Jupyter + Applications::Jupyter.application_name => Applications::Jupyter, + Applications::Knative.application_name => Applications::Knative }.freeze DEFAULT_ENVIRONMENT = '*'.freeze @@ -35,6 +36,7 @@ module Clusters has_one :application_prometheus, class_name: 'Clusters::Applications::Prometheus' has_one :application_runner, class_name: 'Clusters::Applications::Runner' has_one :application_jupyter, class_name: 'Clusters::Applications::Jupyter' + has_one :application_knative, class_name: 'Clusters::Applications::Knative' has_many :kubernetes_namespaces has_one :kubernetes_namespace, -> { order(id: :desc) }, class_name: 'Clusters::KubernetesNamespace' @@ -100,7 +102,8 @@ module Clusters application_ingress || build_application_ingress, application_prometheus || build_application_prometheus, application_runner || build_application_runner, - application_jupyter || build_application_jupyter + application_jupyter || build_application_jupyter, + application_knative || build_application_knative ] end diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index 008e08d9914..d69038be532 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -26,6 +26,7 @@ module Clusters algorithm: 'aes-256-cbc' before_validation :enforce_namespace_to_lower_case + before_validation :enforce_ca_whitespace_trimming validates :namespace, allow_blank: true, @@ -201,6 +202,11 @@ module Clusters self.namespace = self.namespace&.downcase end + def enforce_ca_whitespace_trimming + self.ca_pem = self.ca_pem&.strip + self.token = self.token&.strip + end + def prevent_modification return unless managed? diff --git a/app/models/commit.rb b/app/models/commit.rb index a61ed03cf35..9dd0cbacd9e 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -260,7 +260,7 @@ class Commit request_cache(:author) { author_email.downcase } def committer - @committer ||= User.find_by_any_email(committer_email.downcase) + @committer ||= User.find_by_any_email(committer_email) end def parents diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 54a900a3b85..83434276995 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -10,7 +10,9 @@ class Deployment < ActiveRecord::Base belongs_to :user belongs_to :deployable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations - has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.deployments&.maximum(:iid) } + has_internal_id :iid, scope: :project, init: ->(s) do + Deployment.where(project: s.project).maximum(:iid) if s&.project + end validates :sha, presence: true validates :ref, presence: true diff --git a/app/models/environment.rb b/app/models/environment.rb index 7d104bb0c25..934828946b9 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -50,6 +50,7 @@ class Environment < ActiveRecord::Base scope :in_review_folder, -> { where(environment_type: "review") } scope :for_name, -> (name) { where(name: name) } scope :for_project, -> (project) { where(project_id: project) } + scope :with_deployment, -> (sha) { where('EXISTS (?)', Deployment.select(1).where('deployments.environment_id = environments.id').where(sha: sha)) } state_machine :state, initial: :available do event :start do diff --git a/app/models/environment_status.rb b/app/models/environment_status.rb index 7efc8da09ad..7078496ff52 100644 --- a/app/models/environment_status.rb +++ b/app/models/environment_status.rb @@ -8,17 +8,16 @@ class EnvironmentStatus delegate :id, to: :environment delegate :name, to: :environment delegate :project, to: :environment - delegate :status, to: :deployment, allow_nil: true delegate :deployed_at, to: :deployment, allow_nil: true def self.for_merge_request(mr, user) - build_environments_status(mr, user, mr.head_pipeline) + build_environments_status(mr, user, mr.diff_head_sha) end def self.after_merge_request(mr, user) return [] unless mr.merged? - build_environments_status(mr, user, mr.merge_pipeline) + build_environments_status(mr, user, mr.merge_commit_sha) end def initialize(environment, merge_request, sha) @@ -29,7 +28,7 @@ class EnvironmentStatus def deployment strong_memoize(:deployment) do - environment.first_deployment_for(sha) + Deployment.where(environment: environment).find_by_sha(sha) end end @@ -44,6 +43,22 @@ class EnvironmentStatus .merge_request_diff_files.where(deleted_file: false) end + ## + # Since frontend has not supported all statuses yet, BE has to + # proxy some status to a supported status. + def status + return unless deployment + + case deployment.status + when 'created' + 'running' + when 'canceled' + 'failed' + else + deployment.status + end + end + private PAGE_EXTENSIONS = /\A\.(s?html?|php|asp|cgi|pl)\z/i.freeze @@ -61,21 +76,14 @@ class EnvironmentStatus } end - def self.build_environments_status(mr, user, pipeline) - return [] unless pipeline.present? + def self.build_environments_status(mr, user, sha) + Environment.where(project_id: [mr.source_project_id, mr.target_project_id]) + .available + .with_deployment(sha).map do |environment| + next unless Ability.allowed?(user, :read_environment, environment) - find_environments(user, pipeline).map do |environment| - EnvironmentStatus.new(environment, mr, pipeline.sha) - end + EnvironmentStatus.new(environment, mr, sha) + end.compact end private_class_method :build_environments_status - - def self.find_environments(user, pipeline) - env_ids = Deployment.where(deployable: pipeline.builds).select(:environment_id) - - Environment.available.where(id: env_ids).select do |environment| - Ability.allowed?(user, :read_environment, environment) - end - end - private_class_method :find_environments end diff --git a/app/models/project.rb b/app/models/project.rb index d5a4ae79c47..48905547ab4 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -2073,6 +2073,10 @@ class Project < ActiveRecord::Base storage_version != LATEST_STORAGE_VERSION end + def snippets_visible?(user = nil) + Ability.allowed?(user, :read_project_snippet, self) + end + private def use_hashed_storage diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 1c5846b4023..11856b55902 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -63,6 +63,62 @@ class Snippet < ActiveRecord::Base attr_spammable :title, spam_title: true attr_spammable :content, spam_description: true + def self.with_optional_visibility(value = nil) + if value + where(visibility_level: value) + else + all + end + end + + def self.only_global_snippets + where(project_id: nil) + end + + def self.only_include_projects_visible_to(current_user = nil) + levels = Gitlab::VisibilityLevel.levels_for_user(current_user) + + joins(:project).where('projects.visibility_level IN (?)', levels) + end + + def self.only_include_projects_with_snippets_enabled(include_private: false) + column = ProjectFeature.access_level_attribute(:snippets) + levels = [ProjectFeature::ENABLED, ProjectFeature::PUBLIC] + + levels << ProjectFeature::PRIVATE if include_private + + joins(project: :project_feature) + .where(project_features: { column => levels }) + end + + def self.only_include_authorized_projects(current_user) + where( + 'EXISTS (?)', + ProjectAuthorization + .select(1) + .where('project_id = snippets.project_id') + .where(user_id: current_user.id) + ) + end + + def self.for_project_with_user(project, user = nil) + return none unless project.snippets_visible?(user) + + if user && project.team.member?(user) + project.snippets + else + project.snippets.public_to_user(user) + end + end + + def self.visible_to_or_authored_by(user) + where( + 'snippets.visibility_level IN (?) OR snippets.author_id = ?', + Gitlab::VisibilityLevel.levels_for_user(user), + user.id + ) + end + def self.reference_prefix '$' end @@ -81,27 +137,6 @@ class Snippet < ActiveRecord::Base @link_reference_pattern ||= super("snippets", /(?<snippet>\d+)/) end - # Returns a collection of snippets that are either public or visible to the - # logged in user. - # - # This method does not verify the user actually has the access to the project - # the snippet is in, so it should be only used on a relation that's already scoped - # for project access - def self.public_or_visible_to_user(user = nil) - if user - authorized = user - .project_authorizations - .select(1) - .where('project_authorizations.project_id = snippets.project_id') - - levels = Gitlab::VisibilityLevel.levels_for_user(user) - - where('EXISTS (?) OR snippets.visibility_level IN (?) or snippets.author_id = (?)', authorized, levels, user.id) - else - public_to_user - end - end - def to_reference(from = nil, full: false) reference = "#{self.class.reference_prefix}#{id}" diff --git a/app/models/user.rb b/app/models/user.rb index 039a3854edb..a400058e87e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -347,7 +347,11 @@ class User < ActiveRecord::Base # Find a User by their primary email or any associated secondary email def find_by_any_email(email, confirmed: false) - by_any_email(email, confirmed: confirmed).take + return unless email + + downcased = email.downcase + + find_by_private_commit_email(downcased) || by_any_email(downcased, confirmed: confirmed).take end # Returns a relation containing all the users for the given Email address @@ -361,6 +365,12 @@ class User < ActiveRecord::Base from_union([users, emails]) end + def find_by_private_commit_email(email) + user_id = Gitlab::PrivateCommitEmail.user_id_for_email(email) + + find_by(id: user_id) + end + def filter(filter_name) case filter_name when 'admins' @@ -633,6 +643,10 @@ class User < ActiveRecord::Base def commit_email return self.email unless has_attribute?(:commit_email) + if super == Gitlab::PrivateCommitEmail::TOKEN + return private_commit_email + end + # The commit email is the same as the primary email if undefined super.presence || self.email end @@ -645,6 +659,10 @@ class User < ActiveRecord::Base has_attribute?(:commit_email) && super end + def private_commit_email + Gitlab::PrivateCommitEmail.for_user(self) + end + # see if the new email is already a verified secondary email def check_for_verified_email skip_reconfirmation! if emails.confirmed.where(email: self.email).any? @@ -1020,13 +1038,21 @@ class User < ActiveRecord::Base def verified_emails verified_emails = [] verified_emails << email if primary_email_verified? + verified_emails << private_commit_email verified_emails.concat(emails.confirmed.pluck(:email)) verified_emails end def verified_email?(check_email) downcased = check_email.downcase - email == downcased ? primary_email_verified? : emails.confirmed.where(email: downcased).exists? + + if email == downcased + primary_email_verified? + else + user_id = Gitlab::PrivateCommitEmail.user_id_for_email(downcased) + + user_id == id || emails.confirmed.where(email: downcased).exists? + end end def hook_attrs diff --git a/app/services/clusters/applications/create_service.rb b/app/services/clusters/applications/create_service.rb index 55f917798de..c348cad4803 100644 --- a/app/services/clusters/applications/create_service.rb +++ b/app/services/clusters/applications/create_service.rb @@ -45,7 +45,8 @@ module Clusters "ingress" => -> (cluster) { cluster.application_ingress || cluster.build_application_ingress }, "prometheus" => -> (cluster) { cluster.application_prometheus || cluster.build_application_prometheus }, "runner" => -> (cluster) { cluster.application_runner || cluster.build_application_runner }, - "jupyter" => -> (cluster) { cluster.application_jupyter || cluster.build_application_jupyter } + "jupyter" => -> (cluster) { cluster.application_jupyter || cluster.build_application_jupyter }, + "knative" => -> (cluster) { cluster.application_knative || cluster.build_application_knative } } end diff --git a/app/views/admin/application_settings/_email.html.haml b/app/views/admin/application_settings/_email.html.haml index 86339e61215..60a6be731ea 100644 --- a/app/views/admin/application_settings/_email.html.haml +++ b/app/views/admin/application_settings/_email.html.haml @@ -20,5 +20,11 @@ By default GitLab sends emails in HTML and plain text formats so mail clients can choose what format to use. Disable this option if you only want to send emails in plain text format. + .form-group + = f.label :commit_email_hostname, _('Custom hostname (for private commit emails)'), class: 'label-bold' + = f.text_field :commit_email_hostname, class: 'form-control' + .form-text.text-muted + - commit_email_hostname_docs_link = link_to _('Learn more'), help_page_path('user/admin_area/settings/email', anchor: 'custom-private-commit-email-hostname'), target: '_blank' + = _("This setting will update the hostname that is used to generate private commit emails. %{learn_more}").html_safe % { learn_more: commit_email_hostname_docs_link } = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/clusters/clusters/show.html.haml b/app/views/clusters/clusters/show.html.haml index 1e1157c34bd..7ea85fe43d6 100644 --- a/app/views/clusters/clusters/show.html.haml +++ b/app/views/clusters/clusters/show.html.haml @@ -13,6 +13,7 @@ install_prometheus_path: clusterable.install_applications_cluster_path(@cluster, :prometheus), install_runner_path: clusterable.install_applications_cluster_path(@cluster, :runner), install_jupyter_path: clusterable.install_applications_cluster_path(@cluster, :jupyter), + install_knative_path: clusterable.install_applications_cluster_path(@cluster, :knative), toggle_status: @cluster.enabled? ? 'true': 'false', cluster_status: @cluster.status_name, cluster_status_reason: @cluster.status_reason, diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index ea215e3e718..2603c558c0f 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -91,8 +91,9 @@ = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), { help: s_("Profiles|This email will be displayed on your public profile."), include_blank: s_("Profiles|Do not show on profile") }, control_class: 'select2' - = f.select :commit_email, options_for_select(@user.verified_emails, selected: @user.commit_email), - { help: 'This email will be used for web based operations, such as edits and merges.' }, + - commit_email_docs_link = link_to s_('Profiles|Learn more'), help_page_path('user/profile/index', anchor: 'commit-email', target: '_blank') + = f.select :commit_email, options_for_select(commit_email_select_options(@user), selected: selected_commit_email(@user)), + { help: s_("Profiles|This email will be used for web based operations, such as edits and merges. %{learn_more}").html_safe % { learn_more: commit_email_docs_link } }, control_class: 'select2' = f.select :preferred_language, Gitlab::I18n::AVAILABLE_LANGUAGES.map { |value, label| [label, value] }, { help: s_("Profiles|This feature is experimental and translations are not complete yet.") }, diff --git a/changelogs/unreleased/43521-keep-personal-emails-private.yml b/changelogs/unreleased/43521-keep-personal-emails-private.yml new file mode 100644 index 00000000000..0f0bede6482 --- /dev/null +++ b/changelogs/unreleased/43521-keep-personal-emails-private.yml @@ -0,0 +1,5 @@ +--- +title: Adds option to override commit email with a noreply private email +merge_request: 22560 +author: +type: added diff --git a/changelogs/unreleased/add-action-to-deployment.yml b/changelogs/unreleased/add-action-to-deployment.yml new file mode 100644 index 00000000000..4629f762ae8 --- /dev/null +++ b/changelogs/unreleased/add-action-to-deployment.yml @@ -0,0 +1,5 @@ +--- +title: Fix environment status in merge request widget +merge_request: 22799 +author: +type: changed diff --git a/changelogs/unreleased/diff-expand-all-button.yml b/changelogs/unreleased/diff-expand-all-button.yml new file mode 100644 index 00000000000..77600e726d5 --- /dev/null +++ b/changelogs/unreleased/diff-expand-all-button.yml @@ -0,0 +1,5 @@ +--- +title: Show expand all diffs button when a single diff file is collapsed +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/dm-api-merge-requests-index-merged-at.yml b/changelogs/unreleased/dm-api-merge-requests-index-merged-at.yml new file mode 100644 index 00000000000..8e02a9019df --- /dev/null +++ b/changelogs/unreleased/dm-api-merge-requests-index-merged-at.yml @@ -0,0 +1,5 @@ +--- +title: Expose {closed,merged}_{at,by} in merge requests API index +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/introduce-knative-support.yml b/changelogs/unreleased/introduce-knative-support.yml new file mode 100644 index 00000000000..53290d71977 --- /dev/null +++ b/changelogs/unreleased/introduce-knative-support.yml @@ -0,0 +1,5 @@ +--- +title: Introduce Knative support +author: Chris Baumbauer +merge_request: 43959 +type: added diff --git a/changelogs/unreleased/max_retries_when.yml b/changelogs/unreleased/max_retries_when.yml new file mode 100644 index 00000000000..dad3cd8a123 --- /dev/null +++ b/changelogs/unreleased/max_retries_when.yml @@ -0,0 +1,5 @@ +--- +title: Allow to configure when to retry failed CI jobs +merge_request: 21758 +author: Markus Doits +type: added diff --git a/changelogs/unreleased/refactor-snippets-finder.yml b/changelogs/unreleased/refactor-snippets-finder.yml new file mode 100644 index 00000000000..37cacf71c14 --- /dev/null +++ b/changelogs/unreleased/refactor-snippets-finder.yml @@ -0,0 +1,5 @@ +--- +title: Rewrite SnippetsFinder to improve performance by a factor of 1500 +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/remove-asset-sync.yml b/changelogs/unreleased/remove-asset-sync.yml new file mode 100644 index 00000000000..ddb82212975 --- /dev/null +++ b/changelogs/unreleased/remove-asset-sync.yml @@ -0,0 +1,5 @@ +--- +title: Remove asset_sync gem from Gemfile and related code from codebase +merge_request: 22610 +author: +type: other diff --git a/config/initializers/asset_sync.rb b/config/initializers/asset_sync.rb deleted file mode 100644 index 7f3934853fa..00000000000 --- a/config/initializers/asset_sync.rb +++ /dev/null @@ -1,31 +0,0 @@ -AssetSync.configure do |config| - # Disable the asset_sync gem by default. If it is enabled, but not configured, - # asset_sync will cause the build to fail. - config.enabled = if ENV.has_key?('ASSET_SYNC_ENABLED') - ENV['ASSET_SYNC_ENABLED'] == 'true' - else - false - end - - # Pulled from https://github.com/AssetSync/asset_sync/blob/v2.2.0/lib/asset_sync/engine.rb#L15-L40 - # This allows us to disable asset_sync by default and configure through environment variables - # Updates to asset_sync gem should be checked - config.fog_provider = ENV['FOG_PROVIDER'] if ENV.has_key?('FOG_PROVIDER') - config.fog_directory = ENV['FOG_DIRECTORY'] if ENV.has_key?('FOG_DIRECTORY') - config.fog_region = ENV['FOG_REGION'] if ENV.has_key?('FOG_REGION') - - config.aws_access_key_id = ENV['ASSETS_AWS_ACCESS_KEY_ID'] if ENV.has_key?('ASSETS_AWS_ACCESS_KEY_ID') - config.aws_secret_access_key = ENV['ASSETS_AWS_SECRET_ACCESS_KEY'] if ENV.has_key?('ASSETS_AWS_SECRET_ACCESS_KEY') - config.aws_reduced_redundancy = ENV['AWS_REDUCED_REDUNDANCY'] == true if ENV.has_key?('AWS_REDUCED_REDUNDANCY') - - config.rackspace_username = ENV['RACKSPACE_USERNAME'] if ENV.has_key?('RACKSPACE_USERNAME') - config.rackspace_api_key = ENV['RACKSPACE_API_KEY'] if ENV.has_key?('RACKSPACE_API_KEY') - - config.google_storage_access_key_id = ENV['GOOGLE_STORAGE_ACCESS_KEY_ID'] if ENV.has_key?('GOOGLE_STORAGE_ACCESS_KEY_ID') - config.google_storage_secret_access_key = ENV['GOOGLE_STORAGE_SECRET_ACCESS_KEY'] if ENV.has_key?('GOOGLE_STORAGE_SECRET_ACCESS_KEY') - - config.existing_remote_files = ENV['ASSET_SYNC_EXISTING_REMOTE_FILES'] || "keep" - - config.gzip_compression = (ENV['ASSET_SYNC_GZIP_COMPRESSION'] == 'true') if ENV.has_key?('ASSET_SYNC_GZIP_COMPRESSION') - config.manifest = (ENV['ASSET_SYNC_MANIFEST'] == 'true') if ENV.has_key?('ASSET_SYNC_MANIFEST') -end diff --git a/db/migrate/20180912111628_add_knative_application.rb b/db/migrate/20180912111628_add_knative_application.rb new file mode 100644 index 00000000000..bfda6a945a7 --- /dev/null +++ b/db/migrate/20180912111628_add_knative_application.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class AddKnativeApplication < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + create_table "clusters_applications_knative" do |t| + t.references :cluster, null: false, unique: true, foreign_key: { on_delete: :cascade } + + t.datetime_with_timezone "created_at", null: false + t.datetime_with_timezone "updated_at", null: false + t.integer "status", null: false + t.string "version", null: false + t.string "hostname" + t.text "status_reason" + end + end +end diff --git a/db/migrate/20181025115728_add_private_commit_email_hostname_to_application_settings.rb b/db/migrate/20181025115728_add_private_commit_email_hostname_to_application_settings.rb new file mode 100644 index 00000000000..89ddaf2ae2b --- /dev/null +++ b/db/migrate/20181025115728_add_private_commit_email_hostname_to_application_settings.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class AddPrivateCommitEmailHostnameToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column(:application_settings, :commit_email_hostname, :string, null: true) + end +end diff --git a/db/migrate/20181026143227_migrate_snippets_access_level_default_value.rb b/db/migrate/20181026143227_migrate_snippets_access_level_default_value.rb new file mode 100644 index 00000000000..ede0ee27b8a --- /dev/null +++ b/db/migrate/20181026143227_migrate_snippets_access_level_default_value.rb @@ -0,0 +1,42 @@ +# 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 MigrateSnippetsAccessLevelDefaultValue < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + ENABLED = 20 + + disable_ddl_transaction! + + class ProjectFeature < ActiveRecord::Base + include EachBatch + + self.table_name = 'project_features' + end + + def up + change_column_default :project_features, :snippets_access_level, ENABLED + + # On GitLab.com this will update about 28 000 rows. Since our updates are + # very small and this column is not indexed, these updates should be very + # lightweight. + ProjectFeature.where(snippets_access_level: nil).each_batch do |batch| + batch.update_all(snippets_access_level: ENABLED) + end + + # We do not need to perform this in a post-deployment migration as the + # ProjectFeature model already enforces a default value for all new rows. + change_column_null :project_features, :snippets_access_level, false + end + + def down + change_column_null :project_features, :snippets_access_level, true + change_column_default :project_features, :snippets_access_level, nil + + # We can't migrate from 20 -> NULL, as some projects may have explicitly set + # the access level to 20. + end +end diff --git a/db/migrate/20181106135939_add_index_to_deployments.rb b/db/migrate/20181106135939_add_index_to_deployments.rb new file mode 100644 index 00000000000..5f988a4723c --- /dev/null +++ b/db/migrate/20181106135939_add_index_to_deployments.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddIndexToDeployments < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index :deployments, [:project_id, :status, :created_at] + end + + def down + remove_concurrent_index :deployments, [:project_id, :status, :created_at] + end +end diff --git a/db/schema.rb b/db/schema.rb index 7509941325f..56137caf1d7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -166,6 +166,7 @@ ActiveRecord::Schema.define(version: 20181107054254) do t.integer "receive_max_input_size" t.integer "diff_max_patch_bytes", default: 102400, null: false t.integer "archive_builds_in_seconds" + t.string "commit_email_hostname" end create_table "audit_events", force: :cascade do |t| @@ -704,6 +705,16 @@ ActiveRecord::Schema.define(version: 20181107054254) do t.text "status_reason" end + create_table "clusters_applications_knative", force: :cascade do |t| + t.integer "cluster_id", null: false + t.datetime_with_timezone "created_at", null: false + t.datetime_with_timezone "updated_at", null: false + t.integer "status", null: false + t.string "version", null: false + t.string "hostname" + t.text "status_reason" + end + create_table "clusters_applications_prometheus", force: :cascade do |t| t.integer "cluster_id", null: false t.integer "status", null: false @@ -836,6 +847,7 @@ ActiveRecord::Schema.define(version: 20181107054254) do add_index "deployments", ["environment_id", "status"], name: "index_deployments_on_environment_id_and_status", using: :btree add_index "deployments", ["id"], name: "partial_index_deployments_for_legacy_successful_deployments", where: "((finished_at IS NULL) AND (status = 2))", using: :btree add_index "deployments", ["project_id", "iid"], name: "index_deployments_on_project_id_and_iid", unique: true, using: :btree + add_index "deployments", ["project_id", "status", "created_at"], name: "index_deployments_on_project_id_and_status_and_created_at", using: :btree add_index "deployments", ["project_id", "status"], name: "index_deployments_on_project_id_and_status", using: :btree create_table "emails", force: :cascade do |t| @@ -1631,7 +1643,7 @@ ActiveRecord::Schema.define(version: 20181107054254) do t.integer "merge_requests_access_level" t.integer "issues_access_level" t.integer "wiki_access_level" - t.integer "snippets_access_level" + t.integer "snippets_access_level", default: 20, null: false t.integer "builds_access_level" t.datetime "created_at" t.datetime "updated_at" @@ -2420,6 +2432,7 @@ ActiveRecord::Schema.define(version: 20181107054254) do add_foreign_key "clusters_applications_ingress", "clusters", name: "fk_753a7b41c1", on_delete: :cascade add_foreign_key "clusters_applications_jupyter", "clusters", on_delete: :cascade add_foreign_key "clusters_applications_jupyter", "oauth_applications", on_delete: :nullify + add_foreign_key "clusters_applications_knative", "clusters", on_delete: :cascade add_foreign_key "clusters_applications_prometheus", "clusters", name: "fk_557e773639", on_delete: :cascade add_foreign_key "clusters_applications_runners", "ci_runners", column: "runner_id", name: "fk_02de2ded36", on_delete: :nullify add_foreign_key "clusters_applications_runners", "clusters", on_delete: :cascade diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index f3cfe0ad218..9cb3f0d9c0c 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -57,7 +57,18 @@ Parameters: "project_id": 3, "title": "test1", "description": "fixed login page css paddings", - "state": "opened", + "state": "merged", + "merged_by": { + "id": 87854, + "name": "Douwe Maan", + "username": "DouweM", + "state": "active", + "avatar_url": "https://gitlab.example.com/uploads/-/system/user/avatar/87854/avatar.png", + "web_url": "https://gitlab.com/DouweM" + }, + "merged_at": "2018-09-07T11:16:17.520Z", + "closed_by": null, + "closed_at": null, "created_at": "2017-04-29T08:46:00Z", "updated_at": "2017-04-29T08:46:00Z", "target_branch": "master", @@ -180,7 +191,18 @@ Parameters: "project_id": 3, "title": "test1", "description": "fixed login page css paddings", - "state": "opened", + "state": "merged", + "merged_by": { + "id": 87854, + "name": "Douwe Maan", + "username": "DouweM", + "state": "active", + "avatar_url": "https://gitlab.example.com/uploads/-/system/user/avatar/87854/avatar.png", + "web_url": "https://gitlab.com/DouweM" + }, + "merged_at": "2018-09-07T11:16:17.520Z", + "closed_by": null, + "closed_at": null, "created_at": "2017-04-29T08:46:00Z", "updated_at": "2017-04-29T08:46:00Z", "target_branch": "master", @@ -293,7 +315,18 @@ Parameters: "project_id": 3, "title": "test1", "description": "fixed login page css paddings", - "state": "opened", + "state": "merged", + "merged_by": { + "id": 87854, + "name": "Douwe Maan", + "username": "DouweM", + "state": "active", + "avatar_url": "https://gitlab.example.com/uploads/-/system/user/avatar/87854/avatar.png", + "web_url": "https://gitlab.com/DouweM" + }, + "merged_at": "2018-09-07T11:16:17.520Z", + "closed_by": null, + "closed_at": null, "created_at": "2017-04-29T08:46:00Z", "updated_at": "2017-04-29T08:46:00Z", "target_branch": "master", @@ -383,7 +416,7 @@ Parameters: "project_id": 3, "title": "test1", "description": "fixed login page css paddings", - "state": "opened", + "state": "merged", "created_at": "2017-04-29T08:46:00Z", "updated_at": "2017-04-29T08:46:00Z", "target_branch": "master", @@ -695,7 +728,7 @@ POST /projects/:id/merge_requests "project_id": 3, "title": "test1", "description": "fixed login page css paddings", - "state": "opened", + "state": "merged", "created_at": "2017-04-29T08:46:00Z", "updated_at": "2017-04-29T08:46:00Z", "target_branch": "master", @@ -822,7 +855,7 @@ Must include at least one non-required attribute from above. "project_id": 3, "title": "test1", "description": "fixed login page css paddings", - "state": "opened", + "state": "merged", "created_at": "2017-04-29T08:46:00Z", "updated_at": "2017-04-29T08:46:00Z", "target_branch": "master", @@ -965,7 +998,7 @@ Parameters: "project_id": 3, "title": "test1", "description": "fixed login page css paddings", - "state": "opened", + "state": "merged", "created_at": "2017-04-29T08:46:00Z", "updated_at": "2017-04-29T08:46:00Z", "target_branch": "master", @@ -1080,7 +1113,7 @@ Parameters: "project_id": 3, "title": "test1", "description": "fixed login page css paddings", - "state": "opened", + "state": "merged", "created_at": "2017-04-29T08:46:00Z", "updated_at": "2017-04-29T08:46:00Z", "target_branch": "master", @@ -1279,7 +1312,7 @@ Example response: "project_id": 3, "title": "test1", "description": "fixed login page css paddings", - "state": "opened", + "state": "merged", "created_at": "2017-04-29T08:46:00Z", "updated_at": "2017-04-29T08:46:00Z", "target_branch": "master", @@ -1400,7 +1433,7 @@ Example response: "project_id": 3, "title": "test1", "description": "fixed login page css paddings", - "state": "opened", + "state": "merged", "created_at": "2017-04-29T08:46:00Z", "updated_at": "2017-04-29T08:46:00Z", "target_branch": "master", @@ -1540,7 +1573,7 @@ Example response: "project_id": 3, "title": "Et voluptas laudantium minus nihil recusandae ut accusamus earum aut non.", "description": "Veniam sunt nihil modi earum cumque illum delectus. Nihil ad quis distinctio quia. Autem eligendi at quibusdam repellendus.", - "state": "opened", + "state": "merged", "created_at": "2016-06-17T07:48:04.330Z", "updated_at": "2016-07-01T11:14:15.537Z", "target_branch": "allow_regex_for_project_skip_ref", diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index b3a55e48f4e..c827faace33 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -74,7 +74,7 @@ A job is defined by a list of parameters that define the job behavior. | after_script | no | Override a set of commands that are executed after job | | environment | no | Defines a name of environment to which deployment is done by this job | | coverage | no | Define code coverage settings for a given job | -| retry | no | Define how many times a job can be auto-retried in case of a failure | +| retry | no | Define when and how many times a job can be auto-retried in case of a failure | | parallel | no | Defines how many instances of a job should be run in parallel | ### `extends` @@ -1433,18 +1433,20 @@ job1: ## `retry` > [Introduced][ce-12909] in GitLab 9.5. +> [Behaviour expanded](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21758) +> in GitLab 11.5 to control on which failures to retry. `retry` allows you to configure how many times a job is going to be retried in case of a failure. -When a job fails, and has `retry` configured it is going to be processed again +When a job fails and has `retry` configured, it is going to be processed again up to the amount of times specified by the `retry` keyword. If `retry` is set to 2, and a job succeeds in a second run (first retry), it won't be retried again. `retry` value has to be a positive integer, equal or larger than 0, but lower or equal to 2 (two retries maximum, three runs in total). -A simple example: +A simple example to retry in all failure cases: ```yaml test: @@ -1452,6 +1454,57 @@ test: retry: 2 ``` +By default, a job will be retried on all failure cases. To have a better control +on which failures to retry, `retry` can be a hash with with the following keys: + +- `max`: The maximum number of retries. +- `when`: The failure cases to retry. + +To retry only runner system failures at maximum two times: + +```yaml +test: + script: rspec + retry: + max: 2 + when: runner_system_failure +``` + +If there is another failure, other than a runner system failure, the job will +not be retried. + +To retry on multiple failure cases, `when` can also be an array of failures: + +```yaml +test: + script: rspec + retry: + max: 2 + when: + - runner_system_failure + - stuck_or_timeout_failure +``` + +Possible values for `when` are: + +<!-- + Please make sure to update `RETRY_WHEN_IN_DOCUMENTATION` array in + `spec/lib/gitlab/ci/config/entry/retry_spec.rb` if you change any of + the documented values below. The test there makes sure that all documented + values are really valid as a config option and therefore should always + stay in sync with this documentation. + --> + +- `always`: Retry on any failure (default). +- `unknown_failure`: Retry when the failure reason is unknown. +- `script_failure`: Retry when the script failed. +- `api_failure`: Retry on API failure. +- `stuck_or_timeout_failure`: Retry when the job got stuck or timed out. +- `runner_system_failure`: Retry if there was a runner system failure (e.g. setting up the job failed). +- `missing_dependency_failure`: Retry if a dependency was missing. +- `runner_unsupported`: Retry if the runner was unsupported. + + ## `parallel` > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22631) in GitLab 11.5. diff --git a/doc/development/utilities.md b/doc/development/utilities.md index 0d074a3ef05..e5466ae8914 100644 --- a/doc/development/utilities.md +++ b/doc/development/utilities.md @@ -171,8 +171,8 @@ class Commit extend Gitlab::Cache::RequestCache def author - User.find_by_any_email(author_email.downcase) + User.find_by_any_email(author_email) end - request_cache(:author) { author_email.downcase } + request_cache(:author) { author_email } end ``` diff --git a/doc/user/admin_area/settings/email.md b/doc/user/admin_area/settings/email.md index 7c9e5bf882e..50c318a4969 100644 --- a/doc/user/admin_area/settings/email.md +++ b/doc/user/admin_area/settings/email.md @@ -3,3 +3,20 @@ ## Custom logo The logo in the header of some emails can be customized, see the [logo customization section](../../../customization/branded_page_and_email_header.md). + +## Custom hostname for private commit emails + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22560) in GitLab 11.5. + +This configuration option sets the email hostname for [private commit emails](../../profile/index.md#private-commit-email), +and it's, by default, set to `users.noreply.YOUR_CONFIGURED_HOSTNAME`. + +In order to change this option: + +1. Go to **Admin area > Settings** (`/admin/application_settings`). +1. Under the **Email** section, change the **Custom hostname (for private commit emails)** field. +1. Hit **Save** for the changes to take effect. + +NOTE: **Note**: Once the hostname gets configured, every private commit email using the previous hostname, will not get +recognized by GitLab. This can directly conflict with certain [Push rules](https://docs.gitlab.com/ee/push_rules/push_rules.html) such as +`Check whether author is a GitLab user` and `Check whether committer is the current authenticated user`. diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md index ab62762f343..da7c30b6b39 100644 --- a/doc/user/profile/index.md +++ b/doc/user/profile/index.md @@ -31,6 +31,7 @@ From there, you can: - Update your personal information - Set a [custom status](#current-status) for your profile +- Manage your [commit email](#commit-email) for your profile - Manage [2FA](account/two_factor_authentication.md) - Change your username and [delete your account](account/delete_account.md) - Manage applications that can @@ -132,6 +133,45 @@ They may however contain emoji codes such as `I'm on vacation :palm_tree:`. You can also set your current status [using the API](../../api/users.md#user-status). +## Commit email + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21598) in GitLab 11.4. + +A commit email, is the email that will be displayed in every Git-related action done through the +GitLab interface. + +You are able to select from the list of your own verified emails which email you want to use as the commit email. + +To change it: + +1. Open the user menu in the top-right corner of the navigation bar. +1. Hit **Commit email** selection box. +1. Select any of the verified emails. +1. Hit **Update profile settings**. + +### Private commit email + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22560) in GitLab 11.5. + +GitLab provides the user with an automatically generated private commit email option, +which allows the user to not make their email information public. + +To enable this option: + +1. Open the user menu in the top-right corner of the navigation bar. +1. Hit **Commit email** selection box. +1. Select **Use a private email** option. +1. Hit **Update profile settings**. + +Once this option is enabled, every Git-related action will be performed using the private commit email. + +In order to stay fully annonymous, you can also copy this private commit email +and configure it on your local machine using the following command: + +``` +git config --global user.email "YOUR_PRIVATE_COMMIT_EMAIL" +``` + ## Troubleshooting ### Why do I keep getting signed out? diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index 762d254d6cc..94744cf8500 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -211,7 +211,7 @@ added directly to your configured cluster. Those applications are needed for [Review Apps](../../../ci/review_apps/index.md) and [deployments](../../../ci/environments.md). NOTE: **Note:** -The applications will be installed in a dedicated namespace called +With the exception of Knative, the applications will be installed in a dedicated namespace called `gitlab-managed-apps`. In case you have added an existing Kubernetes cluster with Tiller already installed, you should be careful as GitLab cannot detect it. By installing it via the applications will result into having it @@ -224,6 +224,7 @@ twice, which can lead to confusion during deployments. | [Prometheus](https://prometheus.io/docs/introduction/overview/) | 10.4+ | Prometheus is an open-source monitoring and alerting system useful to supervise your deployed applications. | [stable/prometheus](https://github.com/helm/charts/tree/master/stable/prometheus) | | [GitLab Runner](https://docs.gitlab.com/runner/) | 10.6+ | GitLab Runner is the open source project that is used to run your jobs and send the results back to GitLab. It is used in conjunction with [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/), the open-source continuous integration service included with GitLab that coordinates the jobs. When installing the GitLab Runner via the applications, it will run in **privileged mode** by default. Make sure you read the [security implications](#security-implications) before doing so. | [runner/gitlab-runner](https://gitlab.com/charts/gitlab-runner) | | [JupyterHub](http://jupyter.org/) | 11.0+ | [JupyterHub](https://jupyterhub.readthedocs.io/en/stable/) is a multi-user service for managing notebooks across a team. [Jupyter Notebooks](https://jupyter-notebook.readthedocs.io/en/latest/) provide a web-based interactive programming environment used for data analysis, visualization, and machine learning. We use [this](https://gitlab.com/gitlab-org/jupyterhub-user-image/blob/master/Dockerfile) custom Jupyter image that installs additional useful packages on top of the base Jupyter. You will also see ready-to-use DevOps Runbooks built with Nurtch's [Rubix library](https://github.com/amit1rrr/rubix). More information on creating executable runbooks can be found at [Nurtch Documentation](http://docs.nurtch.com/en/latest). **Note**: Authentication will be enabled for any user of the GitLab server via OAuth2. HTTPS will be supported in a future release. | [jupyter/jupyterhub](https://jupyterhub.github.io/helm-chart/) | +| [Knative](https://cloud.google.com/knative) | 0.1.2 | Knative provides a platform to create, deploy, and manage serverless workloads from a Kubernetes cluster. It is used in conjunction with, and includes [Istio](https://istio.io) to provide an external IP address for all programs hosted by Knative. You will be prompted to enter a wildcard domain where your applications will be exposed. Configure your DNS server to use the external IP address for that domain. For any application created and installed, they will be accessible as <program_name>.<kubernetes_namespace>.<domain_name>. **Note**: This will require your kubernetes cluster to have RBAC enabled. | [knative/knative](https://storage.googleapis.com/triggermesh-charts) ## Getting the external IP address @@ -232,6 +233,10 @@ You need a load balancer installed in your cluster in order to obtain the external IP address with the following procedure. It can be deployed using the [**Ingress** application](#installing-applications). +NOTE: **Note:** +Knative will include its own load balancer in the form of [Istio](https://istio.io). +At this time, to determine the external IP address, you will need to follow the manual approach. + In order to publish your web application, you first need to find the external IP address associated to your load balancer. @@ -262,6 +267,12 @@ run the following command: kubectl get svc --namespace=gitlab-managed-apps ingress-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip} ' ``` +NOTE: **Note:** +For Istio/Knative, the command will be different: +```bash +kubectl get svc --namespace=istio-system knative-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip} ' +``` + Otherwise, you can list the IP addresses of all load balancers: ```bash diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 9f7be27b047..61d57c643f0 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -607,6 +607,22 @@ module API end class MergeRequestBasic < ProjectEntity + expose :merged_by, using: Entities::UserBasic do |merge_request, _options| + merge_request.metrics&.merged_by + end + + expose :merged_at do |merge_request, _options| + merge_request.metrics&.merged_at + end + + expose :closed_by, using: Entities::UserBasic do |merge_request, _options| + merge_request.metrics&.latest_closed_by + end + + expose :closed_at do |merge_request, _options| + merge_request.metrics&.latest_closed_at + end + expose :title_html, if: -> (_, options) { options[:render_html] } do |entity| MarkupHelper.markdown_field(entity, :title) end @@ -676,22 +692,6 @@ module API merge_request.merge_request_diff.real_size end - expose :merged_by, using: Entities::UserBasic do |merge_request, _options| - merge_request.metrics&.merged_by - end - - expose :merged_at do |merge_request, _options| - merge_request.metrics&.merged_at - end - - expose :closed_by, using: Entities::UserBasic do |merge_request, _options| - merge_request.metrics&.latest_closed_by - end - - expose :closed_at do |merge_request, _options| - merge_request.metrics&.latest_closed_at - end - expose :latest_build_started_at, if: -> (_, options) { build_available?(options) } do |merge_request, _options| merge_request.metrics&.latest_build_started_at end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 7909f9c7a00..491b5085bb8 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -28,7 +28,7 @@ module API args[:scope] = args[:scope].underscore if args[:scope] issues = IssuesFinder.new(current_user, args).execute - .preload(:assignees, :labels, :notes, :timelogs, :project, :author) + .preload(:assignees, :labels, :notes, :timelogs, :project, :author, :closed_by) issues.reorder(args[:order_by] => args[:sort]) end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index a617efaaa4c..16f07f16387 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -45,7 +45,7 @@ module API return merge_requests if args[:view] == 'simple' merge_requests - .preload(:notes, :author, :assignee, :milestone, :latest_merge_request_diff, :labels, :timelogs) + .preload(:notes, :author, :assignee, :milestone, :latest_merge_request_diff, :labels, :timelogs, metrics: [:latest_closed_by, :merged_by]) end # rubocop: enable CodeReuse/ActiveRecord diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb index f1786c15f4f..1ae144ca9c1 100644 --- a/lib/api/snippets.rb +++ b/lib/api/snippets.rb @@ -14,7 +14,7 @@ module API end def public_snippets - SnippetsFinder.new(current_user, visibility: Snippet::PUBLIC).execute + SnippetsFinder.new(current_user, scope: :are_public).execute end end diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb index 362014b1a09..8e8c979f973 100644 --- a/lib/gitlab/ci/config/entry/job.rb +++ b/lib/gitlab/ci/config/entry/job.rb @@ -26,16 +26,12 @@ module Gitlab with_options allow_nil: true do validates :tags, array_of_strings: true validates :allow_failure, boolean: true - validates :retry, numericality: { only_integer: true, - greater_than_or_equal_to: 0, - less_than_or_equal_to: 2 } validates :parallel, numericality: { only_integer: true, greater_than_or_equal_to: 2 } validates :when, inclusion: { in: %w[on_success on_failure always manual delayed], message: 'should be on_success, on_failure, ' \ 'always, manual or delayed' } - validates :dependencies, array_of_strings: true validates :extends, type: String end @@ -86,6 +82,9 @@ module Gitlab entry :coverage, Entry::Coverage, description: 'Coverage configuration for this job.' + entry :retry, Entry::Retry, + description: 'Retry configuration for this job.' + helpers :before_script, :script, :stage, :type, :after_script, :cache, :image, :services, :only, :except, :variables, :artifacts, :commands, :environment, :coverage, :retry, @@ -160,7 +159,7 @@ module Gitlab environment: environment_defined? ? environment_value : nil, environment_name: environment_defined? ? environment_value[:name] : nil, coverage: coverage_defined? ? coverage_value : nil, - retry: retry_defined? ? retry_value.to_i : nil, + retry: retry_defined? ? retry_value : nil, parallel: parallel_defined? ? parallel_value.to_i : nil, artifacts: artifacts_value, after_script: after_script_value, diff --git a/lib/gitlab/ci/config/entry/retry.rb b/lib/gitlab/ci/config/entry/retry.rb new file mode 100644 index 00000000000..e39cc5de229 --- /dev/null +++ b/lib/gitlab/ci/config/entry/retry.rb @@ -0,0 +1,90 @@ +module Gitlab + module Ci + class Config + module Entry + ## + # Entry that represents a retry config for a job. + # + class Retry < Simplifiable + strategy :SimpleRetry, if: -> (config) { config.is_a?(Integer) } + strategy :FullRetry, if: -> (config) { config.is_a?(Hash) } + + class SimpleRetry < Entry::Node + include Entry::Validatable + + validations do + validates :config, numericality: { only_integer: true, + greater_than_or_equal_to: 0, + less_than_or_equal_to: 2 } + end + + def value + { + max: config + } + end + + def location + 'retry' + end + end + + class FullRetry < Entry::Node + include Entry::Validatable + include Entry::Attributable + + ALLOWED_KEYS = %i[max when].freeze + attributes :max, :when + + validations do + validates :config, allowed_keys: ALLOWED_KEYS + + with_options allow_nil: true do + validates :max, numericality: { only_integer: true, + greater_than_or_equal_to: 0, + less_than_or_equal_to: 2 } + + validates :when, array_of_strings_or_string: true + validates :when, + allowed_array_values: { in: FullRetry.possible_retry_when_values }, + if: -> (config) { config.when.is_a?(Array) } + validates :when, + inclusion: { in: FullRetry.possible_retry_when_values }, + if: -> (config) { config.when.is_a?(String) } + end + end + + def self.possible_retry_when_values + @possible_retry_when_values ||= ::Ci::Build.failure_reasons.keys.map(&:to_s) + ['always'] + end + + def value + super.tap do |config| + # make sure that `when` is an array, because we allow it to + # be passed as a String in config for simplicity + config[:when] = Array.wrap(config[:when]) if config[:when] + end + end + + def location + 'retry' + end + end + + class UnknownStrategy < Entry::Node + def errors + ["#{location} has to be either an integer or a hash"] + end + + def location + 'retry config' + end + end + + def self.default + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/entry/validators.rb b/lib/gitlab/ci/config/entry/validators.rb index 805d26ca8d8..a1d552fb2e5 100644 --- a/lib/gitlab/ci/config/entry/validators.rb +++ b/lib/gitlab/ci/config/entry/validators.rb @@ -7,11 +7,11 @@ module Gitlab module Validators class AllowedKeysValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) - unknown_keys = record.config.try(:keys).to_a - options[:in] + unknown_keys = value.try(:keys).to_a - options[:in] if unknown_keys.any? - record.errors.add(:config, 'contains unknown keys: ' + - unknown_keys.join(', ')) + record.errors.add(attribute, "contains unknown keys: " + + unknown_keys.join(', ')) end end end @@ -24,6 +24,16 @@ module Gitlab end end + class AllowedArrayValuesValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + unkown_values = value - options[:in] + unless unkown_values.empty? + record.errors.add(attribute, "contains unknown values: " + + unkown_values.join(', ')) + end + end + end + class ArrayOfStringsValidator < ActiveModel::EachValidator include LegacyValidationHelpers @@ -68,6 +78,14 @@ module Gitlab end end + class HashOrIntegerValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + unless value.is_a?(Hash) || value.is_a?(Integer) + record.errors.add(attribute, 'should be a hash or an integer') + end + end + end + class KeyValidator < ActiveModel::EachValidator include LegacyValidationHelpers diff --git a/lib/gitlab/ci/config/normalizer.rb b/lib/gitlab/ci/config/normalizer.rb index 969ae093e8b..b7743bd2090 100644 --- a/lib/gitlab/ci/config/normalizer.rb +++ b/lib/gitlab/ci/config/normalizer.rb @@ -9,14 +9,16 @@ module Gitlab end def normalize_jobs - extract_parallelized_jobs + extract_parallelized_jobs! + return @jobs_config if @parallelized_jobs.empty? + parallelized_config = parallelize_jobs parallelize_dependencies(parallelized_config) end private - def extract_parallelized_jobs + def extract_parallelized_jobs! @parallelized_jobs = {} @jobs_config.each do |job_name, config| @@ -41,8 +43,8 @@ module Gitlab end def parallelize_dependencies(parallelized_config) + parallelized_job_names = @parallelized_jobs.keys.map(&:to_s) parallelized_config.each_with_object({}) do |(job_name, config), hash| - parallelized_job_names = @parallelized_jobs.keys.map(&:to_s) if config[:dependencies] && (intersection = config[:dependencies] & parallelized_job_names).any? deps = intersection.map { |dep| @parallelized_jobs[dep.to_sym].map(&:first) }.flatten hash[job_name] = config.merge(dependencies: deps) diff --git a/lib/gitlab/kubernetes/helm/install_command.rb b/lib/gitlab/kubernetes/helm/install_command.rb index 1be7924d6ac..55add06bdb4 100644 --- a/lib/gitlab/kubernetes/helm/install_command.rb +++ b/lib/gitlab/kubernetes/helm/install_command.rb @@ -4,22 +4,27 @@ module Gitlab class InstallCommand include BaseCommand - attr_reader :name, :files, :chart, :version, :repository + attr_reader :name, :files, :chart, :version, :repository, :preinstall, :postinstall - def initialize(name:, chart:, files:, rbac:, version: nil, repository: nil) + def initialize(name:, chart:, files:, rbac:, version: nil, repository: nil, preinstall: nil, postinstall: nil) @name = name @chart = chart @version = version @rbac = rbac @files = files @repository = repository + @preinstall = preinstall + @postinstall = postinstall end def generate_script super + [ init_command, repository_command, - script_command + repository_update_command, + preinstall_command, + install_command, + postinstall_command ].compact.join("\n") end @@ -37,12 +42,24 @@ module Gitlab ['helm', 'repo', 'add', name, repository].shelljoin if repository end - def script_command + def repository_update_command + 'helm repo update >/dev/null' if repository + end + + def install_command command = ['helm', 'install', chart] + install_command_flags command.shelljoin + " >/dev/null\n" end + def preinstall_command + preinstall.join("\n") if preinstall + end + + def postinstall_command + postinstall.join("\n") if postinstall + end + def install_command_flags name_flag = ['--name', name] namespace_flag = ['--namespace', Gitlab::Kubernetes::Helm::NAMESPACE] diff --git a/lib/gitlab/private_commit_email.rb b/lib/gitlab/private_commit_email.rb new file mode 100644 index 00000000000..bade2248ccd --- /dev/null +++ b/lib/gitlab/private_commit_email.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Gitlab + module PrivateCommitEmail + TOKEN = "_private".freeze + + class << self + def regex + hostname_regexp = Regexp.escape(Gitlab::CurrentSettings.current_application_settings.commit_email_hostname) + + /\A(?<id>([0-9]+))\-([^@]+)@#{hostname_regexp}\z/ + end + + def user_id_for_email(email) + match = email&.match(regex) + return unless match + + match[:id].to_i + end + + def for_user(user) + hostname = Gitlab::CurrentSettings.current_application_settings.commit_email_hostname + + "#{user.id}-#{user.username}@#{hostname}" + end + end + end +end diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index cc0817bdcd2..4e2c6f7b2c2 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -62,6 +62,7 @@ module Gitlab clusters_applications_ingress: count(::Clusters::Applications::Ingress.installed), clusters_applications_prometheus: count(::Clusters::Applications::Prometheus.installed), clusters_applications_runner: count(::Clusters::Applications::Runner.installed), + clusters_applications_knative: count(::Clusters::Applications::Knative.installed), in_review_folder: count(::Environment.in_review_folder), groups: count(Group), issues: count(Issue), diff --git a/locale/gitlab.pot b/locale/gitlab.pot index d834db6caa3..6383f770003 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1352,6 +1352,9 @@ msgstr "" msgid "ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which may incur additional costs depending on the hosting provider your Kubernetes cluster is installed on. If you are using Google Kubernetes Engine, you can %{pricingLink}." msgstr "" +msgid "ClusterIntegration|A Knative build extends Kubernetes and utilizes existing Kubernetes primitives to provide you with the ability to run on-cluster container builds from source. For example, you can write a build that uses Kubernetes-native resources to obtain your source code from a repository, build it into container a image, and then run that image." +msgstr "" + msgid "ClusterIntegration|API URL" msgstr "" @@ -1502,6 +1505,12 @@ msgstr "" msgid "ClusterIntegration|JupyterHub, a multi-user Hub, spawns, manages, and proxies multiple instances of the single-user Jupyter notebook server. JupyterHub can be used to serve notebooks to a class of students, a corporate data science group, or a scientific research group." msgstr "" +msgid "ClusterIntegration|Knative" +msgstr "" + +msgid "ClusterIntegration|Knative Domain Name:" +msgstr "" + msgid "ClusterIntegration|Kubernetes cluster" msgstr "" @@ -2109,6 +2118,9 @@ msgstr "" msgid "Custom CI config path" msgstr "" +msgid "Custom hostname (for private commit emails)" +msgstr "" + msgid "Custom notification events" msgstr "" @@ -4725,6 +4737,9 @@ msgstr "" msgid "Profiles|Invalid username" msgstr "" +msgid "Profiles|Learn more" +msgstr "" + msgid "Profiles|Made a private contribution" msgstr "" @@ -4767,6 +4782,9 @@ msgstr "" msgid "Profiles|This email will be displayed on your public profile." msgstr "" +msgid "Profiles|This email will be used for web based operations, such as edits and merges. %{learn_more}" +msgstr "" + msgid "Profiles|This emoji and message will appear on your profile and throughout the interface." msgstr "" @@ -4791,6 +4809,9 @@ msgstr "" msgid "Profiles|Upload new avatar" msgstr "" +msgid "Profiles|Use a private email - %{email}" +msgstr "" + msgid "Profiles|Username change failed - %{message}" msgstr "" @@ -6323,6 +6344,9 @@ msgstr "" msgid "This setting can be overridden in each project." msgstr "" +msgid "This setting will update the hostname that is used to generate private commit emails. %{learn_more}" +msgstr "" + msgid "This source diff could not be displayed because it is too large." msgstr "" diff --git a/spec/factories/clusters/applications/helm.rb b/spec/factories/clusters/applications/helm.rb index 3c9ca22a051..ff65c76cf26 100644 --- a/spec/factories/clusters/applications/helm.rb +++ b/spec/factories/clusters/applications/helm.rb @@ -57,6 +57,11 @@ FactoryBot.define do cluster factory: %i(cluster with_installed_helm provided_by_gcp) end + factory :clusters_applications_knative, class: Clusters::Applications::Knative do + hostname 'example.com' + cluster factory: %i(cluster with_installed_helm provided_by_gcp) + end + factory :clusters_applications_jupyter, class: Clusters::Applications::Jupyter do oauth_application factory: :oauth_application cluster factory: %i(cluster with_installed_helm provided_by_gcp project) diff --git a/spec/features/merge_request/user_sees_deployment_widget_spec.rb b/spec/features/merge_request/user_sees_deployment_widget_spec.rb index 0e439c8cb2d..74290c0fff9 100644 --- a/spec/features/merge_request/user_sees_deployment_widget_spec.rb +++ b/spec/features/merge_request/user_sees_deployment_widget_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' describe 'Merge request > User sees deployment widget', :js do - describe 'when deployed to an environment' do + describe 'when merge request has associated environments' do let(:user) { create(:user) } let(:project) { create(:project, :repository) } let(:merge_request) { create(:merge_request, :merged, source_project: project) } @@ -10,30 +10,74 @@ describe 'Merge request > User sees deployment widget', :js do let(:ref) { merge_request.target_branch } let(:sha) { project.commit(ref).id } let(:pipeline) { create(:ci_pipeline_without_jobs, sha: sha, project: project, ref: ref) } - let(:build) { create(:ci_build, :success, pipeline: pipeline) } - let!(:deployment) { create(:deployment, :succeed, environment: environment, sha: sha, ref: ref, deployable: build) } let!(:manual) { } before do merge_request.update!(merge_commit_sha: sha) project.add_user(user, role) sign_in(user) - visit project_merge_request_path(project, merge_request) - wait_for_requests end - it 'displays that the environment is deployed' do - wait_for_requests + context 'when deployment succeeded' do + let(:build) { create(:ci_build, :success, pipeline: pipeline) } + let!(:deployment) { create(:deployment, :succeed, environment: environment, sha: sha, ref: ref, deployable: build) } - expect(page).to have_content("Deployed to #{environment.name}") - expect(find('.js-deploy-time')['data-original-title']).to eq(deployment.created_at.to_time.in_time_zone.to_s(:medium)) + it 'displays that the environment is deployed' do + visit project_merge_request_path(project, merge_request) + wait_for_requests + + expect(page).to have_content("Deployed to #{environment.name}") + expect(find('.js-deploy-time')['data-original-title']).to eq(deployment.created_at.to_time.in_time_zone.to_s(:medium)) + end + end + + context 'when deployment failed' do + let(:build) { create(:ci_build, :failed, pipeline: pipeline) } + let!(:deployment) { create(:deployment, :failed, environment: environment, sha: sha, ref: ref, deployable: build) } + + it 'displays that the deployment failed' do + visit project_merge_request_path(project, merge_request) + wait_for_requests + + expect(page).to have_content("Failed to deploy to #{environment.name}") + expect(page).not_to have_css('.js-deploy-time') + end + end + + context 'when deployment running' do + let(:build) { create(:ci_build, :running, pipeline: pipeline) } + let!(:deployment) { create(:deployment, :running, environment: environment, sha: sha, ref: ref, deployable: build) } + + it 'displays that the running deployment' do + visit project_merge_request_path(project, merge_request) + wait_for_requests + + expect(page).to have_content("Deploying to #{environment.name}") + expect(page).not_to have_css('.js-deploy-time') + end + end + + context 'when deployment will happen' do + let(:build) { create(:ci_build, :created, pipeline: pipeline) } + let!(:deployment) { create(:deployment, environment: environment, sha: sha, ref: ref, deployable: build) } + + it 'displays that the environment name' do + visit project_merge_request_path(project, merge_request) + wait_for_requests + + expect(page).to have_content("Deploying to #{environment.name}") + expect(page).not_to have_css('.js-deploy-time') + end end context 'with stop action' do + let(:build) { create(:ci_build, :success, pipeline: pipeline) } + let!(:deployment) { create(:deployment, :succeed, environment: environment, sha: sha, ref: ref, deployable: build) } let(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') } before do deployment.update!(on_stop: manual.name) + visit project_merge_request_path(project, merge_request) wait_for_requests end diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb index 056f4ee2e22..9772a7bacac 100644 --- a/spec/features/projects/environments/environment_spec.rb +++ b/spec/features/projects/environments/environment_spec.rb @@ -25,7 +25,7 @@ describe 'Environment' do end context 'without deployments' do - it 'does show no deployments' do + it 'does not show deployments' do expect(page).to have_content('You don\'t have any deployments right now.') end end @@ -43,6 +43,45 @@ describe 'Environment' do end end + context 'when there is a successful deployment' do + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:build) { create(:ci_build, :success, pipeline: pipeline) } + + let(:deployment) do + create(:deployment, :success, environment: environment, deployable: build) + end + + it 'does show deployments' do + expect(page).to have_link("#{build.name} (##{build.id})") + end + end + + context 'when there is a running deployment' do + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:build) { create(:ci_build, pipeline: pipeline) } + + let(:deployment) do + create(:deployment, :running, environment: environment, deployable: build) + end + + it 'does not show deployments' do + expect(page).to have_content('You don\'t have any deployments right now.') + end + end + + context 'when there is a failed deployment' do + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:build) { create(:ci_build, pipeline: pipeline) } + + let(:deployment) do + create(:deployment, :failed, environment: environment, deployable: build) + end + + it 'does not show deployments' do + expect(page).to have_content('You don\'t have any deployments right now.') + end + end + context 'with related deployable present' do let(:pipeline) { create(:ci_pipeline, project: project) } let(:build) { create(:ci_build, pipeline: pipeline) } diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb index d0ddf69d574..89954d35f91 100644 --- a/spec/features/projects/environments/environments_spec.rb +++ b/spec/features/projects/environments/environments_spec.rb @@ -128,7 +128,7 @@ describe 'Environments page', :js do end end - context 'when there are deployments' do + context 'when there are successful deployments' do let(:project) { create(:project, :repository) } let!(:deployment) do @@ -328,6 +328,22 @@ describe 'Environments page', :js do end end end + + context 'when there is a failed deployment' do + let(:project) { create(:project, :repository) } + + let!(:deployment) do + create(:deployment, :failed, + environment: environment, + sha: project.commit.id) + end + + it 'does not show deployments' do + visit_environments(project) + + expect(page).to have_content('No deployments yet') + end + end end it 'does have a new environment button' do diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb index 1ae0bd988f2..dfeeb3040c6 100644 --- a/spec/finders/snippets_finder_spec.rb +++ b/spec/finders/snippets_finder_spec.rb @@ -4,16 +4,13 @@ describe SnippetsFinder do include Gitlab::Allowable using RSpec::Parameterized::TableSyntax - context 'filter by visibility' do - let!(:snippet1) { create(:personal_snippet, :private) } - let!(:snippet2) { create(:personal_snippet, :internal) } - let!(:snippet3) { create(:personal_snippet, :public) } + describe '#initialize' do + it 'raises ArgumentError when a project and author are given' do + user = build(:user) + project = build(:project) - it "returns public snippets when visibility is PUBLIC" do - snippets = described_class.new(nil, visibility: Snippet::PUBLIC).execute - - expect(snippets).to include(snippet3) - expect(snippets).not_to include(snippet1, snippet2) + expect { described_class.new(user, author: user, project: project) } + .to raise_error(ArgumentError) end end @@ -66,21 +63,21 @@ describe SnippetsFinder do end it "returns internal snippets" do - snippets = described_class.new(user, author: user, visibility: Snippet::INTERNAL).execute + snippets = described_class.new(user, author: user, scope: :are_internal).execute expect(snippets).to include(snippet2) expect(snippets).not_to include(snippet1, snippet3) end it "returns private snippets" do - snippets = described_class.new(user, author: user, visibility: Snippet::PRIVATE).execute + snippets = described_class.new(user, author: user, scope: :are_private).execute expect(snippets).to include(snippet1) expect(snippets).not_to include(snippet2, snippet3) end it "returns public snippets" do - snippets = described_class.new(user, author: user, visibility: Snippet::PUBLIC).execute + snippets = described_class.new(user, author: user, scope: :are_public).execute expect(snippets).to include(snippet3) expect(snippets).not_to include(snippet1, snippet2) @@ -98,6 +95,13 @@ describe SnippetsFinder do expect(snippets).to include(snippet3) expect(snippets).not_to include(snippet2, snippet1) end + + it 'returns all snippets for an admin' do + admin = create(:user, :admin) + snippets = described_class.new(admin, author: user).execute + + expect(snippets).to include(snippet1, snippet2, snippet3) + end end context 'filter by project' do @@ -126,21 +130,21 @@ describe SnippetsFinder do end it "returns public snippets for non project members" do - snippets = described_class.new(user, project: project1, visibility: Snippet::PUBLIC).execute + snippets = described_class.new(user, project: project1, scope: :are_public).execute expect(snippets).to include(@snippet3) expect(snippets).not_to include(@snippet1, @snippet2) end it "returns internal snippets for non project members" do - snippets = described_class.new(user, project: project1, visibility: Snippet::INTERNAL).execute + snippets = described_class.new(user, project: project1, scope: :are_internal).execute expect(snippets).to include(@snippet2) expect(snippets).not_to include(@snippet1, @snippet3) end it "does not return private snippets for non project members" do - snippets = described_class.new(user, project: project1, visibility: Snippet::PRIVATE).execute + snippets = described_class.new(user, project: project1, scope: :are_private).execute expect(snippets).not_to include(@snippet1, @snippet2, @snippet3) end @@ -156,10 +160,17 @@ describe SnippetsFinder do it "returns private snippets for project members" do project1.add_developer(user) - snippets = described_class.new(user, project: project1, visibility: Snippet::PRIVATE).execute + snippets = described_class.new(user, project: project1, scope: :are_private).execute expect(snippets).to include(@snippet1) end + + it 'returns all snippets for an admin' do + admin = create(:user, :admin) + snippets = described_class.new(admin, project: project1).execute + + expect(snippets).to include(@snippet1, @snippet2, @snippet3) + end end describe '#execute' do @@ -184,4 +195,6 @@ describe SnippetsFinder do end end end + + it_behaves_like 'snippet visibility' end diff --git a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json index f7adc4e0b91..6df27bf32b9 100644 --- a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json +++ b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json @@ -9,6 +9,32 @@ "title": { "type": "string" }, "description": { "type": ["string", "null"] }, "state": { "type": "string" }, + "merged_by": { + "type": ["object", "null"], + "properties": { + "name": { "type": "string" }, + "username": { "type": "string" }, + "id": { "type": "integer" }, + "state": { "type": "string" }, + "avatar_url": { "type": "uri" }, + "web_url": { "type": "uri" } + }, + "additionalProperties": false + }, + "merged_at": { "type": ["date", "null"] }, + "closed_by": { + "type": ["object", "null"], + "properties": { + "name": { "type": "string" }, + "username": { "type": "string" }, + "id": { "type": "integer" }, + "state": { "type": "string" }, + "avatar_url": { "type": "uri" }, + "web_url": { "type": "uri" } + }, + "additionalProperties": false + }, + "closed_at": { "type": ["date", "null"] }, "created_at": { "type": "date" }, "updated_at": { "type": "date" }, "target_branch": { "type": "string" }, diff --git a/spec/helpers/profiles_helper_spec.rb b/spec/helpers/profiles_helper_spec.rb index c1d0614c79e..9a2372de69f 100644 --- a/spec/helpers/profiles_helper_spec.rb +++ b/spec/helpers/profiles_helper_spec.rb @@ -1,6 +1,35 @@ require 'rails_helper' describe ProfilesHelper do + describe '#commit_email_select_options' do + it 'returns an array with private commit email along with all the verified emails' do + user = create(:user) + private_email = user.private_commit_email + + verified_emails = user.verified_emails - [private_email] + emails = [ + ["Use a private email - #{private_email}", Gitlab::PrivateCommitEmail::TOKEN], + verified_emails + ] + + expect(helper.commit_email_select_options(user)).to match_array(emails) + end + end + + describe '#selected_commit_email' do + let(:user) { create(:user) } + + it 'returns main email when commit email attribute is nil' do + expect(helper.selected_commit_email(user)).to eq(user.email) + end + + it 'returns DB stored commit_email' do + user.update(commit_email: Gitlab::PrivateCommitEmail::TOKEN) + + expect(helper.selected_commit_email(user)).to eq(Gitlab::PrivateCommitEmail::TOKEN) + end + end + describe '#email_provider_label' do it "returns nil for users without external email" do user = create(:user) diff --git a/spec/javascripts/clusters/components/applications_spec.js b/spec/javascripts/clusters/components/applications_spec.js index a70138c7eee..0e2cc13fa52 100644 --- a/spec/javascripts/clusters/components/applications_spec.js +++ b/spec/javascripts/clusters/components/applications_spec.js @@ -23,6 +23,7 @@ describe('Applications', () => { runner: { title: 'GitLab Runner' }, prometheus: { title: 'Prometheus' }, jupyter: { title: 'JupyterHub' }, + knative: { title: 'Knative' }, }, }); }); @@ -46,6 +47,10 @@ describe('Applications', () => { it('renders a row for Jupyter', () => { expect(vm.$el.querySelector('.js-cluster-application-row-jupyter')).not.toBe(null); }); + + it('renders a row for Knative', () => { + expect(vm.$el.querySelector('.js-cluster-application-row-knative')).not.toBe(null); + }); }); describe('Ingress application', () => { @@ -63,6 +68,7 @@ describe('Applications', () => { runner: { title: 'GitLab Runner' }, prometheus: { title: 'Prometheus' }, jupyter: { title: 'JupyterHub', hostname: '' }, + knative: { title: 'Knative', hostname: '' }, }, }); @@ -86,6 +92,7 @@ describe('Applications', () => { runner: { title: 'GitLab Runner' }, prometheus: { title: 'Prometheus' }, jupyter: { title: 'JupyterHub', hostname: '' }, + knative: { title: 'Knative', hostname: '' }, }, }); @@ -105,6 +112,7 @@ describe('Applications', () => { runner: { title: 'GitLab Runner' }, prometheus: { title: 'Prometheus' }, jupyter: { title: 'JupyterHub', hostname: '' }, + knative: { title: 'Knative', hostname: '' }, }, }); @@ -123,6 +131,7 @@ describe('Applications', () => { runner: { title: 'GitLab Runner' }, prometheus: { title: 'Prometheus' }, jupyter: { title: 'JupyterHub', hostname: '', status: 'installable' }, + knative: { title: 'Knative', hostname: '', status: 'installable' }, }, }); @@ -139,6 +148,7 @@ describe('Applications', () => { runner: { title: 'GitLab Runner' }, prometheus: { title: 'Prometheus' }, jupyter: { title: 'JupyterHub', hostname: '', status: 'installable' }, + knative: { title: 'Knative', hostname: '', status: 'installable' }, }, }); @@ -155,6 +165,7 @@ describe('Applications', () => { runner: { title: 'GitLab Runner' }, prometheus: { title: 'Prometheus' }, jupyter: { title: 'JupyterHub', status: 'installed', hostname: '' }, + knative: { title: 'Knative', status: 'installed', hostname: '' }, }, }); @@ -171,6 +182,7 @@ describe('Applications', () => { runner: { title: 'GitLab Runner' }, prometheus: { title: 'Prometheus' }, jupyter: { title: 'JupyterHub', status: 'not_installable' }, + knative: { title: 'Knative' }, }, }); }); diff --git a/spec/javascripts/clusters/services/mock_data.js b/spec/javascripts/clusters/services/mock_data.js index 4e6ad11cd92..73abf6504c0 100644 --- a/spec/javascripts/clusters/services/mock_data.js +++ b/spec/javascripts/clusters/services/mock_data.js @@ -33,6 +33,11 @@ const CLUSTERS_MOCK_DATA = { status: APPLICATION_STATUS.INSTALLING, status_reason: 'Cannot connect', }, + { + name: 'knative', + status: APPLICATION_STATUS.INSTALLING, + status_reason: 'Cannot connect', + }, ], }, }, @@ -67,6 +72,11 @@ const CLUSTERS_MOCK_DATA = { status: APPLICATION_STATUS.INSTALLABLE, status_reason: 'Cannot connect', }, + { + name: 'knative', + status: APPLICATION_STATUS.INSTALLABLE, + status_reason: 'Cannot connect', + }, ], }, }, @@ -77,6 +87,7 @@ const CLUSTERS_MOCK_DATA = { '/gitlab-org/gitlab-shell/clusters/1/applications/runner': {}, '/gitlab-org/gitlab-shell/clusters/1/applications/prometheus': {}, '/gitlab-org/gitlab-shell/clusters/1/applications/jupyter': {}, + '/gitlab-org/gitlab-shell/clusters/1/applications/knative': {}, }, }; diff --git a/spec/javascripts/clusters/stores/clusters_store_spec.js b/spec/javascripts/clusters/stores/clusters_store_spec.js index e0f55a12fca..34ed36afa5b 100644 --- a/spec/javascripts/clusters/stores/clusters_store_spec.js +++ b/spec/javascripts/clusters/stores/clusters_store_spec.js @@ -100,6 +100,14 @@ describe('Clusters Store', () => { requestReason: null, hostname: '', }, + knative: { + title: 'Knative', + status: mockResponseData.applications[5].status, + statusReason: mockResponseData.applications[5].status_reason, + requestStatus: null, + requestReason: null, + hostname: null, + }, }, }); }); diff --git a/spec/javascripts/diffs/store/getters_spec.js b/spec/javascripts/diffs/store/getters_spec.js index 807a9e3baf0..9c3a38fd526 100644 --- a/spec/javascripts/diffs/store/getters_spec.js +++ b/spec/javascripts/diffs/store/getters_spec.js @@ -49,17 +49,17 @@ describe('Diffs Module Getters', () => { }); }); - describe('areAllFilesCollapsed', () => { + describe('hasCollapsedFile', () => { it('returns true when all files are collapsed', () => { localState.diffFiles = [{ collapsed: true }, { collapsed: true }]; - expect(getters.areAllFilesCollapsed(localState)).toEqual(true); + expect(getters.hasCollapsedFile(localState)).toEqual(true); }); - it('returns false when at least one file is not collapsed', () => { + it('returns true when at least one file is collapsed', () => { localState.diffFiles = [{ collapsed: false }, { collapsed: true }]; - expect(getters.areAllFilesCollapsed(localState)).toEqual(false); + expect(getters.hasCollapsedFile(localState)).toEqual(true); }); }); diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb index f1a2946acda..ac9b0c674a5 100644 --- a/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -10,7 +10,7 @@ describe Gitlab::Ci::Config::Entry::Job do let(:result) do %i[before_script script stage type after_script cache image services only except variables artifacts - environment coverage] + environment coverage retry] end it { is_expected.to match_array result } @@ -98,45 +98,6 @@ describe Gitlab::Ci::Config::Entry::Job do end end - context 'when retry value is not correct' do - context 'when it is not a numeric value' do - let(:config) { { retry: true } } - - it 'returns error about invalid type' do - expect(entry).not_to be_valid - expect(entry.errors).to include 'job retry is not a number' - end - end - - context 'when it is lower than zero' do - let(:config) { { retry: -1 } } - - it 'returns error about value too low' do - expect(entry).not_to be_valid - expect(entry.errors) - .to include 'job retry must be greater than or equal to 0' - end - end - - context 'when it is not an integer' do - let(:config) { { retry: 1.5 } } - - it 'returns error about wrong value' do - expect(entry).not_to be_valid - expect(entry.errors).to include 'job retry must be an integer' - end - end - - context 'when the value is too high' do - let(:config) { { retry: 10 } } - - it 'returns error about value too high' do - expect(entry).not_to be_valid - expect(entry.errors).to include 'job retry must be less than or equal to 2' - end - end - end - context 'when parallel value is not correct' do context 'when it is not a numeric value' do let(:config) { { parallel: true } } diff --git a/spec/lib/gitlab/ci/config/entry/retry_spec.rb b/spec/lib/gitlab/ci/config/entry/retry_spec.rb new file mode 100644 index 00000000000..164a9ed4c3d --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/retry_spec.rb @@ -0,0 +1,236 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Entry::Retry do + let(:entry) { described_class.new(config) } + + shared_context 'when retry value is a numeric', :numeric do + let(:config) { max } + let(:max) {} + end + + shared_context 'when retry value is a hash', :hash do + let(:config) { { max: max, when: public_send(:when) }.compact } + let(:when) {} + let(:max) {} + end + + describe '#value' do + subject(:value) { entry.value } + + context 'when retry value is a numeric', :numeric do + let(:max) { 2 } + + it 'is returned as a hash with max key' do + expect(value).to eq(max: 2) + end + end + + context 'when retry value is a hash', :hash do + context 'and `when` is a string' do + let(:when) { 'unknown_failure' } + + it 'returns when wrapped in an array' do + expect(value).to eq(when: ['unknown_failure']) + end + end + + context 'and `when` is an array' do + let(:when) { %w[unknown_failure runner_system_failure] } + + it 'returns when as it was passed' do + expect(value).to eq(when: %w[unknown_failure runner_system_failure]) + end + end + end + end + + describe 'validation' do + context 'when retry value is correct' do + context 'when it is a numeric', :numeric do + let(:max) { 2 } + + it 'is valid' do + expect(entry).to be_valid + end + end + + context 'when it is a hash', :hash do + context 'with max' do + let(:max) { 2 } + + it 'is valid' do + expect(entry).to be_valid + end + end + + context 'with string when' do + let(:when) { 'unknown_failure' } + + it 'is valid' do + expect(entry).to be_valid + end + end + + context 'with string when always' do + let(:when) { 'always' } + + it 'is valid' do + expect(entry).to be_valid + end + end + + context 'with array when' do + let(:when) { %w[unknown_failure runner_system_failure] } + + it 'is valid' do + expect(entry).to be_valid + end + end + + # Those values are documented at `doc/ci/yaml/README.md`. If any of + # those values gets invalid, documentation must be updated. To make + # sure this is catched, check explicitly that all of the documented + # values are valid. If they are not it means the documentation and this + # array must be updated. + RETRY_WHEN_IN_DOCUMENTATION = %w[ + always + unknown_failure + script_failure + api_failure + stuck_or_timeout_failure + runner_system_failure + missing_dependency_failure + runner_unsupported + ].freeze + + RETRY_WHEN_IN_DOCUMENTATION.each do |reason| + context "with when from documentation `#{reason}`" do + let(:when) { reason } + + it 'is valid' do + expect(entry).to be_valid + end + end + end + + ::Ci::Build.failure_reasons.each_key do |reason| + context "with when from CommitStatus.failure_reasons `#{reason}`" do + let(:when) { reason } + + it 'is valid' do + expect(entry).to be_valid + end + end + end + end + end + + context 'when retry value is not correct' do + context 'when it is not a numeric nor an array' do + let(:config) { true } + + it 'returns error about invalid type' do + expect(entry).not_to be_valid + expect(entry.errors).to include 'retry config has to be either an integer or a hash' + end + end + + context 'when it is a numeric', :numeric do + context 'when it is lower than zero' do + let(:max) { -1 } + + it 'returns error about value too low' do + expect(entry).not_to be_valid + expect(entry.errors) + .to include 'retry config must be greater than or equal to 0' + end + end + + context 'when it is not an integer' do + let(:max) { 1.5 } + + it 'returns error about wrong value' do + expect(entry).not_to be_valid + expect(entry.errors).to include 'retry config has to be either an integer or a hash' + end + end + + context 'when the value is too high' do + let(:max) { 10 } + + it 'returns error about value too high' do + expect(entry).not_to be_valid + expect(entry.errors).to include 'retry config must be less than or equal to 2' + end + end + end + + context 'when it is a hash', :hash do + context 'with unknown keys' do + let(:config) { { max: 2, unknown_key: :something, one_more: :key } } + + it 'returns error about the unknown key' do + expect(entry).not_to be_valid + expect(entry.errors) + .to include 'retry config contains unknown keys: unknown_key, one_more' + end + end + + context 'with max lower than zero' do + let(:max) { -1 } + + it 'returns error about value too low' do + expect(entry).not_to be_valid + expect(entry.errors) + .to include 'retry max must be greater than or equal to 0' + end + end + + context 'with max not an integer' do + let(:max) { 1.5 } + + it 'returns error about wrong value' do + expect(entry).not_to be_valid + expect(entry.errors).to include 'retry max must be an integer' + end + end + + context 'iwth max too high' do + let(:max) { 10 } + + it 'returns error about value too high' do + expect(entry).not_to be_valid + expect(entry.errors).to include 'retry max must be less than or equal to 2' + end + end + + context 'with when in wrong format' do + let(:when) { true } + + it 'returns error about the wrong format' do + expect(entry).not_to be_valid + expect(entry.errors).to include 'retry when should be an array of strings or a string' + end + end + + context 'with an unknown when string' do + let(:when) { 'unknown_reason' } + + it 'returns error about the wrong format' do + expect(entry).not_to be_valid + expect(entry.errors).to include 'retry when is not included in the list' + end + end + + context 'with an unknown failure reason in a when array' do + let(:when) { %w[unknown_reason runner_system_failure] } + + it 'returns error about the wrong format' do + expect(entry).not_to be_valid + expect(entry.errors).to include 'retry when contains unknown values: unknown_reason' + end + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/normalizer_spec.rb b/spec/lib/gitlab/ci/config/normalizer_spec.rb index 7c558cacdd5..97926695b6e 100644 --- a/spec/lib/gitlab/ci/config/normalizer_spec.rb +++ b/spec/lib/gitlab/ci/config/normalizer_spec.rb @@ -31,6 +31,14 @@ describe Gitlab::Ci::Config::Normalizer do expect(configs).to all(eq(original_config)) end + context 'when the job is not parallelized' do + let(:job_config) { { script: 'rspec', name: 'rspec' } } + + it 'returns the same hash' do + is_expected.to eq(config) + end + end + context 'when there is a job with a slash in it' do let(:job_name) { :"rspec 35/2" } diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index dcfd54107a3..441e8214181 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -53,11 +53,11 @@ module Gitlab describe 'retry entry' do context 'when retry count is specified' do let(:config) do - YAML.dump(rspec: { script: 'rspec', retry: 1 }) + YAML.dump(rspec: { script: 'rspec', retry: { max: 1 } }) end it 'includes retry count in build options attribute' do - expect(subject[:options]).to include(retry: 1) + expect(subject[:options]).to include(retry: { max: 1 }) end end diff --git a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb index f28941ce58f..ed879350004 100644 --- a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb @@ -5,6 +5,8 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do let(:repository) { 'https://repository.example.com' } let(:rbac) { false } let(:version) { '1.2.3' } + let(:preinstall) { nil } + let(:postinstall) { nil } let(:install_command) do described_class.new( @@ -13,7 +15,9 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do rbac: rbac, files: files, version: version, - repository: repository + repository: repository, + preinstall: preinstall, + postinstall: postinstall ) end @@ -24,6 +28,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do <<~EOS helm init --client-only >/dev/null helm repo add app-name https://repository.example.com + helm repo update >/dev/null #{helm_install_comand} EOS end @@ -51,6 +56,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do <<~EOS helm init --client-only >/dev/null helm repo add app-name https://repository.example.com + helm repo update >/dev/null #{helm_install_command} EOS end @@ -99,6 +105,53 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do end end + context 'when there is a pre-install script' do + let(:preinstall) { ['/bin/date', '/bin/true'] } + + it_behaves_like 'helm commands' do + let(:commands) do + <<~EOS + helm init --client-only >/dev/null + helm repo add app-name https://repository.example.com + helm repo update >/dev/null + #{helm_install_command} + EOS + end + + let(:helm_install_command) do + <<~EOS.strip + /bin/date + /bin/true + helm install chart-name --name app-name --tls --tls-ca-cert /data/helm/app-name/config/ca.pem --tls-cert /data/helm/app-name/config/cert.pem --tls-key /data/helm/app-name/config/key.pem --version 1.2.3 --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml >/dev/null + EOS + end + end + end + + context 'when there is a post-install script' do + let(:postinstall) { ['/bin/date', "/bin/false\n"] } + + it_behaves_like 'helm commands' do + let(:commands) do + <<~EOS + helm init --client-only >/dev/null + helm repo add app-name https://repository.example.com + helm repo update >/dev/null + #{helm_install_command} + EOS + end + + let(:helm_install_command) do + <<~EOS.strip + helm install chart-name --name app-name --tls --tls-ca-cert /data/helm/app-name/config/ca.pem --tls-cert /data/helm/app-name/config/cert.pem --tls-key /data/helm/app-name/config/key.pem --version 1.2.3 --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml >/dev/null + + /bin/date + /bin/false + EOS + end + end + end + context 'when there is no ca.pem file' do let(:files) { { 'file.txt': 'some content' } } @@ -107,6 +160,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do <<~EOS helm init --client-only >/dev/null helm repo add app-name https://repository.example.com + helm repo update >/dev/null #{helm_install_command} EOS end @@ -131,6 +185,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do <<~EOS helm init --client-only >/dev/null helm repo add app-name https://repository.example.com + helm repo update >/dev/null #{helm_install_command} EOS end diff --git a/spec/lib/gitlab/private_commit_email_spec.rb b/spec/lib/gitlab/private_commit_email_spec.rb new file mode 100644 index 00000000000..bc86cd3842a --- /dev/null +++ b/spec/lib/gitlab/private_commit_email_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::PrivateCommitEmail do + let(:hostname) { Gitlab::CurrentSettings.current_application_settings.commit_email_hostname } + + context '.regex' do + subject { described_class.regex } + + it { is_expected.to match("1-foo@#{hostname}") } + it { is_expected.not_to match("1-foo@#{hostname}.foo") } + it { is_expected.not_to match('1-foo@users.noreply.gitlab.com') } + it { is_expected.not_to match('foo-1@users.noreply.gitlab.com') } + it { is_expected.not_to match('foobar@gitlab.com') } + end + + context '.user_id_for_email' do + let(:id) { 1 } + + it 'parses user id from email' do + email = "#{id}-foo@#{hostname}" + + expect(described_class.user_id_for_email(email)).to eq(id) + end + + it 'returns nil on invalid commit email' do + email = "#{id}-foo@users.noreply.bar.com" + + expect(described_class.user_id_for_email(email)).to be_nil + end + end + + context '.for_user' do + it 'returns email in the format id-username@hostname' do + user = create(:user) + + expect(described_class.for_user(user)).to eq("#{user.id}-#{user.username}@#{hostname}") + end + end +end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 69ee5ff4bcd..76dec4a44fd 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -20,6 +20,7 @@ describe Gitlab::UsageData do create(:clusters_applications_ingress, :installed, cluster: gcp_cluster) create(:clusters_applications_prometheus, :installed, cluster: gcp_cluster) create(:clusters_applications_runner, :installed, cluster: gcp_cluster) + create(:clusters_applications_knative, :installed, cluster: gcp_cluster) end subject { described_class.data } @@ -81,6 +82,7 @@ describe Gitlab::UsageData do clusters_applications_ingress clusters_applications_prometheus clusters_applications_runner + clusters_applications_knative in_review_folder groups issues @@ -126,6 +128,7 @@ describe Gitlab::UsageData do expect(count_data[:clusters_applications_ingress]).to eq(1) expect(count_data[:clusters_applications_prometheus]).to eq(1) expect(count_data[:clusters_applications_runner]).to eq(1) + expect(count_data[:clusters_applications_knative]).to eq(1) end it 'works when queries time out' do diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 95ae7bd21ab..96aa9a82b71 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -25,6 +25,9 @@ describe ApplicationSetting do it { is_expected.to allow_value(https).for(:after_sign_out_path) } it { is_expected.not_to allow_value(ftp).for(:after_sign_out_path) } + it { is_expected.to allow_value("dev.gitlab.com").for(:commit_email_hostname) } + it { is_expected.not_to allow_value("@dev.gitlab").for(:commit_email_hostname) } + describe 'default_artifacts_expire_in' do it 'sets an error if it cannot parse' do setting.update(default_artifacts_expire_in: 'a') @@ -107,6 +110,14 @@ describe ApplicationSetting do it { expect(setting.repository_storages).to eq(['default']) } end + context '#commit_email_hostname' do + it 'returns configured gitlab hostname if commit_email_hostname is not defined' do + setting.update(commit_email_hostname: nil) + + expect(setting.commit_email_hostname).to eq("users.noreply.#{Gitlab.config.gitlab.host}") + end + end + context 'auto_devops_domain setting' do context 'when auto_devops_enabled? is true' do before do diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 5bd2f096656..6849bc6db7a 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1472,15 +1472,15 @@ describe Ci::Build do end describe '#retries_max' do - context 'when max retries value is defined' do - subject { create(:ci_build, options: { retry: 1 }) } + context 'with retries max config option' do + subject { create(:ci_build, options: { retry: { max: 1 } }) } - it 'returns a number of configured max retries' do + it 'returns the number of configured max retries' do expect(subject.retries_max).to eq 1 end end - context 'when max retries value is not defined' do + context 'without retries max config option' do subject { create(:ci_build) } it 'returns zero' do @@ -1495,6 +1495,104 @@ describe Ci::Build do expect(subject.retries_max).to eq 0 end end + + context 'with integer only config option' do + subject { create(:ci_build, options: { retry: 1 }) } + + it 'returns the number of configured max retries' do + expect(subject.retries_max).to eq 1 + end + end + end + + describe '#retry_when' do + context 'with retries when config option' do + subject { create(:ci_build, options: { retry: { when: ['some_reason'] } }) } + + it 'returns the configured when' do + expect(subject.retry_when).to eq ['some_reason'] + end + end + + context 'without retries when config option' do + subject { create(:ci_build) } + + it 'returns always array' do + expect(subject.retry_when).to eq ['always'] + end + end + + context 'with integer only config option' do + subject { create(:ci_build, options: { retry: 1 }) } + + it 'returns always array' do + expect(subject.retry_when).to eq ['always'] + end + end + end + + describe '#retry_failure?' do + subject { create(:ci_build) } + + context 'when retries max is zero' do + before do + expect(subject).to receive(:retries_max).at_least(:once).and_return(0) + end + + it 'returns false' do + expect(subject.retry_failure?).to eq false + end + end + + context 'when retries max equals retries count' do + before do + expect(subject).to receive(:retries_max).at_least(:once).and_return(1) + expect(subject).to receive(:retries_count).at_least(:once).and_return(1) + end + + it 'returns false' do + expect(subject.retry_failure?).to eq false + end + end + + context 'when retries max is higher than retries count' do + before do + expect(subject).to receive(:retries_max).at_least(:once).and_return(2) + expect(subject).to receive(:retries_count).at_least(:once).and_return(1) + end + + context 'and retry when is always' do + before do + expect(subject).to receive(:retry_when).at_least(:once).and_return(['always']) + end + + it 'returns true' do + expect(subject.retry_failure?).to eq true + end + end + + context 'and retry when includes the failure_reason' do + before do + expect(subject).to receive(:failure_reason).at_least(:once).and_return('some_reason') + expect(subject).to receive(:retry_when).at_least(:once).and_return(['some_reason']) + end + + it 'returns true' do + expect(subject.retry_failure?).to eq true + end + end + + context 'and retry when does not include failure_reason' do + before do + expect(subject).to receive(:failure_reason).at_least(:once).and_return('some_reason') + expect(subject).to receive(:retry_when).at_least(:once).and_return(['some', 'other failure']) + end + + it 'returns false' do + expect(subject.retry_failure?).to eq false + end + end + end end end @@ -2887,7 +2985,7 @@ describe Ci::Build do end context 'when build is configured to be retried' do - subject { create(:ci_build, :running, options: { retry: 3 }, project: project, user: user) } + subject { create(:ci_build, :running, options: { retry: { max: 3 } }, project: project, user: user) } it 'retries build and assigns the same user to it' do expect(described_class).to receive(:retry) diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb index 1580ef36127..6b0b23eeab3 100644 --- a/spec/models/clusters/applications/ingress_spec.rb +++ b/spec/models/clusters/applications/ingress_spec.rb @@ -5,6 +5,7 @@ describe Clusters::Applications::Ingress do include_examples 'cluster application core specs', :clusters_applications_ingress include_examples 'cluster application status specs', :clusters_applications_ingress + include_examples 'cluster application helm specs', :clusters_applications_knative before do allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in) @@ -121,28 +122,5 @@ describe Clusters::Applications::Ingress do expect(values).to include('stats') expect(values).to include('podAnnotations') end - - context 'when the helm application does not have a ca_cert' do - before do - application.cluster.application_helm.ca_cert = nil - end - - it 'should not include cert files' do - expect(subject[:'ca.pem']).not_to be_present - expect(subject[:'cert.pem']).not_to be_present - expect(subject[:'key.pem']).not_to be_present - end - end - - it 'should include cert files' do - expect(subject[:'ca.pem']).to be_present - expect(subject[:'ca.pem']).to eq(application.cluster.application_helm.ca_cert) - - expect(subject[:'cert.pem']).to be_present - expect(subject[:'key.pem']).to be_present - - cert = OpenSSL::X509::Certificate.new(subject[:'cert.pem']) - expect(cert.not_after).to be < 60.minutes.from_now - end end end diff --git a/spec/models/clusters/applications/jupyter_spec.rb b/spec/models/clusters/applications/jupyter_spec.rb index 9c4396731eb..faaabafddb7 100644 --- a/spec/models/clusters/applications/jupyter_spec.rb +++ b/spec/models/clusters/applications/jupyter_spec.rb @@ -2,6 +2,7 @@ require 'rails_helper' describe Clusters::Applications::Jupyter do include_examples 'cluster application core specs', :clusters_applications_jupyter + include_examples 'cluster application helm specs', :clusters_applications_knative it { is_expected.to belong_to(:oauth_application) } @@ -79,29 +80,6 @@ describe Clusters::Applications::Jupyter do subject { application.files } - it 'should include cert files' do - expect(subject[:'ca.pem']).to be_present - expect(subject[:'ca.pem']).to eq(application.cluster.application_helm.ca_cert) - - expect(subject[:'cert.pem']).to be_present - expect(subject[:'key.pem']).to be_present - - cert = OpenSSL::X509::Certificate.new(subject[:'cert.pem']) - expect(cert.not_after).to be < 60.minutes.from_now - end - - context 'when the helm application does not have a ca_cert' do - before do - application.cluster.application_helm.ca_cert = nil - end - - it 'should not include cert files' do - expect(subject[:'ca.pem']).not_to be_present - expect(subject[:'cert.pem']).not_to be_present - expect(subject[:'key.pem']).not_to be_present - end - end - it 'should include valid values' do expect(values).to include('ingress') expect(values).to include('hub') diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb new file mode 100644 index 00000000000..be2a91d566b --- /dev/null +++ b/spec/models/clusters/applications/knative_spec.rb @@ -0,0 +1,77 @@ +require 'rails_helper' + +describe Clusters::Applications::Knative do + let(:knative) { create(:clusters_applications_knative) } + + include_examples 'cluster application core specs', :clusters_applications_knative + include_examples 'cluster application status specs', :clusters_applications_knative + include_examples 'cluster application helm specs', :clusters_applications_knative + + describe '.installed' do + subject { described_class.installed } + + let!(:cluster) { create(:clusters_applications_knative, :installed) } + + before do + create(:clusters_applications_knative, :errored) + end + + it { is_expected.to contain_exactly(cluster) } + end + + describe '#make_installing!' do + before do + application.make_installing! + end + + context 'application install previously errored with older version' do + let(:application) { create(:clusters_applications_knative, :scheduled, version: '0.1.3') } + + it 'updates the application version' do + expect(application.reload.version).to eq('0.1.3') + end + end + end + + describe '#make_installed' do + subject { described_class.installed } + + let!(:cluster) { create(:clusters_applications_knative, :installed) } + + before do + create(:clusters_applications_knative, :errored) + end + + it { is_expected.to contain_exactly(cluster) } + end + + describe '#install_command' do + subject { knative.install_command } + + it 'should be an instance of Helm::InstallCommand' do + expect(subject).to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) + end + + it 'should be initialized with knative arguments' do + expect(subject.name).to eq('knative') + expect(subject.chart).to eq('knative/knative') + expect(subject.version).to eq('0.1.3') + expect(subject.files).to eq(knative.files) + end + end + + describe '#files' do + let(:application) { knative } + let(:values) { subject[:'values.yaml'] } + + subject { application.files } + + it 'should include knative specific keys in the values.yaml file' do + expect(values).to include('domain') + end + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:hostname) } + end +end diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb index 48ba163b38c..86de9dc60f2 100644 --- a/spec/models/clusters/applications/prometheus_spec.rb +++ b/spec/models/clusters/applications/prometheus_spec.rb @@ -5,6 +5,7 @@ describe Clusters::Applications::Prometheus do include_examples 'cluster application core specs', :clusters_applications_prometheus include_examples 'cluster application status specs', :clusters_applications_prometheus + include_examples 'cluster application helm specs', :clusters_applications_knative describe '.installed' do subject { described_class.installed } @@ -187,29 +188,6 @@ describe Clusters::Applications::Prometheus do subject { application.files } - it 'should include cert files' do - expect(subject[:'ca.pem']).to be_present - expect(subject[:'ca.pem']).to eq(application.cluster.application_helm.ca_cert) - - expect(subject[:'cert.pem']).to be_present - expect(subject[:'key.pem']).to be_present - - cert = OpenSSL::X509::Certificate.new(subject[:'cert.pem']) - expect(cert.not_after).to be < 60.minutes.from_now - end - - context 'when the helm application does not have a ca_cert' do - before do - application.cluster.application_helm.ca_cert = nil - end - - it 'should not include cert files' do - expect(subject[:'ca.pem']).not_to be_present - expect(subject[:'cert.pem']).not_to be_present - expect(subject[:'key.pem']).not_to be_present - end - end - it 'should include prometheus valid values' do expect(values).to include('alertmanager') expect(values).to include('kubeStateMetrics') diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb index d5fb1a9d010..052cfdbc4b1 100644 --- a/spec/models/clusters/applications/runner_spec.rb +++ b/spec/models/clusters/applications/runner_spec.rb @@ -5,6 +5,7 @@ describe Clusters::Applications::Runner do include_examples 'cluster application core specs', :clusters_applications_runner include_examples 'cluster application status specs', :clusters_applications_runner + include_examples 'cluster application helm specs', :clusters_applications_knative it { is_expected.to belong_to(:runner) } @@ -74,29 +75,6 @@ describe Clusters::Applications::Runner do subject { application.files } - it 'should include cert files' do - expect(subject[:'ca.pem']).to be_present - expect(subject[:'ca.pem']).to eq(application.cluster.application_helm.ca_cert) - - expect(subject[:'cert.pem']).to be_present - expect(subject[:'key.pem']).to be_present - - cert = OpenSSL::X509::Certificate.new(subject[:'cert.pem']) - expect(cert.not_after).to be < 60.minutes.from_now - end - - context 'when the helm application does not have a ca_cert' do - before do - application.cluster.application_helm.ca_cert = nil - end - - it 'should not include cert files' do - expect(subject[:'ca.pem']).not_to be_present - expect(subject[:'cert.pem']).not_to be_present - expect(subject[:'key.pem']).not_to be_present - end - end - it 'should include runner valid values' do expect(values).to include('concurrent') expect(values).to include('checkInterval') diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index 19b76ca8cfb..10b9ca1a778 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -314,9 +314,10 @@ describe Clusters::Cluster do let!(:prometheus) { create(:clusters_applications_prometheus, cluster: cluster) } let!(:runner) { create(:clusters_applications_runner, cluster: cluster) } let!(:jupyter) { create(:clusters_applications_jupyter, cluster: cluster) } + let!(:knative) { create(:clusters_applications_knative, cluster: cluster) } it 'returns a list of created applications' do - is_expected.to contain_exactly(helm, ingress, prometheus, runner, jupyter) + is_expected.to contain_exactly(helm, ingress, prometheus, runner, jupyter, knative) end end end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index e121369f6ac..9a3f1f1c5a1 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -53,6 +53,25 @@ describe Environment do end end + describe '.with_deployment' do + subject { described_class.with_deployment(sha) } + + let(:environment) { create(:environment) } + let(:sha) { RepoHelpers.sample_commit.id } + + context 'when deployment has the specified sha' do + let!(:deployment) { create(:deployment, environment: environment, sha: sha) } + + it { is_expected.to eq([environment]) } + end + + context 'when deployment does not have the specified sha' do + let!(:deployment) { create(:deployment, environment: environment, sha: 'abc') } + + it { is_expected.to be_empty } + end + end + describe '#folder_name' do context 'when it is inside a folder' do subject(:environment) do diff --git a/spec/models/environment_status_spec.rb b/spec/models/environment_status_spec.rb index 52b98552184..90f7e4a4590 100644 --- a/spec/models/environment_status_spec.rb +++ b/spec/models/environment_status_spec.rb @@ -1,8 +1,10 @@ require 'spec_helper' describe EnvironmentStatus do + include ProjectForksHelper + let(:deployment) { create(:deployment, :succeed, :review_app) } - let(:environment) { deployment.environment} + let(:environment) { deployment.environment } let(:project) { deployment.project } let(:merge_request) { create(:merge_request, :deployed_review_app, deployment: deployment) } let(:sha) { deployment.sha } @@ -65,9 +67,9 @@ describe EnvironmentStatus do let(:admin) { create(:admin) } let(:pipeline) { create(:ci_pipeline, sha: sha) } - it 'is based on merge_request.head_pipeline' do - expect(merge_request).to receive(:head_pipeline).and_return(pipeline) - expect(merge_request).not_to receive(:merge_pipeline) + it 'is based on merge_request.diff_head_sha' do + expect(merge_request).to receive(:diff_head_sha) + expect(merge_request).not_to receive(:merge_commit_sha) described_class.for_merge_request(merge_request, admin) end @@ -81,11 +83,83 @@ describe EnvironmentStatus do merge_request.mark_as_merged! end - it 'is based on merge_request.merge_pipeline' do - expect(merge_request).to receive(:merge_pipeline).and_return(pipeline) - expect(merge_request).not_to receive(:head_pipeline) + it 'is based on merge_request.merge_commit_sha' do + expect(merge_request).to receive(:merge_commit_sha) + expect(merge_request).not_to receive(:diff_head_sha) described_class.after_merge_request(merge_request, admin) end end + + describe '.build_environments_status' do + subject { described_class.send(:build_environments_status, merge_request, user, sha) } + + let!(:build) { create(:ci_build, :deploy_to_production, pipeline: pipeline) } + let(:environment) { build.deployment.environment } + let(:user) { project.owner } + + before do + build.deployment&.update!(sha: sha) + end + + context 'when environment is created on a forked project' do + let(:project) { create(:project, :repository) } + let(:forked) { fork_project(project, user, repository: true) } + let(:sha) { forked.commit.sha } + let(:pipeline) { create(:ci_pipeline, sha: sha, project: forked) } + + let(:merge_request) do + create(:merge_request, + source_project: forked, + target_project: project, + target_branch: 'master', + head_pipeline: pipeline) + end + + it 'returns environment status' do + expect(subject.count).to eq(1) + expect(subject[0].environment).to eq(environment) + expect(subject[0].merge_request).to eq(merge_request) + expect(subject[0].sha).to eq(sha) + end + end + + context 'when environment is created on a target project' do + let(:project) { create(:project, :repository) } + let(:sha) { project.commit.sha } + let(:pipeline) { create(:ci_pipeline, sha: sha, project: project) } + + let(:merge_request) do + create(:merge_request, + source_project: project, + source_branch: 'feature', + target_project: project, + target_branch: 'master', + head_pipeline: pipeline) + end + + it 'returns environment status' do + expect(subject.count).to eq(1) + expect(subject[0].environment).to eq(environment) + expect(subject[0].merge_request).to eq(merge_request) + expect(subject[0].sha).to eq(sha) + end + + context 'when the build stops an environment' do + let!(:build) { create(:ci_build, :stop_review_app, pipeline: pipeline) } + + it 'does not return environment status' do + expect(subject.count).to eq(0) + end + end + + context 'when user does not have a permission to see the environment' do + let(:user) { create(:user) } + + it 'does not return environment status' do + expect(subject.count).to eq(0) + end + end + end + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index f020557e4af..471f19f9b7c 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -4010,6 +4010,28 @@ describe Project do end end + describe '#snippets_visible?' do + it 'returns true when a logged in user can read snippets' do + project = create(:project, :public) + user = create(:user) + + expect(project.snippets_visible?(user)).to eq(true) + end + + it 'returns true when an anonymous user can read snippets' do + project = create(:project, :public) + + expect(project.snippets_visible?).to eq(true) + end + + it 'returns false when a user can not read snippets' do + project = create(:project, :private) + user = create(:user) + + expect(project.snippets_visible?(user)).to eq(false) + end + end + def rugged_config rugged_repo(project.repository).config end diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb index e09d89d235d..7a7272ccb60 100644 --- a/spec/models/snippet_spec.rb +++ b/spec/models/snippet_spec.rb @@ -131,6 +131,217 @@ describe Snippet do end end + describe '.with_optional_visibility' do + context 'when a visibility level is provided' do + it 'returns snippets with the given visibility' do + create(:snippet, :private) + + snippet = create(:snippet, :public) + snippets = described_class + .with_optional_visibility(Gitlab::VisibilityLevel::PUBLIC) + + expect(snippets).to eq([snippet]) + end + end + + context 'when a visibility level is not provided' do + it 'returns all snippets' do + snippet1 = create(:snippet, :public) + snippet2 = create(:snippet, :private) + snippets = described_class.with_optional_visibility + + expect(snippets).to include(snippet1, snippet2) + end + end + end + + describe '.only_global_snippets' do + it 'returns snippets not associated with any projects' do + create(:project_snippet) + + snippet = create(:snippet) + snippets = described_class.only_global_snippets + + expect(snippets).to eq([snippet]) + end + end + + describe '.only_include_projects_visible_to' do + let!(:project1) { create(:project, :public) } + let!(:project2) { create(:project, :internal) } + let!(:project3) { create(:project, :private) } + let!(:snippet1) { create(:project_snippet, project: project1) } + let!(:snippet2) { create(:project_snippet, project: project2) } + let!(:snippet3) { create(:project_snippet, project: project3) } + + context 'when a user is provided' do + it 'returns snippets visible to the user' do + user = create(:user) + + snippets = described_class.only_include_projects_visible_to(user) + + expect(snippets).to include(snippet1, snippet2) + expect(snippets).not_to include(snippet3) + end + end + + context 'when a user is not provided' do + it 'returns snippets visible to anonymous users' do + snippets = described_class.only_include_projects_visible_to + + expect(snippets).to include(snippet1) + expect(snippets).not_to include(snippet2, snippet3) + end + end + end + + describe 'only_include_projects_with_snippets_enabled' do + context 'when the include_private option is enabled' do + it 'includes snippets for projects with snippets set to private' do + project = create(:project) + + project.project_feature + .update(snippets_access_level: ProjectFeature::PRIVATE) + + snippet = create(:project_snippet, project: project) + + snippets = described_class + .only_include_projects_with_snippets_enabled(include_private: true) + + expect(snippets).to eq([snippet]) + end + end + + context 'when the include_private option is not enabled' do + it 'does not include snippets for projects that have snippets set to private' do + project = create(:project) + + project.project_feature + .update(snippets_access_level: ProjectFeature::PRIVATE) + + create(:project_snippet, project: project) + + snippets = described_class.only_include_projects_with_snippets_enabled + + expect(snippets).to be_empty + end + end + + it 'includes snippets for projects with snippets enabled' do + project = create(:project) + + project.project_feature + .update(snippets_access_level: ProjectFeature::ENABLED) + + snippet = create(:project_snippet, project: project) + snippets = described_class.only_include_projects_with_snippets_enabled + + expect(snippets).to eq([snippet]) + end + end + + describe '.only_include_authorized_projects' do + it 'only includes snippets for projects the user is authorized to see' do + user = create(:user) + project1 = create(:project, :private) + project2 = create(:project, :private) + + project1.team.add_developer(user) + + create(:project_snippet, project: project2) + + snippet = create(:project_snippet, project: project1) + snippets = described_class.only_include_authorized_projects(user) + + expect(snippets).to eq([snippet]) + end + end + + describe '.for_project_with_user' do + context 'when a user is provided' do + it 'returns an empty collection if the user can not view the snippets' do + project = create(:project, :private) + user = create(:user) + + project.project_feature + .update(snippets_access_level: ProjectFeature::ENABLED) + + create(:project_snippet, :public, project: project) + + expect(described_class.for_project_with_user(project, user)).to be_empty + end + + it 'returns the snippets if the user is a member of the project' do + project = create(:project, :private) + user = create(:user) + snippet = create(:project_snippet, project: project) + + project.team.add_developer(user) + + snippets = described_class.for_project_with_user(project, user) + + expect(snippets).to eq([snippet]) + end + + it 'returns public snippets for a public project the user is not a member of' do + project = create(:project, :public) + + project.project_feature + .update(snippets_access_level: ProjectFeature::ENABLED) + + user = create(:user) + snippet = create(:project_snippet, :public, project: project) + + create(:project_snippet, :private, project: project) + + snippets = described_class.for_project_with_user(project, user) + + expect(snippets).to eq([snippet]) + end + end + + context 'when a user is not provided' do + it 'returns an empty collection for a private project' do + project = create(:project, :private) + + project.project_feature + .update(snippets_access_level: ProjectFeature::ENABLED) + + create(:project_snippet, :public, project: project) + + expect(described_class.for_project_with_user(project)).to be_empty + end + + it 'returns public snippets for a public project' do + project = create(:project, :public) + snippet = create(:project_snippet, :public, project: project) + + project.project_feature + .update(snippets_access_level: ProjectFeature::PUBLIC) + + create(:project_snippet, :private, project: project) + + snippets = described_class.for_project_with_user(project) + + expect(snippets).to eq([snippet]) + end + end + end + + describe '.visible_to_or_authored_by' do + it 'returns snippets visible to the user' do + user = create(:user) + snippet1 = create(:snippet, :public) + snippet2 = create(:snippet, :private, author: user) + snippet3 = create(:snippet, :private) + + snippets = described_class.visible_to_or_authored_by(user) + + expect(snippets).to include(snippet1, snippet2) + expect(snippets).not_to include(snippet3) + end + end + describe '#participants' do let(:project) { create(:project, :public) } let(:snippet) { create(:snippet, content: 'foo', project: project) } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 4e7c8523e65..0ac5bd666ae 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -183,6 +183,12 @@ describe User do expect(found_user.commit_email).to eq(user.email) end + it 'returns the private commit email when commit_email has _private' do + user.update_column(:commit_email, Gitlab::PrivateCommitEmail::TOKEN) + + expect(user.commit_email).to eq(user.private_commit_email) + end + it 'can be set to a confirmed email' do confirmed = create(:email, :confirmed, user: user) user.commit_email = confirmed.email @@ -333,6 +339,40 @@ describe User do expect(user).to be_valid end end + + context 'set_commit_email' do + it 'keeps commit email when private commit email is being used' do + user = create(:user, commit_email: Gitlab::PrivateCommitEmail::TOKEN) + + expect(user.read_attribute(:commit_email)).to eq(Gitlab::PrivateCommitEmail::TOKEN) + end + + it 'keeps the commit email when nil' do + user = create(:user, commit_email: nil) + + expect(user.read_attribute(:commit_email)).to be_nil + end + + it 'reverts to nil when email is not verified' do + user = create(:user, commit_email: "foo@bar.com") + + expect(user.read_attribute(:commit_email)).to be_nil + end + end + + context 'owns_commit_email' do + it 'accepts private commit email' do + user = build(:user, commit_email: Gitlab::PrivateCommitEmail::TOKEN) + + expect(user).to be_valid + end + + it 'accepts nil commit email' do + user = build(:user, commit_email: nil) + + expect(user).to be_valid + end + end end end @@ -1075,6 +1115,14 @@ describe User do end describe '.find_by_any_email' do + it 'finds user through private commit email' do + user = create(:user) + private_email = user.private_commit_email + + expect(described_class.find_by_any_email(private_email)).to eq(user) + expect(described_class.find_by_any_email(private_email, confirmed: true)).to eq(user) + end + it 'finds by primary email' do user = create(:user, email: 'foo@example.com') @@ -1082,6 +1130,13 @@ describe User do expect(described_class.find_by_any_email(user.email, confirmed: true)).to eq user end + it 'finds by uppercased email' do + user = create(:user, email: 'foo@example.com') + + expect(described_class.find_by_any_email(user.email.upcase)).to eq user + expect(described_class.find_by_any_email(user.email.upcase, confirmed: true)).to eq user + end + it 'finds by secondary email' do email = create(:email, email: 'foo@example.com') user = email.user @@ -1457,7 +1512,7 @@ describe User do email_confirmed = create :email, user: user, confirmed_at: Time.now create :email, user: user - expect(user.verified_emails).to match_array([user.email, email_confirmed.email]) + expect(user.verified_emails).to match_array([user.email, user.private_commit_email, email_confirmed.email]) end end @@ -1473,6 +1528,10 @@ describe User do expect(user.verified_email?(email_confirmed.email.titlecase)).to be_truthy end + it 'returns true when user is found through private commit email' do + expect(user.verified_email?(user.private_commit_email)).to be_truthy + end + it 'returns false when the email is not verified/confirmed' do email_unconfirmed = create :email, user: user user.reload @@ -1668,6 +1727,24 @@ describe User do end end + describe '.find_by_private_commit_email' do + context 'with email' do + set(:user) { create(:user) } + + it 'returns user through private commit email' do + expect(described_class.find_by_private_commit_email(user.private_commit_email)).to eq(user) + end + + it 'returns nil when email other than private_commit_email is used' do + expect(described_class.find_by_private_commit_email(user.email)).to be_nil + end + end + + it 'returns nil when email is nil' do + expect(described_class.find_by_private_commit_email(nil)).to be_nil + end + end + describe '#sort_by_attribute' do before do described_class.delete_all diff --git a/spec/serializers/environment_status_entity_spec.rb b/spec/serializers/environment_status_entity_spec.rb index 962ec919092..52bd40ecb5e 100644 --- a/spec/serializers/environment_status_entity_spec.rb +++ b/spec/serializers/environment_status_entity_spec.rb @@ -15,6 +15,7 @@ describe EnvironmentStatusEntity do subject { entity.as_json } before do + deployment.update(sha: merge_request.diff_head_sha) allow(request).to receive(:current_user).and_return(user) end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 054b7b1561c..5c87ed5c3c6 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -435,16 +435,34 @@ describe Ci::CreatePipelineService do end context 'when builds with auto-retries are configured' do - before do - config = YAML.dump(rspec: { script: 'rspec', retry: 2 }) - stub_ci_pipeline_yaml_file(config) + context 'as an integer' do + before do + config = YAML.dump(rspec: { script: 'rspec', retry: 2 }) + stub_ci_pipeline_yaml_file(config) + end + + it 'correctly creates builds with auto-retry value configured' do + pipeline = execute_service + + expect(pipeline).to be_persisted + expect(pipeline.builds.find_by(name: 'rspec').retries_max).to eq 2 + expect(pipeline.builds.find_by(name: 'rspec').retry_when).to eq ['always'] + end end - it 'correctly creates builds with auto-retry value configured' do - pipeline = execute_service + context 'as hash' do + before do + config = YAML.dump(rspec: { script: 'rspec', retry: { max: 2, when: 'runner_system_failure' } }) + stub_ci_pipeline_yaml_file(config) + end - expect(pipeline).to be_persisted - expect(pipeline.builds.find_by(name: 'rspec').retries_max).to eq 2 + it 'correctly creates builds with auto-retry value configured' do + pipeline = execute_service + + expect(pipeline).to be_persisted + expect(pipeline.builds.find_by(name: 'rspec').retries_max).to eq 2 + expect(pipeline.builds.find_by(name: 'rspec').retry_when).to eq ['runner_system_failure'] + end end end diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb index 8c7258c42ad..538992b621e 100644 --- a/spec/services/ci/process_pipeline_service_spec.rb +++ b/spec/services/ci/process_pipeline_service_spec.rb @@ -671,9 +671,9 @@ describe Ci::ProcessPipelineService, '#execute' do context 'when builds with auto-retries are configured' do before do - create_build('build:1', stage_idx: 0, user: user, options: { retry: 2 }) + create_build('build:1', stage_idx: 0, user: user, options: { retry: { max: 2 } }) create_build('test:1', stage_idx: 1, user: user, when: :on_failure) - create_build('test:2', stage_idx: 1, user: user, options: { retry: 1 }) + create_build('test:2', stage_idx: 1, user: user, options: { retry: { max: 1 } }) end it 'automatically retries builds in a valid order' do diff --git a/spec/services/clusters/applications/create_service_spec.rb b/spec/services/clusters/applications/create_service_spec.rb index 056db0c5486..a9985133b93 100644 --- a/spec/services/clusters/applications/create_service_spec.rb +++ b/spec/services/clusters/applications/create_service_spec.rb @@ -67,5 +67,38 @@ describe Clusters::Applications::CreateService do expect { subject }.to raise_error(Clusters::Applications::CreateService::InvalidApplicationError) end end + + context 'knative application' do + let(:params) do + { + application: 'knative', + hostname: 'example.com' + } + end + + before do + allow_any_instance_of(Clusters::Applications::ScheduleInstallationService).to receive(:execute) + end + + it 'creates the application' do + expect do + subject + + cluster.reload + end.to change(cluster, :application_knative) + end + + it 'sets the hostname' do + expect(subject.hostname).to eq('example.com') + end + end + + context 'invalid application' do + let(:params) { { application: 'non-existent' } } + + it 'raises an error' do + expect { subject }.to raise_error(Clusters::Applications::CreateService::InvalidApplicationError) + end + end end end diff --git a/spec/services/clusters/gcp/finalize_creation_service_spec.rb b/spec/services/clusters/gcp/finalize_creation_service_spec.rb index 7fbb6cf2cf5..efee158739d 100644 --- a/spec/services/clusters/gcp/finalize_creation_service_spec.rb +++ b/spec/services/clusters/gcp/finalize_creation_service_spec.rb @@ -33,7 +33,7 @@ describe Clusters::Gcp::FinalizeCreationService, '#execute' do expect(provider.endpoint).to eq(endpoint) expect(platform.api_url).to eq(api_url) - expect(platform.ca_cert).to eq(Base64.decode64(load_sample_cert)) + expect(platform.ca_cert).to eq(Base64.decode64(load_sample_cert).strip) expect(platform.username).to eq(username) expect(platform.password).to eq(password) expect(platform.token).to eq(token) diff --git a/spec/support/shared_examples/models/cluster_application_helm_cert_examples.rb b/spec/support/shared_examples/models/cluster_application_helm_cert_examples.rb new file mode 100644 index 00000000000..d87b3181e80 --- /dev/null +++ b/spec/support/shared_examples/models/cluster_application_helm_cert_examples.rb @@ -0,0 +1,25 @@ +shared_examples 'cluster application helm specs' do |application_name| + let(:application) { create(application_name) } + + describe '#files' do + subject { application.files } + + context 'when the helm application does not have a ca_cert' do + before do + application.cluster.application_helm.ca_cert = nil + end + + it 'should not include cert files when there is no ca_cert entry' do + expect(subject).not_to include(:'ca.pem', :'cert.pem', :'key.pem') + end + end + + it 'should include cert files when there is a ca_cert entry' do + expect(subject).to include(:'ca.pem', :'cert.pem', :'key.pem') + expect(subject[:'ca.pem']).to eq(application.cluster.application_helm.ca_cert) + + cert = OpenSSL::X509::Certificate.new(subject[:'cert.pem']) + expect(cert.not_after).to be < 60.minutes.from_now + end + end +end diff --git a/spec/support/shared_examples/requests/api/merge_requests_list.rb b/spec/support/shared_examples/requests/api/merge_requests_list.rb index 1aed8ab0113..668a390b5d2 100644 --- a/spec/support/shared_examples/requests/api/merge_requests_list.rb +++ b/spec/support/shared_examples/requests/api/merge_requests_list.rb @@ -16,7 +16,12 @@ shared_examples 'merge requests list' do create(:merge_request, state: 'closed', milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: 'Test', created_at: base_time) - create(:merge_request, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: 'Test', created_at: base_time) + merge_request = create(:merge_request, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: 'Test', created_at: base_time) + + merge_request.metrics.update!(merged_by: user, + latest_closed_by: user, + latest_closed_at: 1.hour.ago, + merged_at: 2.hours.ago) expect do get api(endpoint_path, user) |