Commit 1ca9950d authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent bcc77054
...@@ -7,7 +7,7 @@ class Admin::ServicesController < Admin::ApplicationController ...@@ -7,7 +7,7 @@ class Admin::ServicesController < Admin::ApplicationController
before_action :service, only: [:edit, :update] before_action :service, only: [:edit, :update]
def index def index
@services = instance_level_services @services = services_templates
end end
def edit def edit
...@@ -19,7 +19,7 @@ class Admin::ServicesController < Admin::ApplicationController ...@@ -19,7 +19,7 @@ class Admin::ServicesController < Admin::ApplicationController
def update def update
if service.update(service_params[:service]) if service.update(service_params[:service])
PropagateInstanceLevelServiceWorker.perform_async(service.id) if service.active? PropagateServiceTemplateWorker.perform_async(service.id) if service.active?
redirect_to admin_application_settings_services_path, redirect_to admin_application_settings_services_path,
notice: 'Application settings saved successfully' notice: 'Application settings saved successfully'
...@@ -31,17 +31,17 @@ class Admin::ServicesController < Admin::ApplicationController ...@@ -31,17 +31,17 @@ class Admin::ServicesController < Admin::ApplicationController
private private
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def instance_level_services def services_templates
Service.available_services_names.map do |service_name| Service.available_services_names.map do |service_name|
service = "#{service_name}_service".camelize.constantize service_template = "#{service_name}_service".camelize.constantize
service.where(instance: true).first_or_create service_template.where(template: true).first_or_create
end end
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def service def service
@service ||= Service.where(id: params[:id], instance: true).first @service ||= Service.where(id: params[:id], template: true).first
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
......
...@@ -8,11 +8,15 @@ module Projects ...@@ -8,11 +8,15 @@ module Projects
def index def index
respond_to do |format| respond_to do |format|
format.json do format.json do
functions = finder.execute functions = finder.execute.select do |function|
can?(@current_user, :read_cluster, function.cluster)
end
serialized_functions = serialize_function(functions)
render json: { render json: {
knative_installed: finder.knative_installed, knative_installed: finder.knative_installed,
functions: serialize_function(functions) functions: serialized_functions
}.to_json }.to_json
end end
...@@ -23,11 +27,14 @@ module Projects ...@@ -23,11 +27,14 @@ module Projects
end end
def show def show
@service = serialize_function(finder.service(params[:environment_id], params[:id])) function = finder.service(params[:environment_id], params[:id])
@prometheus = finder.has_prometheus?(params[:environment_id]) return not_found unless function && can?(@current_user, :read_cluster, function.cluster)
@service = serialize_function(function)
return not_found if @service.nil? return not_found if @service.nil?
@prometheus = finder.has_prometheus?(params[:environment_id])
respond_to do |format| respond_to do |format|
format.json do format.json do
render json: @service render json: @service
......
...@@ -93,24 +93,32 @@ module Projects ...@@ -93,24 +93,32 @@ module Projects
.services .services
.select { |svc| svc["metadata"]["name"] == name } .select { |svc| svc["metadata"]["name"] == name }
add_metadata(finder, services).first unless services.nil? attributes = add_metadata(finder, services).first
next unless attributes
Gitlab::Serverless::Service.new(attributes)
end end
end end
def knative_services def knative_services
services_finders.map do |finder| services_finders.map do |finder|
services = finder.services attributes = add_metadata(finder, finder.services)
add_metadata(finder, services) unless services.nil? attributes&.map do |attributes|
Gitlab::Serverless::Service.new(attributes)
end
end end
end end
def add_metadata(finder, services) def add_metadata(finder, services)
return if services.nil?
add_pod_count = services.one? add_pod_count = services.one?
services.each do |s| services.each do |s|
s["environment_scope"] = finder.cluster.environment_scope s["environment_scope"] = finder.cluster.environment_scope
s["cluster_id"] = finder.cluster.id s["environment"] = finder.environment
s["cluster"] = finder.cluster
if add_pod_count if add_pod_count
s["podcount"] = finder s["podcount"] = finder
......
...@@ -12,6 +12,10 @@ module Types ...@@ -12,6 +12,10 @@ module Types
description: 'Blob highlighted data', description: 'Blob highlighted data',
null: true null: true
field :plain_highlighted_data, GraphQL::STRING_TYPE,
description: 'Blob plain highlighted data',
null: true
field :raw_path, GraphQL::STRING_TYPE, field :raw_path, GraphQL::STRING_TYPE,
description: 'Blob raw content endpoint path', description: 'Blob raw content endpoint path',
null: false null: false
......
...@@ -290,6 +290,12 @@ module Clusters ...@@ -290,6 +290,12 @@ module Clusters
end end
end end
def serverless_domain
strong_memoize(:serverless_domain) do
self.application_knative&.serverless_domain_cluster
end
end
private private
def unique_management_project_environment_scope def unique_management_project_environment_scope
......
...@@ -62,6 +62,8 @@ class PagesDomain < ApplicationRecord ...@@ -62,6 +62,8 @@ class PagesDomain < ApplicationRecord
scope :for_removal, -> { where("remove_at < ?", Time.now) } scope :for_removal, -> { where("remove_at < ?", Time.now) }
scope :with_logging_info, -> { includes(project: [:namespace, :route]) }
def verified? def verified?
!!verified_at !!verified_at
end end
...@@ -285,3 +287,5 @@ class PagesDomain < ApplicationRecord ...@@ -285,3 +287,5 @@ class PagesDomain < ApplicationRecord
!auto_ssl_enabled? && project&.pages_https_only? !auto_ssl_enabled? && project&.pages_https_only?
end end
end end
PagesDomain.prepend_if_ee('::EE::PagesDomain')
...@@ -1224,13 +1224,13 @@ class Project < ApplicationRecord ...@@ -1224,13 +1224,13 @@ class Project < ApplicationRecord
service = find_service(services, name) service = find_service(services, name)
return service if service return service if service
# We should check if an instance-level service exists # We should check if template for the service exists
instance_level_service = find_service(instance_level_services, name) template = find_service(services_templates, name)
if instance_level_service if template
Service.build_from_instance(id, instance_level_service) Service.build_from_template(id, template)
else else
# If no instance-level service exists, we should create a new service. Ex `build_gitlab_ci_service` # If no template, we should create an instance. Ex `build_gitlab_ci_service`
public_send("build_#{name}_service") # rubocop:disable GitlabSecurity/PublicSend public_send("build_#{name}_service") # rubocop:disable GitlabSecurity/PublicSend
end end
end end
...@@ -2460,8 +2460,8 @@ class Project < ApplicationRecord ...@@ -2460,8 +2460,8 @@ class Project < ApplicationRecord
end end
end end
def instance_level_services def services_templates
@instance_level_services ||= Service.where(instance: true) @services_templates ||= Service.where(template: true)
end end
def ensure_pages_metadatum def ensure_pages_metadatum
......
...@@ -164,7 +164,7 @@ class IssueTrackerService < Service ...@@ -164,7 +164,7 @@ class IssueTrackerService < Service
end end
def one_issue_tracker def one_issue_tracker
return if instance? return if template?
return if project.blank? return if project.blank?
if project.services.external_issue_trackers.where.not(id: id).any? if project.services.external_issue_trackers.where.not(id: id).any?
......
...@@ -85,7 +85,7 @@ class PrometheusService < MonitoringService ...@@ -85,7 +85,7 @@ class PrometheusService < MonitoringService
end end
def prometheus_available? def prometheus_available?
return false if instance? return false if template?
return false unless project return false unless project
project.all_clusters.enabled.any? { |cluster| cluster.application_prometheus_available? } project.all_clusters.enabled.any? { |cluster| cluster.application_prometheus_available? }
......
...@@ -32,7 +32,7 @@ class Service < ApplicationRecord ...@@ -32,7 +32,7 @@ class Service < ApplicationRecord
belongs_to :project, inverse_of: :services belongs_to :project, inverse_of: :services
has_one :service_hook has_one :service_hook
validates :project_id, presence: true, unless: proc { |service| service.instance? } validates :project_id, presence: true, unless: proc { |service| service.template? }
validates :type, presence: true validates :type, presence: true
scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') } scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') }
...@@ -70,8 +70,8 @@ class Service < ApplicationRecord ...@@ -70,8 +70,8 @@ class Service < ApplicationRecord
true true
end end
def instance? def template?
instance template
end end
def category def category
...@@ -299,15 +299,15 @@ class Service < ApplicationRecord ...@@ -299,15 +299,15 @@ class Service < ApplicationRecord
service_names.sort_by(&:downcase) service_names.sort_by(&:downcase)
end end
def self.build_from_instance(project_id, instance_level_service) def self.build_from_template(project_id, template)
service = instance_level_service.dup service = template.dup
if instance_level_service.supports_data_fields? if template.supports_data_fields?
data_fields = instance_level_service.data_fields.dup data_fields = template.data_fields.dup
data_fields.service = service data_fields.service = service
end end
service.instance = false service.template = false
service.project_id = project_id service.project_id = project_id
service.active = false if service.active? && !service.valid? service.active = false if service.active? && !service.valid?
service service
...@@ -321,6 +321,10 @@ class Service < ApplicationRecord ...@@ -321,6 +321,10 @@ class Service < ApplicationRecord
nil nil
end end
def self.find_by_template
find_by(template: true)
end
# override if needed # override if needed
def supports_data_fields? def supports_data_fields?
false false
......
...@@ -4,11 +4,13 @@ class SnippetBlobPresenter < BlobPresenter ...@@ -4,11 +4,13 @@ class SnippetBlobPresenter < BlobPresenter
def highlighted_data def highlighted_data
return if blob.binary? return if blob.binary?
if blob.rich_viewer&.partial_name == 'markup' highlight(plain: false)
blob.rendered_markup
else
highlight
end end
def plain_highlighted_data
return if blob.binary?
highlight(plain: true)
end end
def raw_path def raw_path
......
...@@ -5,91 +5,31 @@ module Projects ...@@ -5,91 +5,31 @@ module Projects
class ServiceEntity < Grape::Entity class ServiceEntity < Grape::Entity
include RequestAwareEntity include RequestAwareEntity
expose :name do |service| expose :name
service.dig('metadata', 'name') expose :namespace
end expose :environment_scope
expose :podcount
expose :namespace do |service| expose :created_at
service.dig('metadata', 'namespace') expose :image
end expose :description
expose :url
expose :environment_scope do |service|
service.dig('environment_scope')
end
expose :cluster_id do |service|
service.dig('cluster_id')
end
expose :detail_url do |service| expose :detail_url do |service|
project_serverless_path( project_serverless_path(
request.project, request.project,
service.dig('environment_scope'), service.environment_scope,
service.dig('metadata', 'name')) service.name)
end
expose :podcount do |service|
service.dig('podcount')
end end
expose :metrics_url do |service| expose :metrics_url do |service|
project_serverless_metrics_path( project_serverless_metrics_path(
request.project, request.project,
service.dig('environment_scope'), service.environment_scope,
service.dig('metadata', 'name')) + ".json" service.name, format: :json)
end
expose :created_at do |service|
service.dig('metadata', 'creationTimestamp')
end
expose :url do |service|
knative_06_07_url(service) || knative_05_url(service)
end
expose :description do |service|
knative_07_description(service) || knative_05_06_description(service)
end end
expose :image do |service| expose :cluster_id do |service|
service.dig( service.cluster&.id
'spec',
'runLatest',
'configuration',
'build',
'template',
'name')
end
private
def knative_07_description(service)
service.dig(
'spec',
'template',
'metadata',
'annotations',
'Description'
)
end
def knative_05_url(service)
"http://#{service.dig('status', 'domain')}"
end
def knative_06_07_url(service)
service.dig('status', 'url')
end
def knative_05_06_description(service)
service.dig(
'spec',
'runLatest',
'configuration',
'revisionTemplate',
'metadata',
'annotations',
'Description')
end end
end end
end end
......
...@@ -135,7 +135,7 @@ module Projects ...@@ -135,7 +135,7 @@ module Projects
if @project.save if @project.save
unless @project.gitlab_project_import? unless @project.gitlab_project_import?
create_services_from_active_instance_level_services(@project) create_services_from_active_templates(@project)
@project.create_labels @project.create_labels
end end
...@@ -161,9 +161,9 @@ module Projects ...@@ -161,9 +161,9 @@ module Projects
end end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def create_services_from_active_instance_level_services(project) def create_services_from_active_templates(project)
Service.where(instance: true, active: true).each do |template| Service.where(template: true, active: true).each do |template|
service = Service.build_from_instance(project.id, template) service = Service.build_from_template(project.id, template)
service.save! service.save!
end end
end end
......
# frozen_string_literal: true # frozen_string_literal: true
module Projects module Projects
class PropagateInstanceLevelService class PropagateServiceTemplate
BATCH_SIZE = 100 BATCH_SIZE = 100
def self.propagate(*args) def self.propagate(*args)
new(*args).propagate new(*args).propagate
end end
def initialize(instance_level_service) def initialize(template)
@instance_level_service = instance_level_service @template = template
end end
def propagate def propagate
return unless @instance_level_service.active? return unless @template.active?
Rails.logger.info("Propagating services for instance_level_service #{@instance_level_service.id}") # rubocop:disable Gitlab/RailsLogger Rails.logger.info("Propagating services for template #{@template.id}") # rubocop:disable Gitlab/RailsLogger
propagate_projects_with_instance_level_service propagate_projects_with_template
end end
private private
def propagate_projects_with_instance_level_service def propagate_projects_with_template
loop do loop do
batch = Project.uncached { project_ids_batch } batch = Project.uncached { project_ids_batch }
bulk_create_from_instance_level_service(batch) unless batch.empty? bulk_create_from_template(batch) unless batch.empty?
break if batch.size < BATCH_SIZE break if batch.size < BATCH_SIZE
end end
end end
def bulk_create_from_instance_level_service(batch) def bulk_create_from_template(batch)
service_list = batch.map do |project_id| service_list = batch.map do |project_id|
service_hash.values << project_id service_hash.values << project_id
end end
...@@ -52,7 +52,7 @@ module Projects ...@@ -52,7 +52,7 @@ module Projects
SELECT true SELECT true
FROM services FROM services
WHERE services.project_id = projects.id WHERE services.project_id = projects.id
AND services.type = '#{@instance_level_service.type}' AND services.type = '#{@template.type}'
) )
AND projects.pending_delete = false AND projects.pending_delete = false
AND projects.archived = false AND projects.archived = false
...@@ -73,9 +73,9 @@ module Projects ...@@ -73,9 +73,9 @@ module Projects
def service_hash def service_hash
@service_hash ||= @service_hash ||=
begin begin
instance_hash = @instance_level_service.as_json(methods: :type).except('id', 'instance', 'project_id') template_hash = @template.as_json(methods: :type).except('id', 'template', 'project_id')
instance_hash.each_with_object({}) do |(key, value), service_hash| template_hash.each_with_object({}) do |(key, value), service_hash|
value = value.is_a?(Hash) ? value.to_json : value value = value.is_a?(Hash) ? value.to_json : value
service_hash[ActiveRecord::Base.connection.quote_column_name(key)] = service_hash[ActiveRecord::Base.connection.quote_column_name(key)] =
...@@ -97,11 +97,11 @@ module Projects ...@@ -97,11 +97,11 @@ module Projects
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def active_external_issue_tracker? def active_external_issue_tracker?
@instance_level_service.issue_tracker? && !@instance_level_service.default @template.issue_tracker? && !@template.default
end end
def active_external_wiki? def active_external_wiki?
@instance_level_service.type == 'ExternalWikiService' @template.type == 'ExternalWikiService'
end end
end end
end end
...@@ -10,8 +10,8 @@ ...@@ -10,8 +10,8 @@
%p.inline %p.inline
= s_("MattermostService|See list of available commands in Mattermost after setting up this service, by entering") = s_("MattermostService|See list of available commands in Mattermost after setting up this service, by entering")
%kbd.inline /&lt;trigger&gt; help %kbd.inline /&lt;trigger&gt; help
- unless enabled || @service.instance? - unless enabled || @service.template?
= render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service = render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service
- if enabled && !@service.instance? - if enabled && !@service.template?
= render 'projects/services/mattermost_slash_commands/installation_info', subject: @service = render 'projects/services/mattermost_slash_commands/installation_info', subject: @service
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
%p.inline %p.inline
= s_("SlackService|See list of available commands in Slack after setting up this service, by entering") = s_("SlackService|See list of available commands in Slack after setting up this service, by entering")
%kbd.inline /&lt;command&gt; help %kbd.inline /&lt;command&gt; help
- unless @service.instance? - unless @service.template?
%p= _("To set up this service:") %p= _("To set up this service:")
%ul.list-unstyled.indent-list %ul.list-unstyled.indent-list
%li %li
......
...@@ -969,7 +969,7 @@ ...@@ -969,7 +969,7 @@
:latency_sensitive: :latency_sensitive:
:resource_boundary: :unknown :resource_boundary: :unknown
:weight: 1 :weight: 1
- :name: propagate_instance_level_service - :name: propagate_service_template
:feature_category: :source_code_management :feature_category: :source_code_management
:has_external_dependencies: :has_external_dependencies:
:latency_sensitive: :latency_sensitive:
......
...@@ -2,14 +2,14 @@ ...@@ -2,14 +2,14 @@
class PagesDomainRemovalCronWorker class PagesDomainRemovalCronWorker
include ApplicationWorker include ApplicationWorker
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext include CronjobQueue
feature_category :pages feature_category :pages
worker_resource_boundary :cpu worker_resource_boundary :cpu
def perform def perform
PagesDomain.for_removal.find_each do |domain| PagesDomain.for_removal.with_logging_info.find_each do |domain|
domain.destroy! with_context(project: domain.project) { domain.destroy! }
rescue => e rescue => e
Gitlab::ErrorTracking.track_exception(e) Gitlab::ErrorTracking.track_exception(e)
end end
......
...@@ -2,15 +2,17 @@ ...@@ -2,15 +2,17 @@
class PagesDomainSslRenewalCronWorker class PagesDomainSslRenewalCronWorker
include ApplicationWorker include ApplicationWorker
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext include CronjobQueue
feature_category :pages feature_category :pages
def perform def perform
return unless ::Gitlab::LetsEncrypt.enabled? return unless ::Gitlab::LetsEncrypt.enabled?
PagesDomain.need_auto_ssl_renewal.find_each do |domain| PagesDomain.need_auto_ssl_renewal.with_logging_info.find_each do |domain|
with_context(project: domain.project) do
PagesDomainSslRenewalWorker.perform_async(domain.id) PagesDomainSslRenewalWorker.perform_async(domain.id)
end end
end end
end
end end
...@@ -2,15 +2,17 @@ ...@@ -2,15 +2,17 @@
class PagesDomainVerificationCronWorker class PagesDomainVerificationCronWorker
include ApplicationWorker include ApplicationWorker
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext include CronjobQueue
feature_category :pages feature_category :pages
def perform def perform
return if Gitlab::Database.read_only? return if Gitlab::Database.read_only?
PagesDomain.needs_verification.find_each do |domain| PagesDomain.needs_verification.with_logging_info.find_each do |domain|
with_context(project: domain.project) do
PagesDomainVerificationWorker.perform_async(domain.id) PagesDomainVerificationWorker.perform_async(domain.id)
end end
end end
end
end end
# frozen_string_literal: true # frozen_string_literal: true
# Worker for updating any project specific caches. # Worker for updating any project specific caches.
class PropagateInstanceLevelServiceWorker class PropagateServiceTemplateWorker
include ApplicationWorker include ApplicationWorker
feature_category :source_code_management feature_category :source_code_management
...@@ -9,18 +9,18 @@ class PropagateInstanceLevelServiceWorker ...@@ -9,18 +9,18 @@ class PropagateInstanceLevelServiceWorker
LEASE_TIMEOUT = 4.hours.to_i LEASE_TIMEOUT = 4.hours.to_i
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def perform(instance_level_service_id) def perform(template_id)
return unless try_obtain_lease_for(instance_level_service_id) return unless try_obtain_lease_for(template_id)
Projects::PropagateInstanceLevelService.propagate(Service.find_by(id: instance_level_service_id)) Projects::PropagateServiceTemplate.propagate(Service.find_by(id: template_id))
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
private private
def try_obtain_lease_for(instance_level_service_id) def try_obtain_lease_for(template_id)
Gitlab::ExclusiveLease Gitlab::ExclusiveLease
.new("propagate_instance_level_service_worker:#{instance_level_service_id}", timeout: LEASE_TIMEOUT) .new("propagate_service_template_worker:#{template_id}", timeout: LEASE_TIMEOUT)
.try_obtain .try_obtain
end end
end end
---
title: Add plain_highlighted_data field to SnippetBlobType
merge_request: 24856
author:
type: changed
---
title: 'Service model: Rename template attribute to instance'
merge_request: 23595
author:
type: other
...@@ -194,7 +194,7 @@ ...@@ -194,7 +194,7 @@
- 1 - 1
- - project_update_repository_storage - - project_update_repository_storage
- 1 - 1
- - propagate_instance_level_service - - propagate_service_template
- 1 - 1
- - reactive_caching - - reactive_caching
- 1 - 1
......
# frozen_string_literal: true
class RenameServicesTemplateToInstance < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
rename_column_concurrently :services, :template, :instance
end
def down
undo_rename_column_concurrently :services, :template, :instance
end
end
...@@ -23,10 +23,6 @@ class RemoveEmptyGithubServiceTemplates < ActiveRecord::Migration[5.2] ...@@ -23,10 +23,6 @@ class RemoveEmptyGithubServiceTemplates < ActiveRecord::Migration[5.2]
private private
def relationship def relationship
# The column `template` was renamed to `instance`. Column information needs
# to be resetted to avoid cache problems after migrating down.
RemoveEmptyGithubServiceTemplates::Service.reset_column_information
RemoveEmptyGithubServiceTemplates::Service.where(template: true, type: 'GithubService') RemoveEmptyGithubServiceTemplates::Service.where(template: true, type: 'GithubService')
end end
end end
# frozen_string_literal: true
class CleanupRenameServicesTemplateToInstance < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
cleanup_concurrent_column_rename :services, :template, :instance
end
def down
undo_cleanup_concurrent_column_rename :services, :template, :instance
end
end
# frozen_string_literal: true
class MigratePropagateServiceTemplateSidekiqQueue < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
sidekiq_queue_migrate 'propagate_service_template', to: 'propagate_instance_level_service'
end
def down
sidekiq_queue_migrate 'propagate_instance_level_service', to: 'propagate_service_template'
end
end
...@@ -3858,9 +3858,9 @@ ActiveRecord::Schema.define(version: 2020_02_07_151640) do ...@@ -3858,9 +3858,9 @@ ActiveRecord::Schema.define(version: 2020_02_07_151640) do
t.boolean "deployment_events", default: false, null: false t.boolean "deployment_events", default: false, null: false
t.string "description", limit: 500 t.string "description", limit: 500
t.boolean "comment_on_event_enabled", default: true, null: false t.boolean "comment_on_event_enabled", default: true, null: false
t.boolean "instance", default: false t.boolean "template", default: false
t.index ["instance"], name: "index_services_on_instance"
t.index ["project_id"], name: "index_services_on_project_id" t.index ["project_id"], name: "index_services_on_project_id"
t.index ["template"], name: "index_services_on_template"
t.index ["type"], name: "index_services_on_type" t.index ["type"], name: "index_services_on_type"
end end
......
...@@ -6777,6 +6777,11 @@ type SnippetBlob { ...@@ -6777,6 +6777,11 @@ type SnippetBlob {
""" """
path: String path: String
"""
Blob plain highlighted data
"""
plainHighlightedData: String
""" """
Blob raw content endpoint path Blob raw content endpoint path
""" """
......
...@@ -7612,6 +7612,20 @@ ...@@ -7612,6 +7612,20 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "plainHighlightedData",
"description": "Blob plain highlighted data",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "rawPath", "name": "rawPath",
"description": "Blob raw content endpoint path", "description": "Blob raw content endpoint path",
......
...@@ -1071,6 +1071,7 @@ Represents the snippet blob ...@@ -1071,6 +1071,7 @@ Represents the snippet blob
| `mode` | String | Blob mode | | `mode` | String | Blob mode |
| `name` | String | Blob name | | `name` | String | Blob name |
| `path` | String | Blob path | | `path` | String | Blob path |
| `plainHighlightedData` | String | Blob plain highlighted data |
| `rawPath` | String! | Blob raw content endpoint path | | `rawPath` | String! | Blob raw content endpoint path |
| `richViewer` | SnippetBlobViewer | Blob content rich viewer | | `richViewer` | SnippetBlobViewer | Blob content rich viewer |
| `simpleViewer` | SnippetBlobViewer! | Blob content simple viewer | | `simpleViewer` | SnippetBlobViewer! | Blob content simple viewer |
......
...@@ -132,7 +132,7 @@ module API ...@@ -132,7 +132,7 @@ module API
helpers do helpers do
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def slash_command_service(project, service_slug, params) def slash_command_service(project, service_slug, params)
project.services.active.where(instance: false).find do |service| project.services.active.where(template: false).find do |service|
service.try(:token) == params[:token] && service.to_param == service_slug.underscore service.try(:token) == params[:token] && service.to_param == service_slug.underscore
end end
end end
......
...@@ -257,7 +257,7 @@ excluded_attributes: ...@@ -257,7 +257,7 @@ excluded_attributes:
- :token - :token
- :token_encrypted - :token_encrypted
services: services:
- :instance - :template
error_tracking_setting: error_tracking_setting:
- :encrypted_token - :encrypted_token
- :encrypted_token_iv - :encrypted_token_iv
......
# frozen_string_literal: true
class Gitlab::Serverless::Service
include Gitlab::Utils::StrongMemoize
def initialize(attributes)
@attributes = attributes
end
def name
@attributes.dig('metadata', 'name')
end
def namespace
@attributes.dig('metadata', 'namespace')
end
def environment_scope
@attributes.dig('environment_scope')
end
def environment
@attributes.dig('environment')
end
def podcount
@attributes.dig('podcount')
end
def created_at
strong_memoize(:created_at) do
timestamp = @attributes.dig('metadata', 'creationTimestamp')
DateTime.parse(timestamp) if timestamp
end
end
def image
@attributes.dig(
'spec',
'runLatest',
'configuration',
'build',
'template',
'name')
end
def description
knative_07_description || knative_05_06_description
end
def cluster
@attributes.dig('cluster')
end
def url
proxy_url || knative_06_07_url || knative_05_url
end
private
def proxy_url
if cluster&.serverless_domain
Gitlab::Serverless::FunctionURI.new(function: name, cluster: cluster.serverless_domain, environment: environment)
end
end
def knative_07_description
@attributes.dig(
'spec',
'template',
'metadata',
'annotations',
'Description'
)
end
def knative_05_06_description
@attributes.dig(
'spec',
'runLatest',
'configuration',
'revisionTemplate',
'metadata',
'annotations',
'Description')
end
def knative_05_url
domain = @attributes.dig('status', 'domain')
return unless domain
"http://#{domain}"
end
def knative_06_07_url
@attributes.dig('status', 'url')
end
end
...@@ -179,7 +179,7 @@ module Gitlab ...@@ -179,7 +179,7 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def services_usage def services_usage
service_counts = count(Service.active.where(instance: false).where.not(type: 'JiraService').group(:type), fallback: Hash.new(-1)) service_counts = count(Service.active.where(template: false).where.not(type: 'JiraService').group(:type), fallback: Hash.new(-1))
results = Service.available_services_names.each_with_object({}) do |service_name, response| results = Service.available_services_names.each_with_object({}) do |service_name, response|
response["projects_#{service_name}_active".to_sym] = service_counts["#{service_name}_service".camelize] || 0 response["projects_#{service_name}_active".to_sym] = service_counts["#{service_name}_service".camelize] || 0
......
...@@ -469,6 +469,7 @@ module QA ...@@ -469,6 +469,7 @@ module QA
autoload :Configure, 'qa/vendor/jenkins/page/configure' autoload :Configure, 'qa/vendor/jenkins/page/configure'
autoload :NewCredentials, 'qa/vendor/jenkins/page/new_credentials' autoload :NewCredentials, 'qa/vendor/jenkins/page/new_credentials'
autoload :NewJob, 'qa/vendor/jenkins/page/new_job' autoload :NewJob, 'qa/vendor/jenkins/page/new_job'
autoload :Job, 'qa/vendor/jenkins/page/job'
autoload :ConfigureJob, 'qa/vendor/jenkins/page/configure_job' autoload :ConfigureJob, 'qa/vendor/jenkins/page/configure_job'
end end
end end
......
# frozen_string_literal: true
require 'capybara/dsl'
module QA
module Vendor
module Jenkins
module Page
class Job < Page::Base
attr_accessor :job_name
def path
"/job/#{@job_name}"
end
def has_successful_build?
page.has_text?("Last successful build")
end
end
end
end
end
end
...@@ -15,11 +15,11 @@ describe Admin::ServicesController do ...@@ -15,11 +15,11 @@ describe Admin::ServicesController do
Service.available_services_names.each do |service_name| Service.available_services_names.each do |service_name|
context "#{service_name}" do context "#{service_name}" do
let!(:service) do let!(:service) do
service_instance = "#{service_name}_service".camelize.constantize service_template = "#{service_name}_service".camelize.constantize
service_instance.where(instance: true).first_or_create service_template.where(template: true).first_or_create
end end
it 'successfully displays the service' do it 'successfully displays the template' do
get :edit, params: { id: service.id } get :edit, params: { id: service.id }
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
...@@ -34,7 +34,7 @@ describe Admin::ServicesController do ...@@ -34,7 +34,7 @@ describe Admin::ServicesController do
RedmineService.create( RedmineService.create(
project: project, project: project,
active: false, active: false,
instance: true, template: true,
properties: { properties: {
project_url: 'http://abc', project_url: 'http://abc',
issues_url: 'http://abc', issues_url: 'http://abc',
...@@ -44,7 +44,7 @@ describe Admin::ServicesController do ...@@ -44,7 +44,7 @@ describe Admin::ServicesController do
end end
it 'calls the propagation worker when service is active' do it 'calls the propagation worker when service is active' do
expect(PropagateInstanceLevelServiceWorker).to receive(:perform_async).with(service.id) expect(PropagateServiceTemplateWorker).to receive(:perform_async).with(service.id)
put :update, params: { id: service.id, service: { active: true } } put :update, params: { id: service.id, service: { active: true } }
...@@ -52,7 +52,7 @@ describe Admin::ServicesController do ...@@ -52,7 +52,7 @@ describe Admin::ServicesController do
end end
it 'does not call the propagation worker when service is not active' do it 'does not call the propagation worker when service is not active' do
expect(PropagateInstanceLevelServiceWorker).not_to receive(:perform_async) expect(PropagateServiceTemplateWorker).not_to receive(:perform_async)
put :update, params: { id: service.id, service: { properties: {} } } put :update, params: { id: service.id, service: { properties: {} } }
......
...@@ -14,9 +14,11 @@ describe Projects::Serverless::FunctionsController do ...@@ -14,9 +14,11 @@ describe Projects::Serverless::FunctionsController do
let!(:deployment) { create(:deployment, :success, environment: environment, cluster: cluster) } let!(:deployment) { create(:deployment, :success, environment: environment, cluster: cluster) }
let(:knative_services_finder) { environment.knative_services_finder } let(:knative_services_finder) { environment.knative_services_finder }
let(:function_description) { 'A serverless function' } let(:function_description) { 'A serverless function' }
let(:function_name) { 'some-function-name' }
let(:knative_stub_options) do let(:knative_stub_options) do
{ namespace: namespace.namespace, name: cluster.project.name, description: function_description } { namespace: namespace.namespace, name: function_name, description: function_description }
end end
let(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
let(:namespace) do let(:namespace) do
create(:cluster_kubernetes_namespace, create(:cluster_kubernetes_namespace,
...@@ -87,25 +89,65 @@ describe Projects::Serverless::FunctionsController do ...@@ -87,25 +89,65 @@ describe Projects::Serverless::FunctionsController do
end end
context 'when functions were found' do context 'when functions were found' do
let(:functions) { ["asdf"] } let(:functions) { [{}, {}] }
before do before do
stub_kubeclient_knative_services(namespace: namespace.namespace) stub_kubeclient_knative_services(namespace: namespace.namespace, cluster_id: cluster.id, name: function_name)
get :index, params: params({ format: :json })
end end
it 'returns functions' do it 'returns functions' do
get :index, params: params({ format: :json })
expect(json_response["functions"]).not_to be_empty expect(json_response["functions"]).not_to be_empty
end end
it { expect(response).to have_gitlab_http_status(:ok) } it 'filters out the functions whose cluster the user does not have permission to read' do
allow(controller).to receive(:can?).and_return(true)
expect(controller).to receive(:can?).with(user, :read_cluster, cluster).and_return(false)
get :index, params: params({ format: :json })
expect(json_response["functions"]).to be_empty
end
it 'returns a successful response status' do
get :index, params: params({ format: :json })
expect(response).to have_gitlab_http_status(:ok)
end
context 'when there is serverless domain for a cluster' do
let!(:serverless_domain_cluster) do
create(:serverless_domain_cluster, clusters_applications_knative_id: knative.id)
end
it 'returns JSON with function details with serverless domain URL' do
get :index, params: params({ format: :json })
expect(response).to have_gitlab_http_status(:ok)
expect(json_response["functions"]).not_to be_empty
expect(json_response["functions"]).to all(
include(
'url' => "https://#{function_name}-#{serverless_domain_cluster.uuid[0..1]}a1#{serverless_domain_cluster.uuid[2..-3]}f2#{serverless_domain_cluster.uuid[-2..-1]}#{"%x" % environment.id}-#{environment.slug}.#{serverless_domain_cluster.domain}"
)
)
end
end
context 'when there is no serverless domain for a cluster' do
it 'keeps function URL as it was' do
expect(Gitlab::Serverless::Domain).not_to receive(:new)
get :index, params: params({ format: :json })
expect(response).to have_gitlab_http_status(:ok)
end
end
end end
end end
end end
describe 'GET #show' do describe 'GET #show' do
context 'invalid data' do context 'with function that does not exist' do
it 'has a bad function name' do it 'returns 404' do
get :show, params: params({ format: :json, environment_id: "*", id: "foo" }) get :show, params: params({ format: :json, environment_id: "*", id: "foo" })
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(:not_found)
end end
...@@ -113,15 +155,50 @@ describe Projects::Serverless::FunctionsController do ...@@ -113,15 +155,50 @@ describe Projects::Serverless::FunctionsController do
context 'with valid data', :use_clean_rails_memory_store_caching do context 'with valid data', :use_clean_rails_memory_store_caching do
shared_examples 'GET #show with valid data' do shared_examples 'GET #show with valid data' do
it 'has a valid function name' do context 'when there is serverless domain for a cluster' do
get :show, params: params({ format: :json, environment_id: "*", id: cluster.project.name }) let!(:serverless_domain_cluster) do
create(:serverless_domain_cluster, clusters_applications_knative_id: knative.id)
end
it 'returns JSON with function details with serverless domain URL' do
get :show, params: params({ format: :json, environment_id: "*", id: function_name })
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to include(
'url' => "https://#{function_name}-#{serverless_domain_cluster.uuid[0..1]}a1#{serverless_domain_cluster.uuid[2..-3]}f2#{serverless_domain_cluster.uuid[-2..-1]}#{"%x" % environment.id}-#{environment.slug}.#{serverless_domain_cluster.domain}"
)
end
it 'returns 404 when user does not have permission to read the cluster' do
allow(controller).to receive(:can?).and_return(true)
expect(controller).to receive(:can?).with(user, :read_cluster, cluster).and_return(false)
get :show, params: params({ format: :json, environment_id: "*", id: function_name })
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when there is no serverless domain for a cluster' do
it 'keeps function URL as it was' do
get :show, params: params({ format: :json, environment_id: "*", id: function_name })
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to include(
'url' => "http://#{function_name}.#{namespace.namespace}.example.com"
)
end
end
it 'return json with function details' do
get :show, params: params({ format: :json, environment_id: "*", id: function_name })
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to include( expect(json_response).to include(
'name' => project.name, 'name' => function_name,
'url' => "http://#{project.name}.#{namespace.namespace}.example.com", 'url' => "http://#{function_name}.#{namespace.namespace}.example.com",
'description' => function_description, 'description' => function_description,
'podcount' => 1 'podcount' => 0
) )
end end
end end
...@@ -180,8 +257,8 @@ describe Projects::Serverless::FunctionsController do ...@@ -180,8 +257,8 @@ describe Projects::Serverless::FunctionsController do
'knative_installed' => 'checking', 'knative_installed' => 'checking',
'functions' => [ 'functions' => [
a_hash_including( a_hash_including(
'name' => project.name, 'name' => function_name,
'url' => "http://#{project.name}.#{namespace.namespace}.example.com", 'url' => "http://#{function_name}.#{namespace.namespace}.example.com",
'description' => function_description 'description' => function_description
) )
] ]
......
...@@ -154,12 +154,12 @@ describe Projects::ServicesController do ...@@ -154,12 +154,12 @@ describe Projects::ServicesController do
end end
end end
context 'when activating Jira service from instance level service' do context 'when activating Jira service from a template' do
let(:service) do let(:service) do
create(:jira_service, project: project, instance: true) create(:jira_service, project: project, template: true)
end end
it 'activate Jira service from instance level service' do it 'activate Jira service from template' do
expect(flash[:notice]).to eq 'Jira activated.' expect(flash[:notice]).to eq 'Jira activated.'
end end
end end
......
...@@ -380,5 +380,9 @@ x6zG6WoibsbsJMj70nwseUnPTBQNDP+j61RJjC/r ...@@ -380,5 +380,9 @@ x6zG6WoibsbsJMj70nwseUnPTBQNDP+j61RJjC/r
scope { :instance } scope { :instance }
usage { :serverless } usage { :serverless }
end end
trait :with_project do
association :project
end
end end
end end
...@@ -153,8 +153,8 @@ describe Projects::Serverless::FunctionsFinder do ...@@ -153,8 +153,8 @@ describe Projects::Serverless::FunctionsFinder do
*knative_services_finder.cache_args) *knative_services_finder.cache_args)
result = finder.service(cluster.environment_scope, cluster.project.name) result = finder.service(cluster.environment_scope, cluster.project.name)
expect(result).not_to be_empty expect(result).to be_present
expect(result["metadata"]["name"]).to be_eql(cluster.project.name) expect(result.name).to be_eql(cluster.project.name)
end end
it 'has metrics', :use_clean_rails_memory_store_caching do it 'has metrics', :use_clean_rails_memory_store_caching do
......
...@@ -2736,7 +2736,7 @@ Service ...@@ -2736,7 +2736,7 @@ Service
when repository is empty when repository is empty
test runs execute test runs execute
Template Template
.build_from_instance .build_from_template
when template is invalid when template is invalid
sets service template to inactive when template is invalid sets service template to inactive when template is invalid
for pushover service for pushover service
......
import { mount, shallowMount } from '@vue/test-utils';
import axios from '~/lib/utils/axios_utils';
import MockAdapter from 'axios-mock-adapter';
import Container from '~/environments/components/container.vue';
import EmptyState from '~/environments/components/empty_state.vue';
import EnvironmentsApp from '~/environments/components/environments_app.vue';
import { environment, folder } from './mock_data';
describe('Environment', () => {
let mock;
let wrapper;
const mockData = {
endpoint: 'environments.json',
canCreateEnvironment: true,
canReadEnvironment: true,
newEnvironmentPath: 'environments/new',
helpPagePath: 'help',
canaryDeploymentFeatureId: 'canary_deployment',
showCanaryDeploymentCallout: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
};
const mockRequest = (response, body) => {
mock.onGet(mockData.endpoint).reply(response, body, {
'X-nExt-pAge': '2',
'x-page': '1',
'X-Per-Page': '1',
'X-Prev-Page': '',
'X-TOTAL': '37',
'X-Total-Pages': '2',
});
};
const createWrapper = (shallow = false) => {
const fn = shallow ? shallowMount : mount;
wrapper = fn(EnvironmentsApp, { propsData: mockData });
return axios.waitForAll();
};
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
wrapper.destroy();
mock.restore();
});
describe('successful request', () => {
describe('without environments', () => {
beforeEach(() => {
mockRequest(200, { environments: [] });
return createWrapper(true);
});
it('should render the empty state', () => {
expect(wrapper.find(EmptyState).exists()).toBe(true);
});
describe('when it is possible to enable a review app', () => {
beforeEach(() => {
mockRequest(200, { environments: [], review_app: { can_setup_review_app: true } });
return createWrapper();
});
it('should render the enable review app button', () => {
expect(wrapper.find('.js-enable-review-app-button').text()).toContain(
'Enable review app',
);
});
});
});
describe('with paginated environments', () => {
const environmentList = [environment];
beforeEach(() => {
mockRequest(200, {
environments: environmentList,
stopped_count: 1,
available_count: 0,
});
return createWrapper();
});
it('should render a conatiner table with environments', () => {
const containerTable = wrapper.find(Container);
expect(containerTable.exists()).toBe(true);
expect(containerTable.props('environments').length).toEqual(environmentList.length);
expect(containerTable.find('.environment-name').text()).toEqual(environmentList[0].name);
});
describe('pagination', () => {
it('should render pagination', () => {
expect(wrapper.findAll('.gl-pagination li').length).toEqual(9);
});
it('should make an API request when page is clicked', () => {
jest.spyOn(wrapper.vm, 'updateContent').mockImplementation(() => {});
wrapper.find('.gl-pagination li:nth-child(3) .page-link').trigger('click');
expect(wrapper.vm.updateContent).toHaveBeenCalledWith({ scope: 'available', page: '2' });
});
it('should make an API request when using tabs', () => {
jest.spyOn(wrapper.vm, 'updateContent').mockImplementation(() => {});
wrapper.find('.js-environments-tab-stopped').trigger('click');
expect(wrapper.vm.updateContent).toHaveBeenCalledWith({ scope: 'stopped', page: '1' });
});
});
});
});
describe('unsuccessful request', () => {
beforeEach(() => {
mockRequest(500, {});
return createWrapper(true);
});
it('should render empty state', () => {
expect(wrapper.find(EmptyState).exists()).toBe(true);
});
});
describe('expandable folders', () => {
beforeEach(() => {
mockRequest(200, {
environments: [folder],
stopped_count: 1,
available_count: 0,
});
mock.onGet(environment.folder_path).reply(200, { environments: [environment] });
return createWrapper().then(() => {
// open folder
wrapper.find('.folder-name').trigger('click');
return axios.waitForAll();
});
});
it('should open a closed folder', () => {
expect(wrapper.find('.folder-icon.ic-chevron-right').exists()).toBe(false);
});
it('should close an opened folder', () => {
expect(wrapper.find('.folder-icon.ic-chevron-down').exists()).toBe(true);
// close folder
wrapper.find('.folder-name').trigger('click');
wrapper.vm.$nextTick(() => {
expect(wrapper.find('.folder-icon.ic-chevron-down').exists()).toBe(false);
});
});
it('should show children environments', () => {
expect(wrapper.findAll('.js-child-row').length).toEqual(1);
});
it('should show a button to show all environments', () => {
expect(wrapper.find('.text-center > a.btn').text()).toContain('Show all');
});
});
});
...@@ -131,20 +131,17 @@ describe('DashboardsDropdown', () => { ...@@ -131,20 +131,17 @@ describe('DashboardsDropdown', () => {
expect(findModal().contains(DuplicateDashboardForm)).toBe(true); expect(findModal().contains(DuplicateDashboardForm)).toBe(true);
}); });
it('saves a new dashboard', done => { it('saves a new dashboard', () => {
findModal().vm.$emit('ok', okEvent); findModal().vm.$emit('ok', okEvent);
waitForPromises() return waitForPromises().then(() => {
.then(() => {
expect(okEvent.preventDefault).toHaveBeenCalled(); expect(okEvent.preventDefault).toHaveBeenCalled();
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.vm.$refs.duplicateDashboardModal.hide).toHaveBeenCalled(); expect(wrapper.vm.$refs.duplicateDashboardModal.hide).toHaveBeenCalled();
expect(wrapper.emitted().selectDashboard).toBeTruthy(); expect(wrapper.emitted().selectDashboard).toBeTruthy();
expect(findAlert().exists()).toBe(false); expect(findAlert().exists()).toBe(false);
done(); });
})
.catch(done.fail);
}); });
describe('when a new dashboard is saved succesfully', () => { describe('when a new dashboard is saved succesfully', () => {
...@@ -167,41 +164,34 @@ describe('DashboardsDropdown', () => { ...@@ -167,41 +164,34 @@ describe('DashboardsDropdown', () => {
findModal().vm.$emit('ok', okEvent); findModal().vm.$emit('ok', okEvent);
}; };
it('to the default branch, redirects to the new dashboard', done => { it('to the default branch, redirects to the new dashboard', () => {
submitForm({ submitForm({
branch: defaultBranch, branch: defaultBranch,
}); });
waitForPromises() return waitForPromises().then(() => {
.then(() => {
expect(wrapper.emitted().selectDashboard[0][0]).toEqual(newDashboard); expect(wrapper.emitted().selectDashboard[0][0]).toEqual(newDashboard);
done(); });
})
.catch(done.fail);
}); });
it('to a new branch refreshes in the current dashboard', done => { it('to a new branch refreshes in the current dashboard', () => {
submitForm({ submitForm({
branch: 'another-branch', branch: 'another-branch',
}); });
waitForPromises() return waitForPromises().then(() => {
.then(() => {
expect(wrapper.emitted().selectDashboard[0][0]).toEqual(dashboardGitResponse[0]); expect(wrapper.emitted().selectDashboard[0][0]).toEqual(dashboardGitResponse[0]);
done(); });
})
.catch(done.fail);
}); });
}); });
it('handles error when a new dashboard is not saved', done => { it('handles error when a new dashboard is not saved', () => {
const errMsg = 'An error occurred'; const errMsg = 'An error occurred';
duplicateDashboardAction.mockRejectedValueOnce(errMsg); duplicateDashboardAction.mockRejectedValueOnce(errMsg);
findModal().vm.$emit('ok', okEvent); findModal().vm.$emit('ok', okEvent);
waitForPromises() return waitForPromises().then(() => {
.then(() => {
expect(okEvent.preventDefault).toHaveBeenCalled(); expect(okEvent.preventDefault).toHaveBeenCalled();
expect(findAlert().exists()).toBe(true); expect(findAlert().exists()).toBe(true);
...@@ -209,10 +199,7 @@ describe('DashboardsDropdown', () => { ...@@ -209,10 +199,7 @@ describe('DashboardsDropdown', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.vm.$refs.duplicateDashboardModal.hide).not.toHaveBeenCalled(); expect(wrapper.vm.$refs.duplicateDashboardModal.hide).not.toHaveBeenCalled();
});
done();
})
.catch(done.fail);
}); });
it('id is correct, as the value of modal directive binding matches modal id', () => { it('id is correct, as the value of modal directive binding matches modal id', () => {
......
...@@ -44,30 +44,27 @@ describe('DuplicateDashboardForm', () => { ...@@ -44,30 +44,27 @@ describe('DuplicateDashboardForm', () => {
describe('validates the file name', () => { describe('validates the file name', () => {
const findInvalidFeedback = () => findByRef('fileNameFormGroup').find('.invalid-feedback'); const findInvalidFeedback = () => findByRef('fileNameFormGroup').find('.invalid-feedback');
it('when is empty', done => { it('when is empty', () => {
setValue('fileName', ''); setValue('fileName', '');
wrapper.vm.$nextTick(() => { return wrapper.vm.$nextTick(() => {
expect(findByRef('fileNameFormGroup').is('.is-valid')).toBe(true); expect(findByRef('fileNameFormGroup').is('.is-valid')).toBe(true);
expect(findInvalidFeedback().exists()).toBe(false); expect(findInvalidFeedback().exists()).toBe(false);
done();
}); });
}); });
it('when is valid', done => { it('when is valid', () => {
setValue('fileName', 'my_dashboard.yml'); setValue('fileName', 'my_dashboard.yml');
wrapper.vm.$nextTick(() => { return wrapper.vm.$nextTick(() => {
expect(findByRef('fileNameFormGroup').is('.is-valid')).toBe(true); expect(findByRef('fileNameFormGroup').is('.is-valid')).toBe(true);
expect(findInvalidFeedback().exists()).toBe(false); expect(findInvalidFeedback().exists()).toBe(false);
done();
}); });
}); });
it('when is not valid', done => { it('when is not valid', () => {
setValue('fileName', 'my_dashboard.exe'); setValue('fileName', 'my_dashboard.exe');
wrapper.vm.$nextTick(() => { return wrapper.vm.$nextTick(() => {
expect(findByRef('fileNameFormGroup').is('.is-invalid')).toBe(true); expect(findByRef('fileNameFormGroup').is('.is-invalid')).toBe(true);
expect(findInvalidFeedback().text()).toBeTruthy(); expect(findInvalidFeedback().text()).toBeTruthy();
done();
}); });
}); });
}); });
...@@ -124,30 +121,26 @@ describe('DuplicateDashboardForm', () => { ...@@ -124,30 +121,26 @@ describe('DuplicateDashboardForm', () => {
}); });
}); });
it('when a `default` branch option is set, branch input is invisible and ignored', done => { it('when a `default` branch option is set, branch input is invisible and ignored', () => {
setChecked(wrapper.vm.$options.radioVals.DEFAULT); setChecked(wrapper.vm.$options.radioVals.DEFAULT);
setValue('branchName', 'a-new-branch'); setValue('branchName', 'a-new-branch');
expect(lastChange()).resolves.toMatchObject({ expect(lastChange()).resolves.toMatchObject({
branch: defaultBranch, branch: defaultBranch,
}); });
wrapper.vm.$nextTick(() => {
return wrapper.vm.$nextTick(() => {
expect(findByRef('branchName').isVisible()).toBe(false); expect(findByRef('branchName').isVisible()).toBe(false);
done();
}); });
}); });
it('when `new` branch option is chosen, focuses on the branch name input', done => { it('when `new` branch option is chosen, focuses on the branch name input', () => {
setChecked(wrapper.vm.$options.radioVals.NEW); setChecked(wrapper.vm.$options.radioVals.NEW);
wrapper.vm return wrapper.vm.$nextTick().then(() => {
.$nextTick()
.then(() => {
wrapper.find('form').trigger('change'); wrapper.find('form').trigger('change');
expect(findByRef('branchName').is(':focus')).toBe(true); expect(findByRef('branchName').is(':focus')).toBe(true);
}) });
.then(done)
.catch(done.fail);
}); });
}); });
}); });
...@@ -32,25 +32,23 @@ describe('Graph group component', () => { ...@@ -32,25 +32,23 @@ describe('Graph group component', () => {
expect(findCaretIcon().props('name')).toBe('angle-down'); expect(findCaretIcon().props('name')).toBe('angle-down');
}); });
it('should show the angle-right caret icon when the user collapses the group', done => { it('should show the angle-right caret icon when the user collapses the group', () => {
wrapper.vm.collapse(); wrapper.vm.collapse();
wrapper.vm.$nextTick(() => { return wrapper.vm.$nextTick(() => {
expect(findContent().isVisible()).toBe(false); expect(findContent().isVisible()).toBe(false);
expect(findCaretIcon().props('name')).toBe('angle-right'); expect(findCaretIcon().props('name')).toBe('angle-right');
done();
}); });
}); });
it('should show the open the group when collapseGroup is set to true', done => { it('should show the open the group when collapseGroup is set to true', () => {
wrapper.setProps({ wrapper.setProps({
collapseGroup: true, collapseGroup: true,
}); });
wrapper.vm.$nextTick(() => { return wrapper.vm.$nextTick(() => {
expect(findContent().isVisible()).toBe(true); expect(findContent().isVisible()).toBe(true);
expect(findCaretIcon().props('name')).toBe('angle-down'); expect(findCaretIcon().props('name')).toBe('angle-down');
done();
}); });
}); });
...@@ -102,13 +100,12 @@ describe('Graph group component', () => { ...@@ -102,13 +100,12 @@ describe('Graph group component', () => {
expect(findCaretIcon().exists()).toBe(false); expect(findCaretIcon().exists()).toBe(false);
}); });
it('should show the panel content when clicked', done => { it('should show the panel content when clicked', () => {
wrapper.vm.collapse(); wrapper.vm.collapse();
wrapper.vm.$nextTick(() => { return wrapper.vm.$nextTick(() => {
expect(findContent().isVisible()).toBe(true); expect(findContent().isVisible()).toBe(true);
expect(findCaretIcon().exists()).toBe(false); expect(findCaretIcon().exists()).toBe(false);
done();
}); });
}); });
}); });
......
...@@ -28,6 +28,8 @@ describe('Panel Type component', () => { ...@@ -28,6 +28,8 @@ describe('Panel Type component', () => {
const exampleText = 'example_text'; const exampleText = 'example_text';
const findCopyLink = () => wrapper.find({ ref: 'copyChartLink' });
const createWrapper = props => { const createWrapper = props => {
wrapper = shallowMount(PanelType, { wrapper = shallowMount(PanelType, {
propsData: { propsData: {
...@@ -96,8 +98,7 @@ describe('Panel Type component', () => { ...@@ -96,8 +98,7 @@ describe('Panel Type component', () => {
}); });
it('sets no clipboard copy link on dropdown by default', () => { it('sets no clipboard copy link on dropdown by default', () => {
const link = () => wrapper.find({ ref: 'copyChartLink' }); expect(findCopyLink().exists()).toBe(false);
expect(link().exists()).toBe(false);
}); });
describe('Time Series Chart panel type', () => { describe('Time Series Chart panel type', () => {
...@@ -204,7 +205,6 @@ describe('Panel Type component', () => { ...@@ -204,7 +205,6 @@ describe('Panel Type component', () => {
}); });
describe('when cliboard data is available', () => { describe('when cliboard data is available', () => {
const link = () => wrapper.find({ ref: 'copyChartLink' });
const clipboardText = 'A value to copy.'; const clipboardText = 'A value to copy.';
beforeEach(() => { beforeEach(() => {
...@@ -219,16 +219,16 @@ describe('Panel Type component', () => { ...@@ -219,16 +219,16 @@ describe('Panel Type component', () => {
}); });
it('sets clipboard text on the dropdown', () => { it('sets clipboard text on the dropdown', () => {
expect(link().exists()).toBe(true); expect(findCopyLink().exists()).toBe(true);
expect(link().element.dataset.clipboardText).toBe(clipboardText); expect(findCopyLink().element.dataset.clipboardText).toBe(clipboardText);
}); });
it('adds a copy button to the dropdown', () => { it('adds a copy button to the dropdown', () => {
expect(link().text()).toContain('Generate link to chart'); expect(findCopyLink().text()).toContain('Generate link to chart');
}); });
it('opens a toast on click', () => { it('opens a toast on click', () => {
link().vm.$emit('click'); findCopyLink().vm.$emit('click');
expect(wrapper.vm.$toast.show).toHaveBeenCalled(); expect(wrapper.vm.$toast.show).toHaveBeenCalled();
}); });
......
...@@ -4,10 +4,9 @@ require 'spec_helper' ...@@ -4,10 +4,9 @@ require 'spec_helper'
describe GitlabSchema.types['SnippetBlob'] do describe GitlabSchema.types['SnippetBlob'] do
it 'has the correct fields' do it 'has the correct fields' do
expected_fields = [:highlighted_data, :raw_path, expected_fields = [:highlighted_data, :plain_highlighted_data,
:size, :binary, :name, :path, :raw_path, :size, :binary, :name, :path,
:simple_viewer, :rich_viewer, :simple_viewer, :rich_viewer, :mode]
:mode]
is_expected.to have_graphql_fields(*expected_fields) is_expected.to have_graphql_fields(*expected_fields)
end end
......
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import axios from '~/lib/utils/axios_utils';
import environmentsComponent from '~/environments/components/environments_app.vue';
import { environment, folder } from './mock_data';
describe('Environment', () => {
const mockData = {
endpoint: 'environments.json',
canCreateEnvironment: true,
canReadEnvironment: true,
newEnvironmentPath: 'environments/new',
helpPagePath: 'help',
canaryDeploymentFeatureId: 'canary_deployment',
showCanaryDeploymentCallout: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
};
let EnvironmentsComponent;
let component;
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
EnvironmentsComponent = Vue.extend(environmentsComponent);
});
afterEach(() => {
component.$destroy();
mock.restore();
});
describe('successful request', () => {
describe('without environments', () => {
beforeEach(done => {
mock.onGet(mockData.endpoint).reply(200, { environments: [] });
component = mountComponent(EnvironmentsComponent, mockData);
setTimeout(() => {
done();
}, 0);
});
it('should render the empty state', () => {
expect(component.$el.querySelector('.js-new-environment-button').textContent).toContain(
'New environment',
);
expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain(
"You don't have any environments right now",
);
});
describe('when it is possible to enable a review app', () => {
beforeEach(done => {
mock
.onGet(mockData.endpoint)
.reply(200, { environments: [], review_app: { can_setup_review_app: true } });
component = mountComponent(EnvironmentsComponent, mockData);
setTimeout(() => {
done();
}, 0);
});
it('should render the enable review app button', () => {
expect(component.$el.querySelector('.js-enable-review-app-button').textContent).toContain(
'Enable review app',
);
});
});
});
describe('with paginated environments', () => {
beforeEach(done => {
mock.onGet(mockData.endpoint).reply(
200,
{
environments: [environment],
stopped_count: 1,
available_count: 0,
},
{
'X-nExt-pAge': '2',
'x-page': '1',
'X-Per-Page': '1',
'X-Prev-Page': '',
'X-TOTAL': '37',
'X-Total-Pages': '2',
},
);
component = mountComponent(EnvironmentsComponent, mockData);
setTimeout(() => {
done();
}, 0);
});
it('should render a table with environments', () => {
expect(component.$el.querySelectorAll('table')).not.toBeNull();
expect(component.$el.querySelector('.environment-name').textContent.trim()).toEqual(
environment.name,
);
});
describe('pagination', () => {
it('should render pagination', () => {
expect(component.$el.querySelectorAll('.gl-pagination li').length).toEqual(9);
});
it('should make an API request when page is clicked', done => {
spyOn(component, 'updateContent');
setTimeout(() => {
component.$el.querySelector('.gl-pagination li:nth-child(3) .page-link').click();
expect(component.updateContent).toHaveBeenCalledWith({ scope: 'available', page: '2' });
done();
}, 0);
});
it('should make an API request when using tabs', done => {
setTimeout(() => {
spyOn(component, 'updateContent');
component.$el.querySelector('.js-environments-tab-stopped').click();
expect(component.updateContent).toHaveBeenCalledWith({ scope: 'stopped', page: '1' });
done();
}, 0);
});
});
});
});
describe('unsuccessfull request', () => {
beforeEach(done => {
mock.onGet(mockData.endpoint).reply(500, {});
component = mountComponent(EnvironmentsComponent, mockData);
setTimeout(() => {
done();
}, 0);
});
it('should render empty state', () => {
expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain(
"You don't have any environments right now",
);
});
});
describe('expandable folders', () => {
beforeEach(() => {
mock.onGet(mockData.endpoint).reply(
200,
{
environments: [folder],
stopped_count: 0,
available_count: 1,
},
{
'X-nExt-pAge': '2',
'x-page': '1',
'X-Per-Page': '1',
'X-Prev-Page': '',
'X-TOTAL': '37',
'X-Total-Pages': '2',
},
);
mock.onGet(environment.folder_path).reply(200, { environments: [environment] });
component = mountComponent(EnvironmentsComponent, mockData);
});
it('should open a closed folder', done => {
setTimeout(() => {
component.$el.querySelector('.folder-name').click();
Vue.nextTick(() => {
expect(component.$el.querySelector('.folder-icon.ic-chevron-right')).toBe(null);
done();
});
}, 0);
});
it('should close an opened folder', done => {
setTimeout(() => {
// open folder
component.$el.querySelector('.folder-name').click();
Vue.nextTick(() => {
// close folder
component.$el.querySelector('.folder-name').click();
Vue.nextTick(() => {
expect(component.$el.querySelector('.folder-icon.ic-chevron-down')).toBe(null);
done();
});
});
}, 0);
});
it('should show children environments and a button to show all environments', done => {
setTimeout(() => {
// open folder
component.$el.querySelector('.folder-name').click();
Vue.nextTick(() => {
// wait for next async request
setTimeout(() => {
expect(component.$el.querySelectorAll('.js-child-row').length).toEqual(1);
expect(component.$el.querySelector('.text-center > a.btn').textContent).toContain(
'Show all',
);
done();
});
});
}, 0);
});
});
describe('methods', () => {
beforeEach(() => {
mock.onGet(mockData.endpoint).reply(
200,
{
environments: [],
stopped_count: 0,
available_count: 1,
},
{},
);
component = mountComponent(EnvironmentsComponent, mockData);
spyOn(window.history, 'pushState').and.stub();
});
describe('updateContent', () => {
it('should set given parameters', done => {
component
.updateContent({ scope: 'stopped', page: '3' })
.then(() => {
expect(component.page).toEqual('3');
expect(component.scope).toEqual('stopped');
expect(component.requestData.scope).toEqual('stopped');
expect(component.requestData.page).toEqual('3');
done();
})
.catch(done.fail);
});
});
describe('onChangeTab', () => {
it('should set page to 1', () => {
spyOn(component, 'updateContent');
component.onChangeTab('stopped');
expect(component.updateContent).toHaveBeenCalledWith({ scope: 'stopped', page: '1' });
});
});
describe('onChangePage', () => {
it('should update page and keep scope', () => {
spyOn(component, 'updateContent');
component.onChangePage(4);
expect(component.updateContent).toHaveBeenCalledWith({ scope: component.scope, page: '4' });
});
});
});
});
...@@ -652,10 +652,10 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do ...@@ -652,10 +652,10 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
setup_import_export_config('light') setup_import_export_config('light')
end end
it 'does not import any instance-level services' do it 'does not import any templated services' do
expect(restored_project_json).to eq(true) expect(restored_project_json).to eq(true)
expect(project.services.where(instance: true).count).to eq(0) expect(project.services.where(template: true).count).to eq(0)
end end
it 'imports labels' do it 'imports labels' do
......
...@@ -453,7 +453,7 @@ Service: ...@@ -453,7 +453,7 @@ Service:
- updated_at - updated_at
- active - active
- properties - properties
- instance - template
- push_events - push_events
- issues_events - issues_events
- commit_events - commit_events
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Serverless::Service do
let(:cluster) { create(:cluster) }
let(:environment) { create(:environment) }
let(:attributes) do
{
'apiVersion' => 'serving.knative.dev/v1alpha1',
'kind' => 'Service',
'metadata' => {
'creationTimestamp' => '2019-10-22T21:19:13Z',
'name' => 'kubetest',
'namespace' => 'project1-1-environment1'
},
'spec' => {
'runLatest' => {
'configuration' => {
'build' => {
'template' => {
'name' => 'some-image'
}
}
}
}
},
'environment_scope' => '*',
'cluster' => cluster,
'environment' => environment,
'podcount' => 0
}
end
it 'exposes methods extracting data from the attributes hash' do
service = Gitlab::Serverless::Service.new(attributes)
expect(service.name).to eq('kubetest')
expect(service.namespace).to eq('project1-1-environment1')
expect(service.environment_scope).to eq('*')
expect(service.podcount).to eq(0)
expect(service.created_at).to eq(DateTime.parse('2019-10-22T21:19:13Z'))
expect(service.image).to eq('some-image')
expect(service.cluster).to eq(cluster)
expect(service.environment).to eq(environment)
end
it 'returns nil for missing attributes' do
service = Gitlab::Serverless::Service.new({})
[:name, :namespace, :environment_scope, :cluster, :podcount, :created_at, :image, :description, :url, :environment].each do |method|
expect(service.send(method)).to be_nil
end
end
describe '#description' do
it 'extracts the description in knative 7 format if available' do
attributes = {
'spec' => {
'template' => {
'metadata' => {
'annotations' => {
'Description' => 'some description'
}
}
}
}
}
service = Gitlab::Serverless::Service.new(attributes)
expect(service.description).to eq('some description')
end
it 'extracts the description in knative 5/6 format if 7 is not available' do
attributes = {
'spec' => {
'runLatest' => {
'configuration' => {
'revisionTemplate' => {
'metadata' => {
'annotations' => {
'Description' => 'some description'
}
}
}
}
}
}
}
service = Gitlab::Serverless::Service.new(attributes)
expect(service.description).to eq('some description')
end
end
describe '#url' do
it 'returns proxy URL if cluster has serverless domain' do
# cluster = create(:cluster)
knative = create(:clusters_applications_knative, :installed, cluster: cluster)
create(:serverless_domain_cluster, clusters_applications_knative_id: knative.id)
service = Gitlab::Serverless::Service.new(attributes.merge('cluster' => cluster))
expect(Gitlab::Serverless::FunctionURI).to receive(:new).with(
function: service.name,
cluster: service.cluster.serverless_domain,
environment: service.environment
).and_return('https://proxy.example.com')
expect(service.url).to eq('https://proxy.example.com')
end
it 'returns the URL from the knative 6/7 format' do
attributes = {
'status' => {
'url' => 'https://example.com'
}
}
service = Gitlab::Serverless::Service.new(attributes)
expect(service.url).to eq('https://example.com')
end
it 'returns the URL from the knative 5 format' do
attributes = {
'status' => {
'domain' => 'example.com'
}
}
service = Gitlab::Serverless::Service.new(attributes)
expect(service.url).to eq('http://example.com')
end
end
end
...@@ -18,7 +18,7 @@ describe Gitlab::UsageData do ...@@ -18,7 +18,7 @@ describe Gitlab::UsageData do
create(:service, project: projects[1], type: 'SlackService', active: true) create(:service, project: projects[1], type: 'SlackService', active: true)
create(:service, project: projects[2], type: 'SlackService', active: true) create(:service, project: projects[2], type: 'SlackService', active: true)
create(:service, project: projects[2], type: 'MattermostService', active: false) create(:service, project: projects[2], type: 'MattermostService', active: false)
create(:service, project: projects[2], type: 'MattermostService', active: true, instance: true) create(:service, project: projects[2], type: 'MattermostService', active: true, template: true)
create(:service, project: projects[2], type: 'CustomIssueTrackerService', active: true) create(:service, project: projects[2], type: 'CustomIssueTrackerService', active: true)
create(:project_error_tracking_setting, project: projects[0]) create(:project_error_tracking_setting, project: projects[0])
create(:project_error_tracking_setting, project: projects[1], enabled: false) create(:project_error_tracking_setting, project: projects[1], enabled: false)
......
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20200206111847_migrate_propagate_service_template_sidekiq_queue.rb')
describe MigratePropagateServiceTemplateSidekiqQueue, :sidekiq, :redis do
include Gitlab::Database::MigrationHelpers
include StubWorker
context 'when there are jobs in the queue' do
it 'correctly migrates queue when migrating up' do
Sidekiq::Testing.disable! do
stub_worker(queue: 'propagate_service_template').perform_async('Something', [1])
stub_worker(queue: 'propagate_instance_level_service').perform_async('Something', [1])
described_class.new.up
expect(sidekiq_queue_length('propagate_service_template')).to eq 0
expect(sidekiq_queue_length('propagate_instance_level_service')).to eq 2
end
end
end
context 'when there are no jobs in the queues' do
it 'does not raise error when migrating up' do
expect { described_class.new.up }.not_to raise_error
end
end
end
...@@ -97,23 +97,23 @@ describe Service do ...@@ -97,23 +97,23 @@ describe Service do
end end
end end
describe "Instance" do describe "Template" do
let(:project) { create(:project) } let(:project) { create(:project) }
describe '.build_from_instance' do describe '.build_from_template' do
context 'when instance level integration is invalid' do context 'when template is invalid' do
it 'sets instance level integration to inactive when instance is invalid' do it 'sets service template to inactive when template is invalid' do
instance = build(:prometheus_service, instance: true, active: true, properties: {}) template = build(:prometheus_service, template: true, active: true, properties: {})
instance.save(validate: false) template.save(validate: false)
service = described_class.build_from_instance(project.id, instance) service = described_class.build_from_template(project.id, template)
expect(service).to be_valid expect(service).to be_valid
expect(service.active).to be false expect(service.active).to be false
end end
end end
describe 'build issue tracker from a instance level integration' do describe 'build issue tracker from a template' do
let(:title) { 'custom title' } let(:title) { 'custom title' }
let(:description) { 'custom description' } let(:description) { 'custom description' }
let(:url) { 'http://jira.example.com' } let(:url) { 'http://jira.example.com' }
...@@ -127,9 +127,9 @@ describe Service do ...@@ -127,9 +127,9 @@ describe Service do
} }
end end
shared_examples 'integration creation from instance level' do shared_examples 'service creation from a template' do
it 'creates a correct service' do it 'creates a correct service' do
service = described_class.build_from_instance(project.id, instance_level_integration) service = described_class.build_from_template(project.id, template)
expect(service).to be_active expect(service).to be_active
expect(service.title).to eq(title) expect(service.title).to eq(title)
...@@ -144,38 +144,38 @@ describe Service do ...@@ -144,38 +144,38 @@ describe Service do
# this will be removed as part of https://gitlab.com/gitlab-org/gitlab/issues/29404 # this will be removed as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
context 'when data are stored in properties' do context 'when data are stored in properties' do
let(:properties) { data_params.merge(title: title, description: description) } let(:properties) { data_params.merge(title: title, description: description) }
let!(:instance_level_integration) do let!(:template) do
create(:jira_service, :without_properties_callback, instance: true, properties: properties.merge(additional: 'something')) create(:jira_service, :without_properties_callback, template: true, properties: properties.merge(additional: 'something'))
end end
it_behaves_like 'integration creation from instance level' it_behaves_like 'service creation from a template'
end end
context 'when data are stored in separated fields' do context 'when data are stored in separated fields' do
let(:instance_level_integration) do let(:template) do
create(:jira_service, data_params.merge(properties: {}, title: title, description: description, instance: true)) create(:jira_service, data_params.merge(properties: {}, title: title, description: description, template: true))
end end
it_behaves_like 'integration creation from instance level' it_behaves_like 'service creation from a template'
end end
context 'when data are stored in both properties and separated fields' do context 'when data are stored in both properties and separated fields' do
let(:properties) { data_params.merge(title: title, description: description) } let(:properties) { data_params.merge(title: title, description: description) }
let(:instance_level_integration) do let(:template) do
create(:jira_service, :without_properties_callback, active: true, instance: true, properties: properties).tap do |service| create(:jira_service, :without_properties_callback, active: true, template: true, properties: properties).tap do |service|
create(:jira_tracker_data, data_params.merge(service: service)) create(:jira_tracker_data, data_params.merge(service: service))
end end
end end
it_behaves_like 'integration creation from instance level' it_behaves_like 'service creation from a template'
end end
end end
end end
describe "for pushover service" do describe "for pushover service" do
let!(:instance_level_integration) do let!(:service_template) do
PushoverService.create( PushoverService.create(
instance: true, template: true,
properties: { properties: {
device: 'MyDevice', device: 'MyDevice',
sound: 'mic', sound: 'mic',
...@@ -188,7 +188,7 @@ describe Service do ...@@ -188,7 +188,7 @@ describe Service do
it "has all fields prefilled" do it "has all fields prefilled" do
service = project.find_or_initialize_service('pushover') service = project.find_or_initialize_service('pushover')
expect(service.instance).to eq(false) expect(service.template).to eq(false)
expect(service.device).to eq('MyDevice') expect(service.device).to eq('MyDevice')
expect(service.sound).to eq('mic') expect(service.sound).to eq('mic')
expect(service.priority).to eq(4) expect(service.priority).to eq(4)
...@@ -391,6 +391,14 @@ describe Service do ...@@ -391,6 +391,14 @@ describe Service do
end end
end end
describe '.find_by_template' do
let!(:service) { create(:service, template: true) }
it 'returns service template' do
expect(described_class.find_by_template).to eq(service)
end
end
describe '#api_field_names' do describe '#api_field_names' do
let(:fake_service) do let(:fake_service) do
Class.new(Service) do Class.new(Service) do
......
...@@ -18,7 +18,7 @@ describe SnippetBlobPresenter do ...@@ -18,7 +18,7 @@ describe SnippetBlobPresenter do
snippet.file_name = 'test.md' snippet.file_name = 'test.md'
snippet.content = '*foo*' snippet.content = '*foo*'
expect(subject).to eq '<p data-sourcepos="1:1-1:5" dir="auto"><em>foo</em></p>' expect(subject).to eq '<span id="LC1" class="line" lang="markdown"><span class="ge">*foo*</span></span>'
end end
it 'returns syntax highlighted content' do it 'returns syntax highlighted content' do
...@@ -33,7 +33,41 @@ describe SnippetBlobPresenter do ...@@ -33,7 +33,41 @@ describe SnippetBlobPresenter do
snippet.file_name = 'test' snippet.file_name = 'test'
snippet.content = 'foo' snippet.content = 'foo'
expect(described_class.new(snippet.blob).highlighted_data).to eq '<span id="LC1" class="line" lang="plaintext">foo</span>' expect(subject).to eq '<span id="LC1" class="line" lang="plaintext">foo</span>'
end
end
describe '#plain_highlighted_data' do
let(:snippet) { build(:personal_snippet) }
subject { described_class.new(snippet.blob).plain_highlighted_data }
it 'returns nil when the snippet blob is binary' do
allow(snippet.blob).to receive(:binary?).and_return(true)
expect(subject).to be_nil
end
it 'returns plain content when snippet file is markup' do
snippet.file_name = 'test.md'
snippet.content = '*foo*'
expect(subject).to eq '<span id="LC1" class="line" lang="">*foo*</span>'
end
it 'returns plain syntax content' do
snippet.file_name = 'test.rb'
snippet.content = 'class Foo;end'
expect(subject)
.to eq '<span id="LC1" class="line" lang="">class Foo;end</span>'
end
it 'returns plain text highlighted content' do
snippet.file_name = 'test'
snippet.content = 'foo'
expect(subject).to eq '<span id="LC1" class="line" lang="">foo</span>'
end end
end end
......
...@@ -15,7 +15,7 @@ describe Projects::CreateService, '#execute' do ...@@ -15,7 +15,7 @@ describe Projects::CreateService, '#execute' do
} }
end end
it 'creates labels on Project creation if there are instance level services' do it 'creates labels on Project creation if there are templates' do
Label.create(title: "bug", template: true) Label.create(title: "bug", template: true)
project = create_project(user, opts) project = create_project(user, opts)
...@@ -96,7 +96,7 @@ describe Projects::CreateService, '#execute' do ...@@ -96,7 +96,7 @@ describe Projects::CreateService, '#execute' do
end end
it 'sets invalid service as inactive' do it 'sets invalid service as inactive' do
create(:service, type: 'JiraService', project: nil, instance: true, active: true) create(:service, type: 'JiraService', project: nil, template: true, active: true)
project = create_project(user, opts) project = create_project(user, opts)
service = project.services.first service = project.services.first
...@@ -342,22 +342,22 @@ describe Projects::CreateService, '#execute' do ...@@ -342,22 +342,22 @@ describe Projects::CreateService, '#execute' do
end end
end end
context 'when there is an active instance level service' do context 'when there is an active service template' do
before do before do
create(:service, project: nil, instance: true, active: true) create(:service, project: nil, template: true, active: true)
end end
it 'creates a service from instance level service' do it 'creates a service from this template' do
project = create_project(user, opts) project = create_project(user, opts)
expect(project.services.count).to eq 1 expect(project.services.count).to eq 1
end end
end end
context 'when a bad instance level service is created' do context 'when a bad service template is created' do
it 'sets service to be inactive' do it 'sets service to be inactive' do
opts[:import_url] = 'http://www.gitlab.com/gitlab-org/gitlab-foss' opts[:import_url] = 'http://www.gitlab.com/gitlab-org/gitlab-foss'
create(:service, type: 'DroneCiService', project: nil, instance: true, active: true) create(:service, type: 'DroneCiService', project: nil, template: true, active: true)
project = create_project(user, opts) project = create_project(user, opts)
service = project.services.first service = project.services.first
......
...@@ -2,11 +2,11 @@ ...@@ -2,11 +2,11 @@
require 'spec_helper' require 'spec_helper'
describe Projects::PropagateInstanceLevelService do describe Projects::PropagateServiceTemplate do
describe '.propagate' do describe '.propagate' do
let!(:instance_level_integration) do let!(:service_template) do
PushoverService.create( PushoverService.create(
instance: true, template: true,
active: true, active: true,
properties: { properties: {
device: 'MyDevice', device: 'MyDevice',
...@@ -22,14 +22,14 @@ describe Projects::PropagateInstanceLevelService do ...@@ -22,14 +22,14 @@ describe Projects::PropagateInstanceLevelService do
it 'creates services for projects' do it 'creates services for projects' do
expect(project.pushover_service).to be_nil expect(project.pushover_service).to be_nil
described_class.propagate(instance_level_integration) described_class.propagate(service_template)
expect(project.reload.pushover_service).to be_present expect(project.reload.pushover_service).to be_present
end end
it 'creates services for a project that has another service' do it 'creates services for a project that has another service' do
BambooService.create( BambooService.create(
instance: true, template: true,
active: true, active: true,
project: project, project: project,
properties: { properties: {
...@@ -42,14 +42,14 @@ describe Projects::PropagateInstanceLevelService do ...@@ -42,14 +42,14 @@ describe Projects::PropagateInstanceLevelService do
expect(project.pushover_service).to be_nil expect(project.pushover_service).to be_nil
described_class.propagate(instance_level_integration) described_class.propagate(service_template)
expect(project.reload.pushover_service).to be_present expect(project.reload.pushover_service).to be_present
end end
it 'does not create the service if it exists already' do it 'does not create the service if it exists already' do
other_service = BambooService.create( other_service = BambooService.create(
instance: true, template: true,
active: true, active: true,
properties: { properties: {
bamboo_url: 'http://gitlab.com', bamboo_url: 'http://gitlab.com',
...@@ -59,17 +59,17 @@ describe Projects::PropagateInstanceLevelService do ...@@ -59,17 +59,17 @@ describe Projects::PropagateInstanceLevelService do
} }
) )
Service.build_from_instance(project.id, instance_level_integration).save! Service.build_from_template(project.id, service_template).save!
Service.build_from_instance(project.id, other_service).save! Service.build_from_template(project.id, other_service).save!
expect { described_class.propagate(instance_level_integration) } expect { described_class.propagate(service_template) }
.not_to change { Service.count } .not_to change { Service.count }
end end
it 'creates the service containing the instance attributes' do it 'creates the service containing the template attributes' do
described_class.propagate(instance_level_integration) described_class.propagate(service_template)
expect(project.pushover_service.properties).to eq(instance_level_integration.properties) expect(project.pushover_service.properties).to eq(service_template.properties)
end end
describe 'bulk update', :use_sql_query_cache do describe 'bulk update', :use_sql_query_cache do
...@@ -80,7 +80,7 @@ describe Projects::PropagateInstanceLevelService do ...@@ -80,7 +80,7 @@ describe Projects::PropagateInstanceLevelService do
project_total.times { create(:project) } project_total.times { create(:project) }
described_class.propagate(instance_level_integration) described_class.propagate(service_template)
end end
it 'creates services for all projects' do it 'creates services for all projects' do
...@@ -90,18 +90,18 @@ describe Projects::PropagateInstanceLevelService do ...@@ -90,18 +90,18 @@ describe Projects::PropagateInstanceLevelService do
describe 'external tracker' do describe 'external tracker' do
it 'updates the project external tracker' do it 'updates the project external tracker' do
instance_level_integration.update!(category: 'issue_tracker', default: false) service_template.update!(category: 'issue_tracker', default: false)
expect { described_class.propagate(instance_level_integration) } expect { described_class.propagate(service_template) }
.to change { project.reload.has_external_issue_tracker }.to(true) .to change { project.reload.has_external_issue_tracker }.to(true)
end end
end end
describe 'external wiki' do describe 'external wiki' do
it 'updates the project external tracker' do it 'updates the project external tracker' do
instance_level_integration.update!(type: 'ExternalWikiService') service_template.update!(type: 'ExternalWikiService')
expect { described_class.propagate(instance_level_integration) } expect { described_class.propagate(service_template) }
.to change { project.reload.has_external_wiki }.to(true) .to change { project.reload.has_external_wiki }.to(true)
end end
end end
......
...@@ -557,7 +557,7 @@ module KubernetesHelpers ...@@ -557,7 +557,7 @@ module KubernetesHelpers
end end
# noinspection RubyStringKeysInHashInspection # noinspection RubyStringKeysInHashInspection
def knative_06_service(name: 'kubetest', namespace: 'default', domain: 'example.com', description: 'a knative service', environment: 'production') def knative_06_service(name: 'kubetest', namespace: 'default', domain: 'example.com', description: 'a knative service', environment: 'production', cluster_id: 9)
{ "apiVersion" => "serving.knative.dev/v1alpha1", { "apiVersion" => "serving.knative.dev/v1alpha1",
"kind" => "Service", "kind" => "Service",
"metadata" => "metadata" =>
...@@ -612,12 +612,12 @@ module KubernetesHelpers ...@@ -612,12 +612,12 @@ module KubernetesHelpers
"url" => "http://#{name}.#{namespace}.#{domain}" "url" => "http://#{name}.#{namespace}.#{domain}"
}, },
"environment_scope" => environment, "environment_scope" => environment,
"cluster_id" => 9, "cluster_id" => cluster_id,
"podcount" => 0 } "podcount" => 0 }
end end
# noinspection RubyStringKeysInHashInspection # noinspection RubyStringKeysInHashInspection
def knative_07_service(name: 'kubetest', namespace: 'default', domain: 'example.com', description: 'a knative service', environment: 'production') def knative_07_service(name: 'kubetest', namespace: 'default', domain: 'example.com', description: 'a knative service', environment: 'production', cluster_id: 5)
{ "apiVersion" => "serving.knative.dev/v1alpha1", { "apiVersion" => "serving.knative.dev/v1alpha1",
"kind" => "Service", "kind" => "Service",
"metadata" => "metadata" =>
...@@ -664,12 +664,12 @@ module KubernetesHelpers ...@@ -664,12 +664,12 @@ module KubernetesHelpers
"traffic" => [{ "latestRevision" => true, "percent" => 100, "revisionName" => "#{name}-92tsj" }], "traffic" => [{ "latestRevision" => true, "percent" => 100, "revisionName" => "#{name}-92tsj" }],
"url" => "http://#{name}.#{namespace}.#{domain}" }, "url" => "http://#{name}.#{namespace}.#{domain}" },
"environment_scope" => environment, "environment_scope" => environment,
"cluster_id" => 5, "cluster_id" => cluster_id,
"podcount" => 0 } "podcount" => 0 }
end end
# noinspection RubyStringKeysInHashInspection # noinspection RubyStringKeysInHashInspection
def knative_09_service(name: 'kubetest', namespace: 'default', domain: 'example.com', description: 'a knative service', environment: 'production') def knative_09_service(name: 'kubetest', namespace: 'default', domain: 'example.com', description: 'a knative service', environment: 'production', cluster_id: 5)
{ "apiVersion" => "serving.knative.dev/v1alpha1", { "apiVersion" => "serving.knative.dev/v1alpha1",
"kind" => "Service", "kind" => "Service",
"metadata" => "metadata" =>
...@@ -716,12 +716,12 @@ module KubernetesHelpers ...@@ -716,12 +716,12 @@ module KubernetesHelpers
"traffic" => [{ "latestRevision" => true, "percent" => 100, "revisionName" => "#{name}-92tsj" }], "traffic" => [{ "latestRevision" => true, "percent" => 100, "revisionName" => "#{name}-92tsj" }],
"url" => "http://#{name}.#{namespace}.#{domain}" }, "url" => "http://#{name}.#{namespace}.#{domain}" },
"environment_scope" => environment, "environment_scope" => environment,
"cluster_id" => 5, "cluster_id" => cluster_id,
"podcount" => 0 } "podcount" => 0 }
end end
# noinspection RubyStringKeysInHashInspection # noinspection RubyStringKeysInHashInspection
def knative_05_service(name: 'kubetest', namespace: 'default', domain: 'example.com', description: 'a knative service', environment: 'production') def knative_05_service(name: 'kubetest', namespace: 'default', domain: 'example.com', description: 'a knative service', environment: 'production', cluster_id: 8)
{ "apiVersion" => "serving.knative.dev/v1alpha1", { "apiVersion" => "serving.knative.dev/v1alpha1",
"kind" => "Service", "kind" => "Service",
"metadata" => "metadata" =>
...@@ -771,7 +771,7 @@ module KubernetesHelpers ...@@ -771,7 +771,7 @@ module KubernetesHelpers
"observedGeneration" => 1, "observedGeneration" => 1,
"traffic" => [{ "percent" => 100, "revisionName" => "#{name}-58qgr" }] }, "traffic" => [{ "percent" => 100, "revisionName" => "#{name}-58qgr" }] },
"environment_scope" => environment, "environment_scope" => environment,
"cluster_id" => 8, "cluster_id" => cluster_id,
"podcount" => 0 } "podcount" => 0 }
end end
......
# frozen_string_literal: true
RSpec.shared_examples 'a pages cronjob scheduling jobs with context' do |scheduled_worker_class|
let(:worker) { described_class.new }
it 'does not cause extra queries for multiple domains' do
control = ActiveRecord::QueryRecorder.new { worker.perform }
extra_domain
expect { worker.perform }.not_to exceed_query_limit(control)
end
it 'schedules the renewal with a context' do
extra_domain
worker.perform
expect(scheduled_worker_class.jobs.last).to include("meta.project" => extra_domain.project.full_path)
end
end
...@@ -12,7 +12,7 @@ describe PagesDomainSslRenewalCronWorker do ...@@ -12,7 +12,7 @@ describe PagesDomainSslRenewalCronWorker do
end end
describe '#perform' do describe '#perform' do
let(:project) { create :project } let_it_be(:project) { create :project }
let!(:domain) { create(:pages_domain, project: project, auto_ssl_enabled: false) } let!(:domain) { create(:pages_domain, project: project, auto_ssl_enabled: false) }
let!(:domain_with_enabled_auto_ssl) { create(:pages_domain, project: project, auto_ssl_enabled: true) } let!(:domain_with_enabled_auto_ssl) { create(:pages_domain, project: project, auto_ssl_enabled: true) }
let!(:domain_with_obtained_letsencrypt) do let!(:domain_with_obtained_letsencrypt) do
...@@ -35,12 +35,16 @@ describe PagesDomainSslRenewalCronWorker do ...@@ -35,12 +35,16 @@ describe PagesDomainSslRenewalCronWorker do
[domain, [domain,
domain_with_obtained_letsencrypt].each do |domain| domain_with_obtained_letsencrypt].each do |domain|
expect(PagesDomainVerificationWorker).not_to receive(:perform_async).with(domain.id) expect(PagesDomainSslRenewalWorker).not_to receive(:perform_async).with(domain.id)
end end
worker.perform worker.perform
end end
it_behaves_like 'a pages cronjob scheduling jobs with context', PagesDomainSslRenewalWorker do
let(:extra_domain) { create(:pages_domain, :with_project, auto_ssl_enabled: true) }
end
shared_examples 'does nothing' do shared_examples 'does nothing' do
it 'does nothing' do it 'does nothing' do
expect(PagesDomainSslRenewalWorker).not_to receive(:perform_async) expect(PagesDomainSslRenewalWorker).not_to receive(:perform_async)
......
...@@ -5,9 +5,9 @@ require 'spec_helper' ...@@ -5,9 +5,9 @@ require 'spec_helper'
describe PagesDomainVerificationCronWorker do describe PagesDomainVerificationCronWorker do
subject(:worker) { described_class.new } subject(:worker) { described_class.new }
describe '#perform' do describe '#perform', :sidekiq do
let!(:verified) { create(:pages_domain) } let!(:verified) { create(:pages_domain) }
let!(:reverify) { create(:pages_domain, :reverify) } let!(:reverify) { create(:pages_domain, :reverify, :with_project) }
let!(:disabled) { create(:pages_domain, :disabled) } let!(:disabled) { create(:pages_domain, :disabled) }
it 'does nothing if the database is read-only' do it 'does nothing if the database is read-only' do
...@@ -26,5 +26,9 @@ describe PagesDomainVerificationCronWorker do ...@@ -26,5 +26,9 @@ describe PagesDomainVerificationCronWorker do
worker.perform worker.perform
end end
it_behaves_like 'a pages cronjob scheduling jobs with context', PagesDomainVerificationWorker do
let(:extra_domain) { create(:pages_domain, :reverify, :with_project) }
end
end end
end end
...@@ -2,13 +2,13 @@ ...@@ -2,13 +2,13 @@
require 'spec_helper' require 'spec_helper'
describe PropagateInstanceLevelServiceWorker do describe PropagateServiceTemplateWorker do
include ExclusiveLeaseHelpers include ExclusiveLeaseHelpers
describe '#perform' do describe '#perform' do
it 'calls the propagate service with the instance level service' do it 'calls the propagate service with the template' do
instance_level_service = PushoverService.create( template = PushoverService.create(
instance: true, template: true,
active: true, active: true,
properties: { properties: {
device: 'MyDevice', device: 'MyDevice',
...@@ -18,14 +18,14 @@ describe PropagateInstanceLevelServiceWorker do ...@@ -18,14 +18,14 @@ describe PropagateInstanceLevelServiceWorker do
api_key: '123456789' api_key: '123456789'
}) })
stub_exclusive_lease("propagate_instance_level_service_worker:#{instance_level_service.id}", stub_exclusive_lease("propagate_service_template_worker:#{template.id}",
timeout: PropagateInstanceLevelServiceWorker::LEASE_TIMEOUT) timeout: PropagateServiceTemplateWorker::LEASE_TIMEOUT)
expect(Projects::PropagateInstanceLevelService) expect(Projects::PropagateServiceTemplate)
.to receive(:propagate) .to receive(:propagate)
.with(instance_level_service) .with(template)
subject.perform(instance_level_service.id) subject.perform(template.id)
end end
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment