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
before_action :service, only: [:edit, :update]
def index
@services = instance_level_services
@services = services_templates
end
def edit
......@@ -19,7 +19,7 @@ class Admin::ServicesController < Admin::ApplicationController
def update
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,
notice: 'Application settings saved successfully'
......@@ -31,17 +31,17 @@ class Admin::ServicesController < Admin::ApplicationController
private
# rubocop: disable CodeReuse/ActiveRecord
def instance_level_services
def services_templates
Service.available_services_names.map do |service_name|
service = "#{service_name}_service".camelize.constantize
service.where(instance: true).first_or_create
service_template = "#{service_name}_service".camelize.constantize
service_template.where(template: true).first_or_create
end
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def service
@service ||= Service.where(id: params[:id], instance: true).first
@service ||= Service.where(id: params[:id], template: true).first
end
# rubocop: enable CodeReuse/ActiveRecord
......
......@@ -8,11 +8,15 @@ module Projects
def index
respond_to do |format|
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: {
knative_installed: finder.knative_installed,
functions: serialize_function(functions)
functions: serialized_functions
}.to_json
end
......@@ -23,11 +27,14 @@ module Projects
end
def show
@service = serialize_function(finder.service(params[:environment_id], params[:id]))
@prometheus = finder.has_prometheus?(params[:environment_id])
function = finder.service(params[:environment_id], params[:id])
return not_found unless function && can?(@current_user, :read_cluster, function.cluster)
@service = serialize_function(function)
return not_found if @service.nil?
@prometheus = finder.has_prometheus?(params[:environment_id])
respond_to do |format|
format.json do
render json: @service
......
......@@ -93,24 +93,32 @@ module Projects
.services
.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
def knative_services
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
def add_metadata(finder, services)
return if services.nil?
add_pod_count = services.one?
services.each do |s|
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
s["podcount"] = finder
......
......@@ -12,6 +12,10 @@ module Types
description: 'Blob highlighted data',
null: true
field :plain_highlighted_data, GraphQL::STRING_TYPE,
description: 'Blob plain highlighted data',
null: true
field :raw_path, GraphQL::STRING_TYPE,
description: 'Blob raw content endpoint path',
null: false
......
......@@ -290,6 +290,12 @@ module Clusters
end
end
def serverless_domain
strong_memoize(:serverless_domain) do
self.application_knative&.serverless_domain_cluster
end
end
private
def unique_management_project_environment_scope
......
......@@ -62,6 +62,8 @@ class PagesDomain < ApplicationRecord
scope :for_removal, -> { where("remove_at < ?", Time.now) }
scope :with_logging_info, -> { includes(project: [:namespace, :route]) }
def verified?
!!verified_at
end
......@@ -285,3 +287,5 @@ class PagesDomain < ApplicationRecord
!auto_ssl_enabled? && project&.pages_https_only?
end
end
PagesDomain.prepend_if_ee('::EE::PagesDomain')
......@@ -1224,13 +1224,13 @@ class Project < ApplicationRecord
service = find_service(services, name)
return service if service
# We should check if an instance-level service exists
instance_level_service = find_service(instance_level_services, name)
# We should check if template for the service exists
template = find_service(services_templates, name)
if instance_level_service
Service.build_from_instance(id, instance_level_service)
if template
Service.build_from_template(id, template)
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
end
end
......@@ -2460,8 +2460,8 @@ class Project < ApplicationRecord
end
end
def instance_level_services
@instance_level_services ||= Service.where(instance: true)
def services_templates
@services_templates ||= Service.where(template: true)
end
def ensure_pages_metadatum
......
......@@ -164,7 +164,7 @@ class IssueTrackerService < Service
end
def one_issue_tracker
return if instance?
return if template?
return if project.blank?
if project.services.external_issue_trackers.where.not(id: id).any?
......
......@@ -85,7 +85,7 @@ class PrometheusService < MonitoringService
end
def prometheus_available?
return false if instance?
return false if template?
return false unless project
project.all_clusters.enabled.any? { |cluster| cluster.application_prometheus_available? }
......
......@@ -32,7 +32,7 @@ class Service < ApplicationRecord
belongs_to :project, inverse_of: :services
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
scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') }
......@@ -70,8 +70,8 @@ class Service < ApplicationRecord
true
end
def instance?
instance
def template?
template
end
def category
......@@ -299,15 +299,15 @@ class Service < ApplicationRecord
service_names.sort_by(&:downcase)
end
def self.build_from_instance(project_id, instance_level_service)
service = instance_level_service.dup
def self.build_from_template(project_id, template)
service = template.dup
if instance_level_service.supports_data_fields?
data_fields = instance_level_service.data_fields.dup
if template.supports_data_fields?
data_fields = template.data_fields.dup
data_fields.service = service
end
service.instance = false
service.template = false
service.project_id = project_id
service.active = false if service.active? && !service.valid?
service
......@@ -321,6 +321,10 @@ class Service < ApplicationRecord
nil
end
def self.find_by_template
find_by(template: true)
end
# override if needed
def supports_data_fields?
false
......
......@@ -4,11 +4,13 @@ class SnippetBlobPresenter < BlobPresenter
def highlighted_data
return if blob.binary?
if blob.rich_viewer&.partial_name == 'markup'
blob.rendered_markup
else
highlight
end
highlight(plain: false)
end
def plain_highlighted_data
return if blob.binary?
highlight(plain: true)
end
def raw_path
......
......@@ -5,91 +5,31 @@ module Projects
class ServiceEntity < Grape::Entity
include RequestAwareEntity
expose :name do |service|
service.dig('metadata', 'name')
end
expose :namespace do |service|
service.dig('metadata', 'namespace')
end
expose :environment_scope do |service|
service.dig('environment_scope')
end
expose :cluster_id do |service|
service.dig('cluster_id')
end
expose :name
expose :namespace
expose :environment_scope
expose :podcount
expose :created_at
expose :image
expose :description
expose :url
expose :detail_url do |service|
project_serverless_path(
request.project,
service.dig('environment_scope'),
service.dig('metadata', 'name'))
end
expose :podcount do |service|
service.dig('podcount')
service.environment_scope,
service.name)
end
expose :metrics_url do |service|
project_serverless_metrics_path(
request.project,
service.dig('environment_scope'),
service.dig('metadata', 'name')) + ".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)
service.environment_scope,
service.name, format: :json)
end
expose :image do |service|
service.dig(
'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')
expose :cluster_id do |service|
service.cluster&.id
end
end
end
......
......@@ -135,7 +135,7 @@ module Projects
if @project.save
unless @project.gitlab_project_import?
create_services_from_active_instance_level_services(@project)
create_services_from_active_templates(@project)
@project.create_labels
end
......@@ -161,9 +161,9 @@ module Projects
end
# rubocop: disable CodeReuse/ActiveRecord
def create_services_from_active_instance_level_services(project)
Service.where(instance: true, active: true).each do |template|
service = Service.build_from_instance(project.id, template)
def create_services_from_active_templates(project)
Service.where(template: true, active: true).each do |template|
service = Service.build_from_template(project.id, template)
service.save!
end
end
......
# frozen_string_literal: true
module Projects
class PropagateInstanceLevelService
class PropagateServiceTemplate
BATCH_SIZE = 100
def self.propagate(*args)
new(*args).propagate
end
def initialize(instance_level_service)
@instance_level_service = instance_level_service
def initialize(template)
@template = template
end
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
private
def propagate_projects_with_instance_level_service
def propagate_projects_with_template
loop do
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
end
end
def bulk_create_from_instance_level_service(batch)
def bulk_create_from_template(batch)
service_list = batch.map do |project_id|
service_hash.values << project_id
end
......@@ -52,7 +52,7 @@ module Projects
SELECT true
FROM services
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.archived = false
......@@ -73,9 +73,9 @@ module Projects
def service_hash
@service_hash ||=
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
service_hash[ActiveRecord::Base.connection.quote_column_name(key)] =
......@@ -97,11 +97,11 @@ module Projects
# rubocop: enable CodeReuse/ActiveRecord
def active_external_issue_tracker?
@instance_level_service.issue_tracker? && !@instance_level_service.default
@template.issue_tracker? && !@template.default
end
def active_external_wiki?
@instance_level_service.type == 'ExternalWikiService'
@template.type == 'ExternalWikiService'
end
end
end
......@@ -10,8 +10,8 @@
%p.inline
= s_("MattermostService|See list of available commands in Mattermost after setting up this service, by entering")
%kbd.inline /&lt;trigger&gt; help
- unless enabled || @service.instance?
- unless enabled || @service.template?
= 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
......@@ -11,7 +11,7 @@
%p.inline
= s_("SlackService|See list of available commands in Slack after setting up this service, by entering")
%kbd.inline /&lt;command&gt; help
- unless @service.instance?
- unless @service.template?
%p= _("To set up this service:")
%ul.list-unstyled.indent-list
%li
......
......@@ -969,7 +969,7 @@
:latency_sensitive:
:resource_boundary: :unknown
:weight: 1
- :name: propagate_instance_level_service
- :name: propagate_service_template
:feature_category: :source_code_management
:has_external_dependencies:
:latency_sensitive:
......
......@@ -2,14 +2,14 @@
class PagesDomainRemovalCronWorker
include ApplicationWorker
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
include CronjobQueue
feature_category :pages
worker_resource_boundary :cpu
def perform
PagesDomain.for_removal.find_each do |domain|
domain.destroy!
PagesDomain.for_removal.with_logging_info.find_each do |domain|
with_context(project: domain.project) { domain.destroy! }
rescue => e
Gitlab::ErrorTracking.track_exception(e)
end
......
......@@ -2,15 +2,17 @@
class PagesDomainSslRenewalCronWorker
include ApplicationWorker
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
include CronjobQueue
feature_category :pages
def perform
return unless ::Gitlab::LetsEncrypt.enabled?
PagesDomain.need_auto_ssl_renewal.find_each do |domain|
PagesDomainSslRenewalWorker.perform_async(domain.id)
PagesDomain.need_auto_ssl_renewal.with_logging_info.find_each do |domain|
with_context(project: domain.project) do
PagesDomainSslRenewalWorker.perform_async(domain.id)
end
end
end
end
......@@ -2,15 +2,17 @@
class PagesDomainVerificationCronWorker
include ApplicationWorker
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
include CronjobQueue
feature_category :pages
def perform
return if Gitlab::Database.read_only?
PagesDomain.needs_verification.find_each do |domain|
PagesDomainVerificationWorker.perform_async(domain.id)
PagesDomain.needs_verification.with_logging_info.find_each do |domain|
with_context(project: domain.project) do
PagesDomainVerificationWorker.perform_async(domain.id)
end
end
end
end
# frozen_string_literal: true
# Worker for updating any project specific caches.
class PropagateInstanceLevelServiceWorker
class PropagateServiceTemplateWorker
include ApplicationWorker
feature_category :source_code_management
......@@ -9,18 +9,18 @@ class PropagateInstanceLevelServiceWorker
LEASE_TIMEOUT = 4.hours.to_i
# rubocop: disable CodeReuse/ActiveRecord
def perform(instance_level_service_id)
return unless try_obtain_lease_for(instance_level_service_id)
def perform(template_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
# rubocop: enable CodeReuse/ActiveRecord
private
def try_obtain_lease_for(instance_level_service_id)
def try_obtain_lease_for(template_id)
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
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 @@
- 1
- - project_update_repository_storage
- 1
- - propagate_instance_level_service
- - propagate_service_template
- 1
- - reactive_caching
- 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]
private
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')
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
t.boolean "deployment_events", default: false, null: false
t.string "description", limit: 500
t.boolean "comment_on_event_enabled", default: true, null: false
t.boolean "instance", default: false
t.index ["instance"], name: "index_services_on_instance"
t.boolean "template", default: false
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"
end
......
......@@ -6777,6 +6777,11 @@ type SnippetBlob {
"""
path: String
"""
Blob plain highlighted data
"""
plainHighlightedData: String
"""
Blob raw content endpoint path
"""
......
......@@ -7612,6 +7612,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "plainHighlightedData",
"description": "Blob plain highlighted data",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "rawPath",
"description": "Blob raw content endpoint path",
......
......@@ -1071,6 +1071,7 @@ Represents the snippet blob
| `mode` | String | Blob mode |
| `name` | String | Blob name |
| `path` | String | Blob path |
| `plainHighlightedData` | String | Blob plain highlighted data |
| `rawPath` | String! | Blob raw content endpoint path |
| `richViewer` | SnippetBlobViewer | Blob content rich viewer |
| `simpleViewer` | SnippetBlobViewer! | Blob content simple viewer |
......
......@@ -132,7 +132,7 @@ module API
helpers do
# rubocop: disable CodeReuse/ActiveRecord
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
end
end
......
......@@ -257,7 +257,7 @@ excluded_attributes:
- :token
- :token_encrypted
services:
- :instance
- :template
error_tracking_setting:
- :encrypted_token
- :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
# rubocop: disable CodeReuse/ActiveRecord
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|
response["projects_#{service_name}_active".to_sym] = service_counts["#{service_name}_service".camelize] || 0
......
......@@ -469,6 +469,7 @@ module QA
autoload :Configure, 'qa/vendor/jenkins/page/configure'
autoload :NewCredentials, 'qa/vendor/jenkins/page/new_credentials'
autoload :NewJob, 'qa/vendor/jenkins/page/new_job'
autoload :Job, 'qa/vendor/jenkins/page/job'
autoload :ConfigureJob, 'qa/vendor/jenkins/page/configure_job'
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
Service.available_services_names.each do |service_name|
context "#{service_name}" do
let!(:service) do
service_instance = "#{service_name}_service".camelize.constantize
service_instance.where(instance: true).first_or_create
service_template = "#{service_name}_service".camelize.constantize
service_template.where(template: true).first_or_create
end
it 'successfully displays the service' do
it 'successfully displays the template' do
get :edit, params: { id: service.id }
expect(response).to have_gitlab_http_status(:ok)
......@@ -34,7 +34,7 @@ describe Admin::ServicesController do
RedmineService.create(
project: project,
active: false,
instance: true,
template: true,
properties: {
project_url: 'http://abc',
issues_url: 'http://abc',
......@@ -44,7 +44,7 @@ describe Admin::ServicesController do
end
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 } }
......@@ -52,7 +52,7 @@ describe Admin::ServicesController do
end
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: {} } }
......
......@@ -14,9 +14,11 @@ describe Projects::Serverless::FunctionsController do
let!(:deployment) { create(:deployment, :success, environment: environment, cluster: cluster) }
let(:knative_services_finder) { environment.knative_services_finder }
let(:function_description) { 'A serverless function' }
let(:function_name) { 'some-function-name' }
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
let(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
let(:namespace) do
create(:cluster_kubernetes_namespace,
......@@ -87,25 +89,65 @@ describe Projects::Serverless::FunctionsController do
end
context 'when functions were found' do
let(:functions) { ["asdf"] }
let(:functions) { [{}, {}] }
before do
stub_kubeclient_knative_services(namespace: namespace.namespace)
get :index, params: params({ format: :json })
stub_kubeclient_knative_services(namespace: namespace.namespace, cluster_id: cluster.id, name: function_name)
end
it 'returns functions' do
get :index, params: params({ format: :json })
expect(json_response["functions"]).not_to be_empty
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
describe 'GET #show' do
context 'invalid data' do
it 'has a bad function name' do
context 'with function that does not exist' do
it 'returns 404' do
get :show, params: params({ format: :json, environment_id: "*", id: "foo" })
expect(response).to have_gitlab_http_status(:not_found)
end
......@@ -113,15 +155,50 @@ describe Projects::Serverless::FunctionsController do
context 'with valid data', :use_clean_rails_memory_store_caching do
shared_examples 'GET #show with valid data' do
it 'has a valid function name' do
get :show, params: params({ format: :json, environment_id: "*", id: cluster.project.name })
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 :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(json_response).to include(
'name' => project.name,
'url' => "http://#{project.name}.#{namespace.namespace}.example.com",
'name' => function_name,
'url' => "http://#{function_name}.#{namespace.namespace}.example.com",
'description' => function_description,
'podcount' => 1
'podcount' => 0
)
end
end
......@@ -180,8 +257,8 @@ describe Projects::Serverless::FunctionsController do
'knative_installed' => 'checking',
'functions' => [
a_hash_including(
'name' => project.name,
'url' => "http://#{project.name}.#{namespace.namespace}.example.com",
'name' => function_name,
'url' => "http://#{function_name}.#{namespace.namespace}.example.com",
'description' => function_description
)
]
......
......@@ -154,12 +154,12 @@ describe Projects::ServicesController do
end
end
context 'when activating Jira service from instance level service' do
context 'when activating Jira service from a template' do
let(:service) do
create(:jira_service, project: project, instance: true)
create(:jira_service, project: project, template: true)
end
it 'activate Jira service from instance level service' do
it 'activate Jira service from template' do
expect(flash[:notice]).to eq 'Jira activated.'
end
end
......
......@@ -380,5 +380,9 @@ x6zG6WoibsbsJMj70nwseUnPTBQNDP+j61RJjC/r
scope { :instance }
usage { :serverless }
end
trait :with_project do
association :project
end
end
end
......@@ -153,8 +153,8 @@ describe Projects::Serverless::FunctionsFinder do
*knative_services_finder.cache_args)
result = finder.service(cluster.environment_scope, cluster.project.name)
expect(result).not_to be_empty
expect(result["metadata"]["name"]).to be_eql(cluster.project.name)
expect(result).to be_present
expect(result.name).to be_eql(cluster.project.name)
end
it 'has metrics', :use_clean_rails_memory_store_caching do
......
......@@ -2736,7 +2736,7 @@ Service
when repository is empty
test runs execute
Template
.build_from_instance
.build_from_template
when template is invalid
sets service template to inactive when template is invalid
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', () => {
expect(findModal().contains(DuplicateDashboardForm)).toBe(true);
});
it('saves a new dashboard', done => {
it('saves a new dashboard', () => {
findModal().vm.$emit('ok', okEvent);
waitForPromises()
.then(() => {
expect(okEvent.preventDefault).toHaveBeenCalled();
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.vm.$refs.duplicateDashboardModal.hide).toHaveBeenCalled();
expect(wrapper.emitted().selectDashboard).toBeTruthy();
expect(findAlert().exists()).toBe(false);
done();
})
.catch(done.fail);
return waitForPromises().then(() => {
expect(okEvent.preventDefault).toHaveBeenCalled();
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.vm.$refs.duplicateDashboardModal.hide).toHaveBeenCalled();
expect(wrapper.emitted().selectDashboard).toBeTruthy();
expect(findAlert().exists()).toBe(false);
});
});
describe('when a new dashboard is saved succesfully', () => {
......@@ -167,52 +164,42 @@ describe('DashboardsDropdown', () => {
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({
branch: defaultBranch,
});
waitForPromises()
.then(() => {
expect(wrapper.emitted().selectDashboard[0][0]).toEqual(newDashboard);
done();
})
.catch(done.fail);
return waitForPromises().then(() => {
expect(wrapper.emitted().selectDashboard[0][0]).toEqual(newDashboard);
});
});
it('to a new branch refreshes in the current dashboard', done => {
it('to a new branch refreshes in the current dashboard', () => {
submitForm({
branch: 'another-branch',
});
waitForPromises()
.then(() => {
expect(wrapper.emitted().selectDashboard[0][0]).toEqual(dashboardGitResponse[0]);
done();
})
.catch(done.fail);
return waitForPromises().then(() => {
expect(wrapper.emitted().selectDashboard[0][0]).toEqual(dashboardGitResponse[0]);
});
});
});
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';
duplicateDashboardAction.mockRejectedValueOnce(errMsg);
findModal().vm.$emit('ok', okEvent);
waitForPromises()
.then(() => {
expect(okEvent.preventDefault).toHaveBeenCalled();
expect(findAlert().exists()).toBe(true);
expect(findAlert().text()).toBe(errMsg);
return waitForPromises().then(() => {
expect(okEvent.preventDefault).toHaveBeenCalled();
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.vm.$refs.duplicateDashboardModal.hide).not.toHaveBeenCalled();
expect(findAlert().exists()).toBe(true);
expect(findAlert().text()).toBe(errMsg);
done();
})
.catch(done.fail);
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.vm.$refs.duplicateDashboardModal.hide).not.toHaveBeenCalled();
});
});
it('id is correct, as the value of modal directive binding matches modal id', () => {
......
......@@ -44,30 +44,27 @@ describe('DuplicateDashboardForm', () => {
describe('validates the file name', () => {
const findInvalidFeedback = () => findByRef('fileNameFormGroup').find('.invalid-feedback');
it('when is empty', done => {
it('when is empty', () => {
setValue('fileName', '');
wrapper.vm.$nextTick(() => {
return wrapper.vm.$nextTick(() => {
expect(findByRef('fileNameFormGroup').is('.is-valid')).toBe(true);
expect(findInvalidFeedback().exists()).toBe(false);
done();
});
});
it('when is valid', done => {
it('when is valid', () => {
setValue('fileName', 'my_dashboard.yml');
wrapper.vm.$nextTick(() => {
return wrapper.vm.$nextTick(() => {
expect(findByRef('fileNameFormGroup').is('.is-valid')).toBe(true);
expect(findInvalidFeedback().exists()).toBe(false);
done();
});
});
it('when is not valid', done => {
it('when is not valid', () => {
setValue('fileName', 'my_dashboard.exe');
wrapper.vm.$nextTick(() => {
return wrapper.vm.$nextTick(() => {
expect(findByRef('fileNameFormGroup').is('.is-invalid')).toBe(true);
expect(findInvalidFeedback().text()).toBeTruthy();
done();
});
});
});
......@@ -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);
setValue('branchName', 'a-new-branch');
expect(lastChange()).resolves.toMatchObject({
branch: defaultBranch,
});
wrapper.vm.$nextTick(() => {
return wrapper.vm.$nextTick(() => {
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);
wrapper.vm
.$nextTick()
.then(() => {
wrapper.find('form').trigger('change');
expect(findByRef('branchName').is(':focus')).toBe(true);
})
.then(done)
.catch(done.fail);
return wrapper.vm.$nextTick().then(() => {
wrapper.find('form').trigger('change');
expect(findByRef('branchName').is(':focus')).toBe(true);
});
});
});
});
......@@ -32,25 +32,23 @@ describe('Graph group component', () => {
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.$nextTick(() => {
return wrapper.vm.$nextTick(() => {
expect(findContent().isVisible()).toBe(false);
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({
collapseGroup: true,
});
wrapper.vm.$nextTick(() => {
return wrapper.vm.$nextTick(() => {
expect(findContent().isVisible()).toBe(true);
expect(findCaretIcon().props('name')).toBe('angle-down');
done();
});
});
......@@ -102,13 +100,12 @@ describe('Graph group component', () => {
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.$nextTick(() => {
return wrapper.vm.$nextTick(() => {
expect(findContent().isVisible()).toBe(true);
expect(findCaretIcon().exists()).toBe(false);
done();
});
});
});
......
......@@ -28,6 +28,8 @@ describe('Panel Type component', () => {
const exampleText = 'example_text';
const findCopyLink = () => wrapper.find({ ref: 'copyChartLink' });
const createWrapper = props => {
wrapper = shallowMount(PanelType, {
propsData: {
......@@ -96,8 +98,7 @@ describe('Panel Type component', () => {
});
it('sets no clipboard copy link on dropdown by default', () => {
const link = () => wrapper.find({ ref: 'copyChartLink' });
expect(link().exists()).toBe(false);
expect(findCopyLink().exists()).toBe(false);
});
describe('Time Series Chart panel type', () => {
......@@ -204,7 +205,6 @@ describe('Panel Type component', () => {
});
describe('when cliboard data is available', () => {
const link = () => wrapper.find({ ref: 'copyChartLink' });
const clipboardText = 'A value to copy.';
beforeEach(() => {
......@@ -219,16 +219,16 @@ describe('Panel Type component', () => {
});
it('sets clipboard text on the dropdown', () => {
expect(link().exists()).toBe(true);
expect(link().element.dataset.clipboardText).toBe(clipboardText);
expect(findCopyLink().exists()).toBe(true);
expect(findCopyLink().element.dataset.clipboardText).toBe(clipboardText);
});
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', () => {
link().vm.$emit('click');
findCopyLink().vm.$emit('click');
expect(wrapper.vm.$toast.show).toHaveBeenCalled();
});
......
......@@ -4,10 +4,9 @@ require 'spec_helper'
describe GitlabSchema.types['SnippetBlob'] do
it 'has the correct fields' do
expected_fields = [:highlighted_data, :raw_path,
:size, :binary, :name, :path,
:simple_viewer, :rich_viewer,
:mode]
expected_fields = [:highlighted_data, :plain_highlighted_data,
:raw_path, :size, :binary, :name, :path,
:simple_viewer, :rich_viewer, :mode]
is_expected.to have_graphql_fields(*expected_fields)
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
setup_import_export_config('light')
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(project.services.where(instance: true).count).to eq(0)
expect(project.services.where(template: true).count).to eq(0)
end
it 'imports labels' do
......
......@@ -453,7 +453,7 @@ Service:
- updated_at
- active
- properties
- instance
- template
- push_events
- issues_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
create(:service, project: projects[1], 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: true, instance: true)
create(:service, project: projects[2], type: 'MattermostService', active: true, template: 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[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
end
end
describe "Instance" do
describe "Template" do
let(:project) { create(:project) }
describe '.build_from_instance' do
context 'when instance level integration is invalid' do
it 'sets instance level integration to inactive when instance is invalid' do
instance = build(:prometheus_service, instance: true, active: true, properties: {})
instance.save(validate: false)
describe '.build_from_template' do
context 'when template is invalid' do
it 'sets service template to inactive when template is invalid' do
template = build(:prometheus_service, template: true, active: true, properties: {})
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.active).to be false
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(:description) { 'custom description' }
let(:url) { 'http://jira.example.com' }
......@@ -127,9 +127,9 @@ describe Service do
}
end
shared_examples 'integration creation from instance level' do
shared_examples 'service creation from a template' 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.title).to eq(title)
......@@ -144,38 +144,38 @@ describe Service do
# this will be removed as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
context 'when data are stored in properties' do
let(:properties) { data_params.merge(title: title, description: description) }
let!(:instance_level_integration) do
create(:jira_service, :without_properties_callback, instance: true, properties: properties.merge(additional: 'something'))
let!(:template) do
create(:jira_service, :without_properties_callback, template: true, properties: properties.merge(additional: 'something'))
end
it_behaves_like 'integration creation from instance level'
it_behaves_like 'service creation from a template'
end
context 'when data are stored in separated fields' do
let(:instance_level_integration) do
create(:jira_service, data_params.merge(properties: {}, title: title, description: description, instance: true))
let(:template) do
create(:jira_service, data_params.merge(properties: {}, title: title, description: description, template: true))
end
it_behaves_like 'integration creation from instance level'
it_behaves_like 'service creation from a template'
end
context 'when data are stored in both properties and separated fields' do
let(:properties) { data_params.merge(title: title, description: description) }
let(:instance_level_integration) do
create(:jira_service, :without_properties_callback, active: true, instance: true, properties: properties).tap do |service|
let(:template) do
create(:jira_service, :without_properties_callback, active: true, template: true, properties: properties).tap do |service|
create(:jira_tracker_data, data_params.merge(service: service))
end
end
it_behaves_like 'integration creation from instance level'
it_behaves_like 'service creation from a template'
end
end
end
describe "for pushover service" do
let!(:instance_level_integration) do
let!(:service_template) do
PushoverService.create(
instance: true,
template: true,
properties: {
device: 'MyDevice',
sound: 'mic',
......@@ -188,7 +188,7 @@ describe Service do
it "has all fields prefilled" do
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.sound).to eq('mic')
expect(service.priority).to eq(4)
......@@ -391,6 +391,14 @@ describe Service do
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
let(:fake_service) do
Class.new(Service) do
......
......@@ -18,7 +18,7 @@ describe SnippetBlobPresenter do
snippet.file_name = 'test.md'
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
it 'returns syntax highlighted content' do
......@@ -33,7 +33,41 @@ describe SnippetBlobPresenter do
snippet.file_name = 'test'
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
......
......@@ -15,7 +15,7 @@ describe Projects::CreateService, '#execute' do
}
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)
project = create_project(user, opts)
......@@ -96,7 +96,7 @@ describe Projects::CreateService, '#execute' do
end
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)
service = project.services.first
......@@ -342,22 +342,22 @@ describe Projects::CreateService, '#execute' do
end
end
context 'when there is an active instance level service' do
context 'when there is an active service template' do
before do
create(:service, project: nil, instance: true, active: true)
create(:service, project: nil, template: true, active: true)
end
it 'creates a service from instance level service' do
it 'creates a service from this template' do
project = create_project(user, opts)
expect(project.services.count).to eq 1
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
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)
service = project.services.first
......
......@@ -2,11 +2,11 @@
require 'spec_helper'
describe Projects::PropagateInstanceLevelService do
describe Projects::PropagateServiceTemplate do
describe '.propagate' do
let!(:instance_level_integration) do
let!(:service_template) do
PushoverService.create(
instance: true,
template: true,
active: true,
properties: {
device: 'MyDevice',
......@@ -22,14 +22,14 @@ describe Projects::PropagateInstanceLevelService do
it 'creates services for projects' do
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
end
it 'creates services for a project that has another service' do
BambooService.create(
instance: true,
template: true,
active: true,
project: project,
properties: {
......@@ -42,14 +42,14 @@ describe Projects::PropagateInstanceLevelService do
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
end
it 'does not create the service if it exists already' do
other_service = BambooService.create(
instance: true,
template: true,
active: true,
properties: {
bamboo_url: 'http://gitlab.com',
......@@ -59,17 +59,17 @@ describe Projects::PropagateInstanceLevelService do
}
)
Service.build_from_instance(project.id, instance_level_integration).save!
Service.build_from_instance(project.id, other_service).save!
Service.build_from_template(project.id, service_template).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 }
end
it 'creates the service containing the instance attributes' do
described_class.propagate(instance_level_integration)
it 'creates the service containing the template attributes' do
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
describe 'bulk update', :use_sql_query_cache do
......@@ -80,7 +80,7 @@ describe Projects::PropagateInstanceLevelService do
project_total.times { create(:project) }
described_class.propagate(instance_level_integration)
described_class.propagate(service_template)
end
it 'creates services for all projects' do
......@@ -90,18 +90,18 @@ describe Projects::PropagateInstanceLevelService do
describe '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)
end
end
describe 'external wiki' 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)
end
end
......
......@@ -557,7 +557,7 @@ module KubernetesHelpers
end
# 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",
"kind" => "Service",
"metadata" =>
......@@ -612,12 +612,12 @@ module KubernetesHelpers
"url" => "http://#{name}.#{namespace}.#{domain}"
},
"environment_scope" => environment,
"cluster_id" => 9,
"cluster_id" => cluster_id,
"podcount" => 0 }
end
# 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",
"kind" => "Service",
"metadata" =>
......@@ -664,12 +664,12 @@ module KubernetesHelpers
"traffic" => [{ "latestRevision" => true, "percent" => 100, "revisionName" => "#{name}-92tsj" }],
"url" => "http://#{name}.#{namespace}.#{domain}" },
"environment_scope" => environment,
"cluster_id" => 5,
"cluster_id" => cluster_id,
"podcount" => 0 }
end
# 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",
"kind" => "Service",
"metadata" =>
......@@ -716,12 +716,12 @@ module KubernetesHelpers
"traffic" => [{ "latestRevision" => true, "percent" => 100, "revisionName" => "#{name}-92tsj" }],
"url" => "http://#{name}.#{namespace}.#{domain}" },
"environment_scope" => environment,
"cluster_id" => 5,
"cluster_id" => cluster_id,
"podcount" => 0 }
end
# 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",
"kind" => "Service",
"metadata" =>
......@@ -771,7 +771,7 @@ module KubernetesHelpers
"observedGeneration" => 1,
"traffic" => [{ "percent" => 100, "revisionName" => "#{name}-58qgr" }] },
"environment_scope" => environment,
"cluster_id" => 8,
"cluster_id" => cluster_id,
"podcount" => 0 }
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
end
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_with_enabled_auto_ssl) { create(:pages_domain, project: project, auto_ssl_enabled: true) }
let!(:domain_with_obtained_letsencrypt) do
......@@ -35,12 +35,16 @@ describe PagesDomainSslRenewalCronWorker 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
worker.perform
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
it 'does nothing' do
expect(PagesDomainSslRenewalWorker).not_to receive(:perform_async)
......
......@@ -5,9 +5,9 @@ require 'spec_helper'
describe PagesDomainVerificationCronWorker do
subject(:worker) { described_class.new }
describe '#perform' do
describe '#perform', :sidekiq do
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) }
it 'does nothing if the database is read-only' do
......@@ -26,5 +26,9 @@ describe PagesDomainVerificationCronWorker do
worker.perform
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
......@@ -2,13 +2,13 @@
require 'spec_helper'
describe PropagateInstanceLevelServiceWorker do
describe PropagateServiceTemplateWorker do
include ExclusiveLeaseHelpers
describe '#perform' do
it 'calls the propagate service with the instance level service' do
instance_level_service = PushoverService.create(
instance: true,
it 'calls the propagate service with the template' do
template = PushoverService.create(
template: true,
active: true,
properties: {
device: 'MyDevice',
......@@ -18,14 +18,14 @@ describe PropagateInstanceLevelServiceWorker do
api_key: '123456789'
})
stub_exclusive_lease("propagate_instance_level_service_worker:#{instance_level_service.id}",
timeout: PropagateInstanceLevelServiceWorker::LEASE_TIMEOUT)
stub_exclusive_lease("propagate_service_template_worker:#{template.id}",
timeout: PropagateServiceTemplateWorker::LEASE_TIMEOUT)
expect(Projects::PropagateInstanceLevelService)
expect(Projects::PropagateServiceTemplate)
.to receive(:propagate)
.with(instance_level_service)
.with(template)
subject.perform(instance_level_service.id)
subject.perform(template.id)
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