diff options
90 files changed, 1934 insertions, 864 deletions
diff --git a/.prettierignore b/.prettierignore index b674ccd50cf..dc9e572ab54 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,3 +3,7 @@ /public/ /vendor/ /tmp/ + +# ignore stylesheets for now as this clashes with our linter +*.css +*.scss diff --git a/app/assets/javascripts/behaviors/markdown/render_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_mermaid.js index 56f64f934a1..720f30e18e6 100644 --- a/app/assets/javascripts/behaviors/markdown/render_mermaid.js +++ b/app/assets/javascripts/behaviors/markdown/render_mermaid.js @@ -17,7 +17,7 @@ import flash from '~/flash'; export default function renderMermaid($els) { if (!$els.length) return; - import(/* webpackChunkName: 'mermaid' */ 'blackst0ne-mermaid') + import(/* webpackChunkName: 'mermaid' */ 'mermaid') .then(mermaid => { mermaid.initialize({ // mermaid core options diff --git a/app/assets/javascripts/jobs/components/job_container_item.vue b/app/assets/javascripts/jobs/components/job_container_item.vue new file mode 100644 index 00000000000..81cc0823792 --- /dev/null +++ b/app/assets/javascripts/jobs/components/job_container_item.vue @@ -0,0 +1,66 @@ +<script> +import _ from 'underscore'; +import CiIcon from '~/vue_shared/components/ci_icon.vue'; +import Icon from '~/vue_shared/components/icon.vue'; +import tooltip from '~/vue_shared/directives/tooltip'; + +export default { + components: { + CiIcon, + Icon, + }, + + directives: { + tooltip, + }, + + props: { + job: { + type: Object, + required: true, + }, + isActive: { + type: Boolean, + required: true, + }, + }, + + computed: { + tooltipText() { + return `${_.escape(this.job.name)} - ${this.job.status.tooltip}`; + }, + }, +}; +</script> + +<template> + <div + class="build-job" + :class="{ retried: job.retried, active: isActive }" + > + <a + v-tooltip + :href="job.status.details_path" + :title="tooltipText" + data-container="body" + data-boundary="viewport" + class="js-job-link" + > + <icon + v-if="isActive" + name="arrow-right" + class="js-arrow-right icon-arrow-right" + /> + + <ci-icon :status="job.status" /> + + <span>{{ job.name ? job.name : job.id }}</span> + + <icon + v-if="job.retried" + name="retry" + class="js-retry-icon" + /> + </a> + </div> +</template> diff --git a/app/assets/javascripts/jobs/components/jobs_container.vue b/app/assets/javascripts/jobs/components/jobs_container.vue index 03f36ec5c8b..951bcb36600 100644 --- a/app/assets/javascripts/jobs/components/jobs_container.vue +++ b/app/assets/javascripts/jobs/components/jobs_container.vue @@ -1,17 +1,11 @@ <script> -import _ from 'underscore'; -import CiIcon from '~/vue_shared/components/ci_icon.vue'; -import Icon from '~/vue_shared/components/icon.vue'; -import tooltip from '~/vue_shared/directives/tooltip'; +import JobContainerItem from './job_container_item.vue'; export default { components: { - CiIcon, - Icon, - }, - directives: { - tooltip, + JobContainerItem, }, + props: { jobs: { type: Array, @@ -26,49 +20,16 @@ export default { isJobActive(currentJobId) { return this.jobId === currentJobId; }, - tooltipText(job) { - return `${_.escape(job.name)} - ${job.status.tooltip}`; - }, }, }; </script> <template> <div class="js-jobs-container builds-container"> - <div + <job-container-item v-for="job in jobs" :key="job.id" - class="build-job" - :class="{ retried: job.retried, active: isJobActive(job.id) }" - > - <a - v-tooltip - :href="job.status.details_path" - :title="tooltipText(job)" - data-container="body" - > - <icon - v-if="isJobActive(job.id)" - name="arrow-right" - class="js-arrow-right icon-arrow-right" - /> - - <ci-icon :status="job.status" /> - - <span> - <template v-if="job.name"> - {{ job.name }} - </template> - <template v-else> - {{ job.id }} - </template> - </span> - - <icon - v-if="job.retried" - name="retry" - class="js-retry-icon" - /> - </a> - </div> + :job="job" + :is-active="isJobActive(job.id)" + /> </div> </template> diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss index af73954bd2e..1e00aa4ff7e 100644 --- a/app/assets/stylesheets/bootstrap_migration.scss +++ b/app/assets/stylesheets/bootstrap_migration.scss @@ -238,10 +238,6 @@ h3.popover-header { } .card { - .card-title { - margin-bottom: 0; - } - &.card-without-border { @extend .border-0; } @@ -255,13 +251,6 @@ h3.popover-header { } } -.card-header { - h3.card-title, - h4.card-title { - margin-top: 0; - } -} - .nav-tabs { // Override bootstrap's default border border-bottom: 0; diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss index 5ca4d944d73..3a117106cff 100644 --- a/app/assets/stylesheets/framework/panels.scss +++ b/app/assets/stylesheets/framework/panels.scss @@ -53,8 +53,3 @@ margin-top: $gl-padding; } } - -.card-title { - font-size: inherit; - line-height: inherit; -} diff --git a/app/assets/stylesheets/framework/terms.scss b/app/assets/stylesheets/framework/terms.scss index 7cda674e5c8..3f4be8829d7 100644 --- a/app/assets/stylesheets/framework/terms.scss +++ b/app/assets/stylesheets/framework/terms.scss @@ -19,17 +19,12 @@ justify-content: space-between; line-height: $line-height-base; - .card-title { + .logo-text { + width: 55px; + height: 24px; display: flex; - align-items: center; - - .logo-text { - width: 55px; - height: 24px; - display: flex; - flex-direction: column; - justify-content: center; - } + flex-direction: column; + justify-content: center; } .navbar-collapse { diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 17024e8a0af..aeee7f0a5d2 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -268,6 +268,12 @@ module Ci stage unless stage.statuses_count.zero? end + def ref_exists? + project.repository.ref_exists?(git_ref) + rescue Gitlab::Git::Repository::NoRepository + false + end + ## # TODO We do not completely switch to persisted stages because of # race conditions with setting statuses gitlab-ce#23257. @@ -674,11 +680,11 @@ module Ci def push_details strong_memoize(:push_details) do - Gitlab::Git::Push.new(project, before_sha, sha, push_ref) + Gitlab::Git::Push.new(project, before_sha, sha, git_ref) end end - def push_ref + def git_ref if branch? Gitlab::Git::BRANCH_REF_PREFIX + ref.to_s elsif tag? diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 20d53b8e620..95efecfc41d 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -31,6 +31,9 @@ module Clusters has_one :application_runner, class_name: 'Clusters::Applications::Runner' has_one :application_jupyter, class_name: 'Clusters::Applications::Jupyter' + has_many :kubernetes_namespaces + has_one :kubernetes_namespace, -> { order(id: :desc) }, class_name: 'Clusters::KubernetesNamespace' + accepts_nested_attributes_for :provider_gcp, update_only: true accepts_nested_attributes_for :platform_kubernetes, update_only: true diff --git a/app/models/clusters/kubernetes_namespace.rb b/app/models/clusters/kubernetes_namespace.rb new file mode 100644 index 00000000000..fb5f6b65d9d --- /dev/null +++ b/app/models/clusters/kubernetes_namespace.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module Clusters + class KubernetesNamespace < ActiveRecord::Base + self.table_name = 'clusters_kubernetes_namespaces' + + belongs_to :cluster_project, class_name: 'Clusters::Project' + belongs_to :cluster, class_name: 'Clusters::Cluster' + belongs_to :project, class_name: '::Project' + has_one :platform_kubernetes, through: :cluster + + validates :namespace, presence: true + validates :namespace, uniqueness: { scope: :cluster_id } + + before_validation :set_namespace_and_service_account_to_default, on: :create + + attr_encrypted :service_account_token, + mode: :per_attribute_iv, + key: Settings.attr_encrypted_db_key_base_truncated, + algorithm: 'aes-256-cbc' + + def token_name + "#{namespace}-token" + end + + private + + def set_namespace_and_service_account_to_default + self.namespace ||= default_namespace + self.service_account_name ||= default_service_account_name + end + + def default_namespace + platform_kubernetes&.namespace.presence || project_namespace + end + + def default_service_account_name + "#{namespace}-service-account" + end + + def project_namespace + Gitlab::NamespaceSanitizer.sanitize(project_slug) + end + + def project_slug + "#{project.path}-#{project.id}".downcase + end + end +end diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index 3a335909101..e8e943872de 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -7,6 +7,8 @@ module Clusters include ReactiveCaching include EnumWithNil + RESERVED_NAMESPACES = %w(gitlab-managed-apps).freeze + self.table_name = 'cluster_platforms_kubernetes' self.reactive_cache_key = ->(kubernetes) { [kubernetes.class.model_name.singular, kubernetes.id] } @@ -32,6 +34,8 @@ module Clusters message: Gitlab::Regex.kubernetes_namespace_regex_message } + validates :namespace, exclusion: { in: RESERVED_NAMESPACES } + # We expect to be `active?` only when enabled and cluster is created (the api_url is assigned) validates :api_url, url: true, presence: true validates :token, presence: true @@ -45,6 +49,7 @@ module Clusters delegate :project, to: :cluster, allow_nil: true delegate :enabled?, to: :cluster, allow_nil: true delegate :managed?, to: :cluster, allow_nil: true + delegate :kubernetes_namespace, to: :cluster alias_method :active?, :enabled? @@ -116,10 +121,19 @@ module Clusters end def default_namespace + kubernetes_namespace&.namespace.presence || fallback_default_namespace + end + + # DEPRECATED + # + # On 11.4 Clusters::KubernetesNamespace was introduced, this model will allow to + # have multiple namespaces per project. This method will be removed after migration + # has been completed. + def fallback_default_namespace return unless project slug = "#{project.path}-#{project.id}".downcase - slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '') + Gitlab::NamespaceSanitizer.sanitize(slug) end def build_kube_client!(api_groups: ['api'], api_version: 'v1') diff --git a/app/models/clusters/project.rb b/app/models/clusters/project.rb index 839ce796081..15092b1c9d2 100644 --- a/app/models/clusters/project.rb +++ b/app/models/clusters/project.rb @@ -6,5 +6,8 @@ module Clusters belongs_to :cluster, class_name: 'Clusters::Cluster' belongs_to :project, class_name: '::Project' + + has_many :kubernetes_namespaces, class_name: 'Clusters::KubernetesNamespace', foreign_key: :cluster_project_id + has_one :kubernetes_namespace, -> { order(id: :desc) }, class_name: 'Clusters::KubernetesNamespace', foreign_key: :cluster_project_id end end diff --git a/app/models/project.rb b/app/models/project.rb index 086f256174f..be99408fcea 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -688,6 +688,8 @@ class Project < ActiveRecord::Base else super end + rescue + super end def valid_import_url? @@ -1628,34 +1630,6 @@ class Project < ActiveRecord::Base end # rubocop: enable CodeReuse/ServiceClass - def rename_repo - path_before = previous_changes['path'].first - full_path_before = full_path_was - full_path_after = build_full_path - - Gitlab::AppLogger.info("Attempting to rename #{full_path_was} -> #{full_path_after}") - - if has_container_registry_tags? - Gitlab::AppLogger.info("Project #{full_path_was} cannot be renamed because container registry tags are present!") - - # we currently don't support renaming repository if it contains images in container registry - raise StandardError.new('Project cannot be renamed, because images are present in its container registry') - end - - expire_caches_before_rename(full_path_before) - - if rename_or_migrate_repository! - Gitlab::AppLogger.info("Project was renamed: #{full_path_before} -> #{full_path_after}") - after_rename_repository(full_path_before, path_before) - else - Gitlab::AppLogger.info("Repository could not be renamed: #{full_path_before} -> #{full_path_after}") - - # if we cannot move namespace directory we should rollback - # db changes in order to prevent out of sync between db and fs - raise StandardError.new('Repository cannot be renamed') - end - end - def write_repository_config(gl_full_path: full_path) # We'd need to keep track of project full path otherwise directory tree # created with hashed storage enabled cannot be usefully imported using @@ -2084,51 +2058,6 @@ class Project < ActiveRecord::Base auto_cancel_pending_pipelines == 'enabled' end - private - - # rubocop: disable CodeReuse/ServiceClass - def rename_or_migrate_repository! - if Gitlab::CurrentSettings.hashed_storage_enabled? && - storage_upgradable? && - Feature.disabled?(:skip_hashed_storage_upgrade) # kill switch in case we need to disable upgrade behavior - ::Projects::HashedStorageMigrationService.new(self, full_path_was).execute - else - storage.rename_repo - end - end - # rubocop: enable CodeReuse/ServiceClass - - def storage_upgradable? - storage_version != LATEST_STORAGE_VERSION - end - - def after_rename_repository(full_path_before, path_before) - execute_rename_repository_hooks!(full_path_before) - - write_repository_config - - # We need to check if project had been rolled out to move resource to hashed storage or not and decide - # if we need execute any take action or no-op. - unless hashed_storage?(:attachments) - Gitlab::UploadsTransfer.new.rename_project(path_before, self.path, namespace.full_path) - end - - Gitlab::PagesTransfer.new.rename_project(path_before, self.path, namespace.full_path) - end - - # rubocop: disable CodeReuse/ServiceClass - def execute_rename_repository_hooks!(full_path_before) - # When we import a project overwriting the original project, there - # is a move operation. In that case we don't want to send the instructions. - send_move_instructions(full_path_before) unless import_started? - - self.old_path_with_namespace = full_path_before - SystemHooksService.new.execute_hooks_for(self, :rename) - - reload_repository! - end - # rubocop: enable CodeReuse/ServiceClass - def storage @storage ||= if hashed_storage?(:repository) @@ -2138,6 +2067,12 @@ class Project < ActiveRecord::Base end end + def storage_upgradable? + storage_version != LATEST_STORAGE_VERSION + end + + private + def use_hashed_storage if self.new_record? && Gitlab::CurrentSettings.hashed_storage_enabled self.storage_version = LATEST_STORAGE_VERSION diff --git a/app/services/projects/after_rename_service.rb b/app/services/projects/after_rename_service.rb new file mode 100644 index 00000000000..4131da44f5a --- /dev/null +++ b/app/services/projects/after_rename_service.rb @@ -0,0 +1,135 @@ +# frozen_string_literal: true + +module Projects + # Service class for performing operations that should take place after a + # project has been renamed. + # + # Example usage: + # + # project = Project.find(42) + # + # project.update(...) + # + # Projects::AfterRenameService.new(project).execute + class AfterRenameService + attr_reader :project, :full_path_before, :full_path_after, :path_before + + RenameFailedError = Class.new(StandardError) + + # @param [Project] project The Project of the repository to rename. + def initialize(project) + @project = project + + # The full path of the namespace + project, before the rename took place. + @full_path_before = project.full_path_was + + # The full path of the namespace + project, after the rename took place. + @full_path_after = project.build_full_path + + # The path of just the project, before the rename took place. + @path_before = project.path_was + end + + def execute + first_ensure_no_registry_tags_are_present + expire_caches_before_rename + rename_or_migrate_repository! + send_move_instructions + execute_system_hooks + update_repository_configuration + rename_transferred_documents + log_completion + end + + def first_ensure_no_registry_tags_are_present + return unless project.has_container_registry_tags? + + raise RenameFailedError.new( + "Project #{full_path_before} cannot be renamed because images are " \ + "present in its container registry" + ) + end + + def expire_caches_before_rename + project.expire_caches_before_rename(full_path_before) + end + + def rename_or_migrate_repository! + success = + if migrate_to_hashed_storage? + ::Projects::HashedStorageMigrationService + .new(project, full_path_before) + .execute + else + project.storage.rename_repo + end + + rename_failed! unless success + end + + def send_move_instructions + return unless send_move_instructions? + + project.send_move_instructions(full_path_before) + end + + def execute_system_hooks + project.old_path_with_namespace = full_path_before + SystemHooksService.new.execute_hooks_for(project, :rename) + end + + def update_repository_configuration + project.reload_repository! + project.write_repository_config + end + + def rename_transferred_documents + if rename_uploads? + Gitlab::UploadsTransfer + .new + .rename_project(path_before, project_path, namespace_full_path) + end + + Gitlab::PagesTransfer + .new + .rename_project(path_before, project_path, namespace_full_path) + end + + def log_completion + Gitlab::AppLogger.info( + "Project #{project.id} has been renamed from " \ + "#{full_path_before} to #{full_path_after}" + ) + end + + def migrate_to_hashed_storage? + Gitlab::CurrentSettings.hashed_storage_enabled? && + project.storage_upgradable? && + Feature.disabled?(:skip_hashed_storage_upgrade) + end + + def send_move_instructions? + !project.import_started? + end + + def rename_uploads? + !project.hashed_storage?(:attachments) + end + + def project_path + project.path + end + + def namespace_full_path + project.namespace.full_path + end + + def rename_failed! + error = "Repository #{full_path_before} could not be renamed to #{full_path_after}" + + Gitlab::AppLogger.error(error) + + raise RenameFailedError.new(error) + end + end +end diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index f25a4e30938..93e48fc0199 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -67,7 +67,7 @@ module Projects end if project.previous_changes.include?('path') - project.rename_repo + AfterRenameService.new(project).execute else system_hook_service.execute_hooks_for(project, :update) end diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 21c1260e982..5f205d1bcbc 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -63,10 +63,9 @@ .card .card-header - %h3.card-title - = _('Projects') - %span.badge.badge-pill - #{@group.projects.count} + = _('Projects') + %span.badge.badge-pill + #{@group.projects.count} %ul.content-list - @projects.each do |project| %li diff --git a/app/views/layouts/terms.html.haml b/app/views/layouts/terms.html.haml index 977eb350365..cdad617f006 100644 --- a/app/views/layouts/terms.html.haml +++ b/app/views/layouts/terms.html.haml @@ -16,19 +16,18 @@ .content{ id: "content-body" } .card .card-header - .card-title - = brand_header_logo - - logo_text = brand_header_logo_type - - if logo_text.present? - %span.logo-text.prepend-left-8 - = logo_text - - if header_link?(:user_dropdown) - .navbar-collapse - %ul.nav.navbar-nav - %li.header-user.dropdown - = link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do - = image_tag avatar_icon_for_user(current_user, 23), width: 23, height: 23, class: "header-user-avatar qa-user-avatar" - = sprite_icon('angle-down', css_class: 'caret-down') - .dropdown-menu.dropdown-menu-right - = render 'layouts/header/current_user_dropdown' + = brand_header_logo + - logo_text = brand_header_logo_type + - if logo_text.present? + %span.logo-text.prepend-left-8 + = logo_text + - if header_link?(:user_dropdown) + .navbar-collapse + %ul.nav.navbar-nav + %li.header-user.dropdown + = link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do + = image_tag avatar_icon_for_user(current_user, 23), width: 23, height: 23, class: "header-user-avatar qa-user-avatar" + = sprite_icon('angle-down', css_class: 'caret-down') + .dropdown-menu.dropdown-menu-right + = render 'layouts/header/current_user_dropdown' = yield diff --git a/app/views/projects/branches/_panel.html.haml b/app/views/projects/branches/_panel.html.haml index 398f76d379a..0e4b119bb54 100644 --- a/app/views/projects/branches/_panel.html.haml +++ b/app/views/projects/branches/_panel.html.haml @@ -9,8 +9,7 @@ .card.prepend-top-10 .card-header - %h4.card-title - = panel_title + = panel_title %ul.content-list.all-branches - branches.first(overview_max_branches).each do |branch| = render "projects/branches/branch", branch: branch, merged: project.repository.merged_to_root_ref?(branch) diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index dbb563f51ea..2575efc0981 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -13,7 +13,11 @@ = pluralize @pipeline.total_size, "job" - if @pipeline.ref from - = link_to @pipeline.ref, project_ref_path(@project, @pipeline.ref), class: "ref-name" + - if @pipeline.ref_exists? + = link_to @pipeline.ref, project_ref_path(@project, @pipeline.ref), class: "ref-name" + - else + %span.ref-name + = @pipeline.ref - if @pipeline.duration in = time_interval_in_words(@pipeline.duration) diff --git a/app/views/projects/protected_branches/shared/_branches_list.html.haml b/app/views/projects/protected_branches/shared/_branches_list.html.haml index 9a06eca89bb..1913d06a6f8 100644 --- a/app/views/projects/protected_branches/shared/_branches_list.html.haml +++ b/app/views/projects/protected_branches/shared/_branches_list.html.haml @@ -1,8 +1,7 @@ .protected-branches-list.js-protected-branches-list.qa-protected-branches-list - if @protected_branches.empty? .card-header.bg-white - %h3.card-title.mb-0 - Protected branch (#{@protected_branches_count}) + Protected branch (#{@protected_branches_count}) %p.settings-message.text-center There are currently no protected branches, protect a branch with the form above. - else diff --git a/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml b/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml index c3b8f2f8964..d617d85afc2 100644 --- a/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml +++ b/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml @@ -2,8 +2,7 @@ %input{ type: 'hidden', name: 'update_section', value: 'js-protected-branches-settings' } .card .card-header - %h3.card-title - Protect a branch + Protect a branch .card-body = form_errors(@protected_branch) .form-group.row diff --git a/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml b/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml index b274c73d035..cbf1938664c 100644 --- a/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml +++ b/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml @@ -2,8 +2,7 @@ %input{ type: 'hidden', name: 'update_section', value: 'js-protected-tags-settings' } .card .card-header - %h3.card-title - Protect a tag + Protect a tag .card-body = form_errors(@protected_tag) .form-group.row diff --git a/app/views/projects/protected_tags/shared/_tags_list.html.haml b/app/views/projects/protected_tags/shared/_tags_list.html.haml index c3081d75fb4..382ea848243 100644 --- a/app/views/projects/protected_tags/shared/_tags_list.html.haml +++ b/app/views/projects/protected_tags/shared/_tags_list.html.haml @@ -1,8 +1,7 @@ .protected-tags-list.js-protected-tags-list - if @protected_tags.empty? .card-header - %h3.card-title - Protected tag (#{@protected_tags_count}) + Protected tag (#{@protected_tags_count}) %p.settings-message.text-center There are currently no protected tags, protect a tag with the form above. - else diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml index 0426f2215ad..db1f15f96b8 100644 --- a/app/views/projects/registry/repositories/index.html.haml +++ b/app/views/projects/registry/repositories/index.html.haml @@ -18,8 +18,7 @@ .col-lg-12 .card .card-header - %h4.card-title - = s_('ContainerRegistry|How to use the Container Registry') + = s_('ContainerRegistry|How to use the Container Registry') .card-body %p - link_token = link_to(_('personal access token'), help_page_path('user/profile/account/two_factor_authentication', anchor: 'personal-access-tokens'), target: '_blank') diff --git a/app/views/projects/services/prometheus/_metrics.html.haml b/app/views/projects/services/prometheus/_metrics.html.haml index 98d64fafe86..597c029f755 100644 --- a/app/views/projects/services/prometheus/_metrics.html.haml +++ b/app/views/projects/services/prometheus/_metrics.html.haml @@ -2,9 +2,8 @@ .card.js-panel-monitored-metrics{ data: { active_metrics: active_common_project_prometheus_metrics_path(project, :json), metrics_help_path: help_page_path('user/project/integrations/prometheus_library/metrics') } } .card-header - %h3.card-title - = s_('PrometheusService|Common metrics') - %span.badge.badge-pill.js-monitored-count 0 + = s_('PrometheusService|Common metrics') + %span.badge.badge-pill.js-monitored-count 0 .card-body .loading-metrics.js-loading-metrics %p.prepend-top-10.prepend-left-10 @@ -17,10 +16,9 @@ .card.hidden.js-panel-missing-env-vars .card-header - %h3.card-title - = icon('caret-right lg fw', class: 'panel-toggle js-panel-toggle', 'aria-label' => 'Toggle panel') - = s_('PrometheusService|Missing environment variable') - %span.badge.badge-pill.js-env-var-count 0 + = icon('caret-right lg fw', class: 'panel-toggle js-panel-toggle', 'aria-label' => 'Toggle panel') + = s_('PrometheusService|Missing environment variable') + %span.badge.badge-pill.js-env-var-count 0 .card-body.hidden .flash-container .flash-notice diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index 24724394259..5e6d06d980e 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -20,8 +20,9 @@ .col-sm-10.create-from .dropdown = hidden_field_tag :ref, default_ref - = button_tag type: 'button', title: default_ref, class: 'dropdown-menu-toggle wide js-branch-select', required: true, data: { toggle: 'dropdown', selected: default_ref, field_name: 'ref' } do + = button_tag type: 'button', title: default_ref, class: 'dropdown-menu-toggle wide js-branch-select monospace', required: true, data: { toggle: 'dropdown', selected: default_ref, field_name: 'ref' } do .text-left.dropdown-toggle-text= default_ref + = icon('chevron-down') = render 'shared/ref_dropdown', dropdown_class: 'wide' .form-text.text-muted = s_('TagsPage|Existing branch name, tag, or commit SHA') diff --git a/app/views/projects/triggers/_index.html.haml b/app/views/projects/triggers/_index.html.haml index a15bb4c4f3f..a559ce41e57 100644 --- a/app/views/projects/triggers/_index.html.haml +++ b/app/views/projects/triggers/_index.html.haml @@ -3,8 +3,7 @@ = render "projects/triggers/content" .card .card-header - %h4.card-title - Manage your project's triggers + Manage your project's triggers .card-body = render "projects/triggers/form", btn_text: "Add trigger" %hr diff --git a/app/views/shared/runners/show.html.haml b/app/views/shared/runners/show.html.haml index 362569bfbaf..f62eed694d2 100644 --- a/app/views/shared/runners/show.html.haml +++ b/app/views/shared/runners/show.html.haml @@ -24,7 +24,7 @@ %td= @runner.active? ? 'Yes' : 'No' %tr %td Protected - %td= @runner.active? ? _('Yes') : _('No') + %td= @runner.ref_protected? ? 'Yes' : 'No' %tr %td Can run untagged jobs %td= @runner.run_untagged? ? 'Yes' : 'No' diff --git a/changelogs/unreleased/42611-removed-branch-link.yml b/changelogs/unreleased/42611-removed-branch-link.yml new file mode 100644 index 00000000000..03a206871b4 --- /dev/null +++ b/changelogs/unreleased/42611-removed-branch-link.yml @@ -0,0 +1,5 @@ +--- +title: Only render link to branch when branch still exists in pipeline page +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/46884-remove-card-title.yml b/changelogs/unreleased/46884-remove-card-title.yml new file mode 100644 index 00000000000..95f08a67638 --- /dev/null +++ b/changelogs/unreleased/46884-remove-card-title.yml @@ -0,0 +1,5 @@ +--- +title: Remove .card-title from .card-header for BS4 migration +merge_request: 19335 +author: Takuya Noguchi +type: other diff --git a/changelogs/unreleased/51716-add-kubernetes-namespace-model.yml b/changelogs/unreleased/51716-add-kubernetes-namespace-model.yml new file mode 100644 index 00000000000..ad43c512ba3 --- /dev/null +++ b/changelogs/unreleased/51716-add-kubernetes-namespace-model.yml @@ -0,0 +1,5 @@ +--- +title: Introduce new model to persist specific cluster information +merge_request: 22404 +author: +type: added diff --git a/changelogs/unreleased/52840-fix-runners-details-page.yml b/changelogs/unreleased/52840-fix-runners-details-page.yml new file mode 100644 index 00000000000..b061390fcf0 --- /dev/null +++ b/changelogs/unreleased/52840-fix-runners-details-page.yml @@ -0,0 +1,5 @@ +--- +title: Fix rendering of 'Protected' value on Runner details page +merge_request: 22459 +author: +type: fixed diff --git a/changelogs/unreleased/add-new-kubernetes-spec-helpers.yml b/changelogs/unreleased/add-new-kubernetes-spec-helpers.yml new file mode 100644 index 00000000000..87023ede020 --- /dev/null +++ b/changelogs/unreleased/add-new-kubernetes-spec-helpers.yml @@ -0,0 +1,5 @@ +--- +title: Introduce new kubernetes helpers +merge_request: 22525 +author: +type: other diff --git a/changelogs/unreleased/blackst0ne-bump-mermaid.yml b/changelogs/unreleased/blackst0ne-bump-mermaid.yml new file mode 100644 index 00000000000..cb924ac8448 --- /dev/null +++ b/changelogs/unreleased/blackst0ne-bump-mermaid.yml @@ -0,0 +1,5 @@ +--- +title: Bump mermaid to 8.0.0-rc.8 +merge_request: 22509 +author: "@blackst0ne" +type: changed diff --git a/changelogs/unreleased/change-branch-font-type-in-tag-creation.yml b/changelogs/unreleased/change-branch-font-type-in-tag-creation.yml new file mode 100644 index 00000000000..0f46efb693f --- /dev/null +++ b/changelogs/unreleased/change-branch-font-type-in-tag-creation.yml @@ -0,0 +1,5 @@ +--- +title: Change branch font type in tag creation +merge_request: 22454 +author: George Tsiolis +type: other diff --git a/changelogs/unreleased/feature-gb-improve-include-config-errors-reporting.yml b/changelogs/unreleased/feature-gb-improve-include-config-errors-reporting.yml new file mode 100644 index 00000000000..67eb6b78096 --- /dev/null +++ b/changelogs/unreleased/feature-gb-improve-include-config-errors-reporting.yml @@ -0,0 +1,5 @@ +--- +title: Improve validation errors for external CI/CD configuration +merge_request: 22394 +author: +type: added diff --git a/changelogs/unreleased/gt-remove-empty-spec-describe-blocks.yml b/changelogs/unreleased/gt-remove-empty-spec-describe-blocks.yml index c1f7a2a40c7..d2a65d48d8d 100644 --- a/changelogs/unreleased/gt-remove-empty-spec-describe-blocks.yml +++ b/changelogs/unreleased/gt-remove-empty-spec-describe-blocks.yml @@ -1,5 +1,5 @@ --- title: Remove empty spec describe blocks merge_request: 22451 -author: george Tsiolis +author: George Tsiolis type: other diff --git a/changelogs/unreleased/sh-pages-eof-error.yml b/changelogs/unreleased/sh-pages-eof-error.yml new file mode 100644 index 00000000000..497a74c1458 --- /dev/null +++ b/changelogs/unreleased/sh-pages-eof-error.yml @@ -0,0 +1,5 @@ +--- +title: Fix EOF detection with CI artifacts metadata +merge_request: 22479 +author: +type: fixed diff --git a/db/migrate/20181009190428_create_clusters_kubernetes_namespaces.rb b/db/migrate/20181009190428_create_clusters_kubernetes_namespaces.rb new file mode 100644 index 00000000000..a58c190e1d6 --- /dev/null +++ b/db/migrate/20181009190428_create_clusters_kubernetes_namespaces.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class CreateClustersKubernetesNamespaces < ActiveRecord::Migration + DOWNTIME = false + INDEX_NAME = 'kubernetes_namespaces_cluster_and_namespace' + + def change + create_table :clusters_kubernetes_namespaces, id: :bigserial do |t| + t.references :cluster, null: false, index: true, foreign_key: { on_delete: :cascade } + t.references :project, index: true, foreign_key: { on_delete: :nullify } + t.references :cluster_project, index: true, foreign_key: { on_delete: :nullify } + + t.timestamps_with_timezone null: false + + t.string :encrypted_service_account_token_iv + t.string :namespace, null: false + t.string :service_account_name + + t.text :encrypted_service_account_token + + t.index [:cluster_id, :namespace], name: INDEX_NAME, unique: true + end + end +end diff --git a/db/post_migrate/20161221153951_rename_reserved_project_names.rb b/db/post_migrate/20161221153951_rename_reserved_project_names.rb index 08d7f499eec..678876e886c 100644 --- a/db/post_migrate/20161221153951_rename_reserved_project_names.rb +++ b/db/post_migrate/20161221153951_rename_reserved_project_names.rb @@ -113,7 +113,9 @@ class RenameReservedProjectNames < ActiveRecord::Migration begin # Because project path update is quite complex operation we can't safely # copy-paste all code from GitLab. As exception we use Rails code here - project.rename_repo if rename_project_row(project, path) + if rename_project_row(project, path) + Projects::AfterRenameService.new(project).execute + end rescue Exception => e # rubocop: disable Lint/RescueException Rails.logger.error "Exception when renaming project #{id}: #{e.message}" end @@ -123,6 +125,6 @@ class RenameReservedProjectNames < ActiveRecord::Migration def rename_project_row(project, path) project.respond_to?(:update_attributes) && project.update(path: path) && - project.respond_to?(:rename_repo) + defined?(Projects::AfterRenameService) end end diff --git a/db/post_migrate/20170313133418_rename_more_reserved_project_names.rb b/db/post_migrate/20170313133418_rename_more_reserved_project_names.rb index 43a37667250..26a67b0f814 100644 --- a/db/post_migrate/20170313133418_rename_more_reserved_project_names.rb +++ b/db/post_migrate/20170313133418_rename_more_reserved_project_names.rb @@ -55,7 +55,9 @@ class RenameMoreReservedProjectNames < ActiveRecord::Migration begin # Because project path update is quite complex operation we can't safely # copy-paste all code from GitLab. As exception we use Rails code here - project.rename_repo if rename_project_row(project, path) + if rename_project_row(project, path) + Projects::AfterRenameService.new(project).execute + end rescue Exception => e # rubocop: disable Lint/RescueException Rails.logger.error "Exception when renaming project #{id}: #{e.message}" end @@ -65,6 +67,6 @@ class RenameMoreReservedProjectNames < ActiveRecord::Migration def rename_project_row(project, path) project.respond_to?(:update_attributes) && project.update(path: path) && - project.respond_to?(:rename_repo) + defined?(Projects::AfterRenameService) end end diff --git a/db/schema.rb b/db/schema.rb index 3f3bec0ce04..50989960aa9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -691,6 +691,23 @@ ActiveRecord::Schema.define(version: 20181013005024) do add_index "clusters_applications_runners", ["cluster_id"], name: "index_clusters_applications_runners_on_cluster_id", unique: true, using: :btree add_index "clusters_applications_runners", ["runner_id"], name: "index_clusters_applications_runners_on_runner_id", using: :btree + create_table "clusters_kubernetes_namespaces", id: :bigserial, force: :cascade do |t| + t.integer "cluster_id", null: false + t.integer "project_id" + t.integer "cluster_project_id" + t.datetime_with_timezone "created_at", null: false + t.datetime_with_timezone "updated_at", null: false + t.text "encrypted_service_account_token" + t.string "encrypted_service_account_token_iv" + t.string "namespace", null: false + t.string "service_account_name" + end + + add_index "clusters_kubernetes_namespaces", ["cluster_id", "namespace"], name: "kubernetes_namespaces_cluster_and_namespace", unique: true, using: :btree + add_index "clusters_kubernetes_namespaces", ["cluster_id"], name: "index_clusters_kubernetes_namespaces_on_cluster_id", using: :btree + add_index "clusters_kubernetes_namespaces", ["cluster_project_id"], name: "index_clusters_kubernetes_namespaces_on_cluster_project_id", using: :btree + add_index "clusters_kubernetes_namespaces", ["project_id"], name: "index_clusters_kubernetes_namespaces_on_project_id", using: :btree + create_table "container_repositories", force: :cascade do |t| t.integer "project_id", null: false t.string "name", null: false @@ -2325,6 +2342,9 @@ ActiveRecord::Schema.define(version: 20181013005024) do add_foreign_key "clusters_applications_prometheus", "clusters", name: "fk_557e773639", on_delete: :cascade add_foreign_key "clusters_applications_runners", "ci_runners", column: "runner_id", name: "fk_02de2ded36", on_delete: :nullify add_foreign_key "clusters_applications_runners", "clusters", on_delete: :cascade + add_foreign_key "clusters_kubernetes_namespaces", "cluster_projects", on_delete: :nullify + add_foreign_key "clusters_kubernetes_namespaces", "clusters", on_delete: :cascade + add_foreign_key "clusters_kubernetes_namespaces", "projects", on_delete: :nullify add_foreign_key "container_repositories", "projects" add_foreign_key "deploy_keys_projects", "projects", name: "fk_58a901ca7e", on_delete: :cascade add_foreign_key "deployments", "projects", name: "fk_b9a3851b82", on_delete: :cascade diff --git a/doc/development/contributing/index.md b/doc/development/contributing/index.md index ec850c53deb..9da4c66933c 100644 --- a/doc/development/contributing/index.md +++ b/doc/development/contributing/index.md @@ -8,7 +8,7 @@ We want to create a welcoming environment for everyone who is interested in cont For a first-time step-by-step guide to the contribution process, please see ["Contributing to GitLab"](https://about.gitlab.com/contributing/). -Looking for something to work on? Look for issues in the [Backlog (Accepting merge requests) milestone](#i-want-to-contribute). +Looking for something to work on? Look for issues with the label [`Accepting merge requests`](#i-want-to-contribute). GitLab comes in two flavors, GitLab Community Edition (CE) our free and open source edition, and GitLab Enterprise Edition (EE) which is our commercial @@ -30,6 +30,11 @@ Please report suspected security vulnerabilities in private to Please do **NOT** create publicly viewable issues for suspected security vulnerabilities. +## Code of conduct + +Our code of conduct can be found on the +["Contributing to GitLab"](https://about.gitlab.com/contributing/) page. + ## Closing policy for issues and merge requests GitLab is a popular open source project and the capacity to deal with issues @@ -61,10 +66,10 @@ the remaining issues on the GitHub issue tracker. ## I want to contribute! -If you want to contribute to GitLab, [issues in the `Backlog (Accepting merge requests)` milestone][accepting-mrs-weight] -are a great place to start. Issues with a lower weight (1 or 2) are deemed -suitable for beginners. These issues will be of reasonable size and challenge, -for anyone to start contributing to GitLab. If you have any questions or need help visit [Getting Help](https://about.gitlab.com/getting-help/#discussion) to +If you want to contribute to GitLab, +[issues with the `Accepting merge requests` label](issue_workflow.md#label-for-community-contributors) +are a great place to start. +If you have any questions or need help visit [Getting Help](https://about.gitlab.com/getting-help/#discussion) to learn how to communicate with GitLab. If you're looking for a Gitter or Slack channel please consider we favor [asynchronous communication](https://about.gitlab.com/handbook/communication/#internal-communication) over real time communication. Thanks for your contribution! @@ -126,4 +131,3 @@ This [documentation](style_guides.md) outlines the current style guidelines. [team]: https://about.gitlab.com/team/ [getting-help]: https://about.gitlab.com/getting-help/ [codetriage]: http://www.codetriage.com/gitlabhq/gitlabhq -[accepting-mrs-weight]: https://gitlab.com/gitlab-org/gitlab-ce/issues?scope=all&utf8=✓&state=opened&assignee_id=0&milestone_title=Backlog%20(Accepting%20merge%20requests) diff --git a/doc/development/contributing/issue_workflow.md b/doc/development/contributing/issue_workflow.md index 47264bec571..4661d11b29e 100644 --- a/doc/development/contributing/issue_workflow.md +++ b/doc/development/contributing/issue_workflow.md @@ -181,10 +181,10 @@ Severity levels can be applied further depending on the facet of the impact; e.g Issues that are beneficial to our users, 'nice to haves', that we currently do not have the capacity for or want to give the priority to, are labeled as -~"Accepting Merge Requests", so the community can make a contribution. +~"Accepting merge requests", so the community can make a contribution. Community contributors can submit merge requests for any issue they want, but -the ~"Accepting Merge Requests" label has a special meaning. It points to +the ~"Accepting merge requests" label has a special meaning. It points to changes that: 1. We already agreed on, @@ -192,26 +192,26 @@ changes that: 1. Are likely to get accepted by a maintainer. We want to avoid a situation when a contributor picks an -~"Accepting Merge Requests" issue and then their merge request gets closed, +~"Accepting merge requests" issue and then their merge request gets closed, because we realize that it does not fit our vision, or we want to solve it in a different way. -We add the ~"Accepting Merge Requests" label to: +We add the ~"Accepting merge requests" label to: - Low priority ~bug issues (i.e. we do not add it to the bugs that we want to solve in the ~"Next Patch Release") - Small ~"feature proposal" - Small ~"technical debt" issues -After adding the ~"Accepting Merge Requests" label, we try to estimate the +After adding the ~"Accepting merge requests" label, we try to estimate the [weight](#issue-weight) of the issue. We use issue weight to let contributors know how difficult the issue is. Additionally: -- We advertise ["Accepting Merge Requests" issues with weight < 5][up-for-grabs] +- We advertise [`Accepting merge requests` issues with weight < 5][up-for-grabs] as suitable for people that have never contributed to GitLab before on the [Up For Grabs campaign](http://up-for-grabs.net) - We encourage people that have never contributed to any open source project to - look for ["Accepting Merge Requests" issues with a weight of 1][firt-timers] + look for [`Accepting merge requests` issues with a weight of 1][firt-timers] If you've decided that you would like to work on an issue, please @-mention the [appropriate product manager](https://about.gitlab.com/handbook/product/#who-to-talk-to-for-what) @@ -220,12 +220,12 @@ members to further discuss scope, design, and technical considerations. This wil ensure that your contribution is aligned with the GitLab product and minimize any rework and delay in getting it merged into master. -GitLab team members who apply the ~"Accepting Merge Requests" label to an issue +GitLab team members who apply the ~"Accepting merge requests" label to an issue should update the issue description with a responsible product manager, inviting any potential community contributor to @-mention per above. -[up-for-grabs]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=Accepting+Merge+Requests&scope=all&sort=weight_asc&state=opened -[firt-timers]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=Accepting+Merge+Requests&scope=all&sort=upvotes_desc&state=opened&weight=1 +[up-for-grabs]: https://gitlab.com/groups/gitlab-org/-/issues?state=opened&label_name[]=Accepting+merge+requests&assignee_id=0&sort=weight +[firt-timers]: https://gitlab.com/groups/gitlab-org/-/issues?state=opened&label_name[]=Accepting+merge+requests&assignee_id=0&sort=weight&weight=1 ## Issue triaging diff --git a/doc/development/contributing/merge_request_workflow.md b/doc/development/contributing/merge_request_workflow.md index cc7d8a1e1db..1764e2d8b21 100644 --- a/doc/development/contributing/merge_request_workflow.md +++ b/doc/development/contributing/merge_request_workflow.md @@ -2,10 +2,9 @@ We welcome merge requests with fixes and improvements to GitLab code, tests, and/or documentation. The issues that are specifically suitable for -community contributions are listed with the -[`Backlog (Accepting merge requests)` milestone in the CE issue tracker][accepting-mrs-ce] -and [EE issue tracker][accepting-mrs-ee], but you are free to contribute to any other issue -you want. +community contributions are listed with +[the `Accepting merge requests` label](issue_workflow.md#label-for-community-contributors), +but you are free to contribute to any other issue you want. Please note that if an issue is marked for the current milestone either before or while you are working on it, a team member may take over the merge request @@ -25,8 +24,6 @@ some potentially easy issues. To start with GitLab development download the [GitLab Development Kit][gdk] and see the [Development section](../../README.md) for some guidelines. -[accepting-mrs-ce]: https://gitlab.com/gitlab-org/gitlab-ce/issues?milestone_title=Backlog%20(Accepting%20merge%20requests) -[accepting-mrs-ee]: https://gitlab.com/gitlab-org/gitlab-ee/issues?milestone_title=Backlog%20(Accepting%20merge%20requests) [gitlab-mr-tracker]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests [gdk]: https://gitlab.com/gitlab-org/gitlab-development-kit diff --git a/doc/integration/saml.md b/doc/integration/saml.md index e2eea57d694..a7470d27b4b 100644 --- a/doc/integration/saml.md +++ b/doc/integration/saml.md @@ -339,6 +339,23 @@ args: { } ``` +### `uid_attribute` + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/43806) in GitLab 10.7. + +By default, the `uid` is set as the `name_id` in the SAML response. If you'd like to designate a unique attribute for the `uid`, you can set the `uid_attribute`. In the example below, the value of `uid` attribute in the SAML response is set as the `uid_attribute`. + +```yaml +args: { + assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback', + idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', + idp_sso_target_url: 'https://login.example.com/idp', + issuer: 'https://gitlab.example.com', + name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', + uid_attribute: 'uid' +} +``` + ## Troubleshooting ### 500 error after login diff --git a/lib/gitlab/ci/build/artifacts/metadata.rb b/lib/gitlab/ci/build/artifacts/metadata.rb index 375d8bc1ff5..551d4f4473e 100644 --- a/lib/gitlab/ci/build/artifacts/metadata.rb +++ b/lib/gitlab/ci/build/artifacts/metadata.rb @@ -59,9 +59,12 @@ module Gitlab until gz.eof? begin - path = read_string(gz).force_encoding('UTF-8') - meta = read_string(gz).force_encoding('UTF-8') + path = read_string(gz)&.force_encoding('UTF-8') + meta = read_string(gz)&.force_encoding('UTF-8') + # We might hit an EOF while reading either value, so we should + # abort if we don't get any data. + next unless path && meta next unless path.valid_encoding? && meta.valid_encoding? next unless path =~ match_pattern next if path =~ INVALID_PATH_PATTERN diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb index fe98d25af29..fedaf18ef30 100644 --- a/lib/gitlab/ci/config.rb +++ b/lib/gitlab/ci/config.rb @@ -13,10 +13,10 @@ module Gitlab @global = Entry::Global.new(@config) @global.compose! - rescue Loader::FormatError, Extendable::ExtensionError => e + rescue Loader::FormatError, + Extendable::ExtensionError, + External::Processor::IncludeError => e raise Config::ConfigError, e.message - rescue ::Gitlab::Ci::External::Processor::FileError => e - raise ::Gitlab::Ci::YamlProcessor::ValidationError, e.message end def valid? @@ -81,7 +81,7 @@ module Gitlab def process_external_files(config, project, opts) sha = opts.fetch(:sha) { project.repository.root_ref_sha } - ::Gitlab::Ci::External::Processor.new(config, project, sha).perform + Config::External::Processor.new(config, project, sha).perform end end end diff --git a/lib/gitlab/ci/config/external/file/base.rb b/lib/gitlab/ci/config/external/file/base.rb new file mode 100644 index 00000000000..15ca47ef60e --- /dev/null +++ b/lib/gitlab/ci/config/external/file/base.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class Config + module External + module File + class Base + include Gitlab::Utils::StrongMemoize + + attr_reader :location, :opts, :errors + + YAML_WHITELIST_EXTENSION = /.+\.(yml|yaml)$/i.freeze + + def initialize(location, opts = {}) + @location = location + @opts = opts + @errors = [] + + validate! + end + + def invalid_extension? + !::File.basename(location).match(YAML_WHITELIST_EXTENSION) + end + + def valid? + errors.none? + end + + def error_message + errors.first + end + + def content + raise NotImplementedError, 'subclass must implement fetching raw content' + end + + def to_hash + @hash ||= Ci::Config::Loader.new(content).load! + rescue Ci::Config::Loader::FormatError + nil + end + + protected + + def validate! + validate_location! + validate_content! if errors.none? + validate_hash! if errors.none? + end + + def validate_location! + if invalid_extension? + errors.push("Included file `#{location}` does not have YAML extension!") + end + end + + def validate_content! + if content.blank? + errors.push("Included file `#{location}` is empty or does not exist!") + end + end + + def validate_hash! + if to_hash.blank? + errors.push("Included file `#{location}` does not have valid YAML syntax!") + end + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/external/file/local.rb b/lib/gitlab/ci/config/external/file/local.rb new file mode 100644 index 00000000000..2a256aff65c --- /dev/null +++ b/lib/gitlab/ci/config/external/file/local.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class Config + module External + module File + class Local < Base + include Gitlab::Utils::StrongMemoize + + attr_reader :project, :sha + + def initialize(location, opts = {}) + @project = opts.fetch(:project) + @sha = opts.fetch(:sha) + + super + end + + def content + strong_memoize(:content) { fetch_local_content } + end + + private + + def validate_content! + if content.nil? + errors.push("Local file `#{location}` does not exist!") + elsif content.blank? + errors.push("Local file `#{location}` is empty!") + end + end + + def fetch_local_content + project.repository.blob_data_at(sha, location) + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/external/file/remote.rb b/lib/gitlab/ci/config/external/file/remote.rb new file mode 100644 index 00000000000..86fa5ad8800 --- /dev/null +++ b/lib/gitlab/ci/config/external/file/remote.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class Config + module External + module File + class Remote < Base + include Gitlab::Utils::StrongMemoize + + def content + strong_memoize(:content) { fetch_remote_content } + end + + private + + def validate_location! + super + + unless ::Gitlab::UrlSanitizer.valid?(location) + errors.push("Remote file `#{location}` does not have a valid address!") + end + end + + def fetch_remote_content + begin + response = Gitlab::HTTP.get(location) + rescue SocketError + errors.push("Remote file `#{location}` could not be fetched because of a socket error!") + rescue Timeout::Error + errors.push("Remote file `#{location}` could not be fetched because of a timeout error!") + rescue Gitlab::HTTP::Error + errors.push("Remote file `#{location}` could not be fetched because of HTTP error!") + rescue Gitlab::HTTP::BlockedUrlError => e + errors.push("Remote file could not be fetched because #{e}!") + end + + if response&.code.to_i >= 400 + errors.push("Remote file `#{location}` could not be fetched because of HTTP code `#{response.code}` error!") + end + + response.to_s if errors.none? + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/external/mapper.rb b/lib/gitlab/ci/config/external/mapper.rb new file mode 100644 index 00000000000..def3563e505 --- /dev/null +++ b/lib/gitlab/ci/config/external/mapper.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class Config + module External + class Mapper + def initialize(values, project, sha) + @locations = Array(values.fetch(:include, [])) + @project = project + @sha = sha + end + + def process + locations.map { |location| build_external_file(location) } + end + + private + + attr_reader :locations, :project, :sha + + def build_external_file(location) + if ::Gitlab::UrlSanitizer.valid?(location) + External::File::Remote.new(location) + else + External::File::Local.new(location, project: project, sha: sha) + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/external/processor.rb b/lib/gitlab/ci/config/external/processor.rb new file mode 100644 index 00000000000..eae0bdeb644 --- /dev/null +++ b/lib/gitlab/ci/config/external/processor.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class Config + module External + class Processor + IncludeError = Class.new(StandardError) + + def initialize(values, project, sha) + @values = values + @external_files = External::Mapper.new(values, project, sha).process + @content = {} + end + + def perform + return @values if @external_files.empty? + + validate_external_files! + merge_external_files! + append_inline_content! + remove_include_keyword! + end + + private + + def validate_external_files! + @external_files.each do |file| + raise IncludeError, file.error_message unless file.valid? + end + end + + def merge_external_files! + @external_files.each do |file| + @content.deep_merge!(file.to_hash) + end + end + + def append_inline_content! + @content.deep_merge!(@values) + end + + def remove_include_keyword! + @content.tap { @content.delete(:include) } + end + end + end + end + end +end diff --git a/lib/gitlab/ci/external/file/base.rb b/lib/gitlab/ci/external/file/base.rb deleted file mode 100644 index f4da07b0b02..00000000000 --- a/lib/gitlab/ci/external/file/base.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - module External - module File - class Base - YAML_WHITELIST_EXTENSION = /(yml|yaml)$/i.freeze - - def initialize(location, opts = {}) - @location = location - end - - def valid? - location.match(YAML_WHITELIST_EXTENSION) && content - end - - def content - raise NotImplementedError, 'content must be implemented and return a string or nil' - end - - def error_message - raise NotImplementedError, 'error_message must be implemented and return a string' - end - end - end - end - end -end diff --git a/lib/gitlab/ci/external/file/local.rb b/lib/gitlab/ci/external/file/local.rb deleted file mode 100644 index 1aa7f687507..00000000000 --- a/lib/gitlab/ci/external/file/local.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - module External - module File - class Local < Base - attr_reader :location, :project, :sha - - def initialize(location, opts = {}) - super - - @project = opts.fetch(:project) - @sha = opts.fetch(:sha) - end - - def content - @content ||= fetch_local_content - end - - def error_message - "Local file '#{location}' is not valid." - end - - private - - def fetch_local_content - project.repository.blob_data_at(sha, location) - end - end - end - end - end -end diff --git a/lib/gitlab/ci/external/file/remote.rb b/lib/gitlab/ci/external/file/remote.rb deleted file mode 100644 index 59bb3e8999e..00000000000 --- a/lib/gitlab/ci/external/file/remote.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - module External - module File - class Remote < Base - include Gitlab::Utils::StrongMemoize - attr_reader :location - - def content - return @content if defined?(@content) - - @content = strong_memoize(:content) do - begin - Gitlab::HTTP.get(location) - rescue Gitlab::HTTP::Error, Timeout::Error, SocketError, Gitlab::HTTP::BlockedUrlError - nil - end - end - end - - def error_message - "Remote file '#{location}' is not valid." - end - end - end - end - end -end diff --git a/lib/gitlab/ci/external/mapper.rb b/lib/gitlab/ci/external/mapper.rb deleted file mode 100644 index 58bd6a19acf..00000000000 --- a/lib/gitlab/ci/external/mapper.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - module External - class Mapper - def initialize(values, project, sha) - @locations = Array(values.fetch(:include, [])) - @project = project - @sha = sha - end - - def process - locations.map { |location| build_external_file(location) } - end - - private - - attr_reader :locations, :project, :sha - - def build_external_file(location) - if ::Gitlab::UrlSanitizer.valid?(location) - Gitlab::Ci::External::File::Remote.new(location) - else - options = { project: project, sha: sha } - Gitlab::Ci::External::File::Local.new(location, options) - end - end - end - end - end -end diff --git a/lib/gitlab/ci/external/processor.rb b/lib/gitlab/ci/external/processor.rb deleted file mode 100644 index 76cf3ce89f9..00000000000 --- a/lib/gitlab/ci/external/processor.rb +++ /dev/null @@ -1,52 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - module External - class Processor - FileError = Class.new(StandardError) - - def initialize(values, project, sha) - @values = values - @external_files = Gitlab::Ci::External::Mapper.new(values, project, sha).process - @content = {} - end - - def perform - return values if external_files.empty? - - external_files.each do |external_file| - validate_external_file(external_file) - @content.deep_merge!(content_of(external_file)) - end - - append_inline_content - remove_include_keyword - end - - private - - attr_reader :values, :external_files, :content - - def validate_external_file(external_file) - unless external_file.valid? - raise FileError, external_file.error_message - end - end - - def content_of(external_file) - Gitlab::Ci::Config::Loader.new(external_file.content).load! - end - - def append_inline_content - @content.deep_merge!(@values) - end - - def remove_include_keyword - content.delete(:include) - content - end - end - end - end -end diff --git a/lib/gitlab/namespace_sanitizer.rb b/lib/gitlab/namespace_sanitizer.rb new file mode 100644 index 00000000000..d755bbbcaf9 --- /dev/null +++ b/lib/gitlab/namespace_sanitizer.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Gitlab + class NamespaceSanitizer + def self.sanitize(namespace) + namespace.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '') + end + end +end diff --git a/package.json b/package.json index 8ec47bc2837..5b0a92ee7a1 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "karma-coverage": "BABEL_ENV=coverage karma start --single-run true config/karma.config.js", "karma-start": "BABEL_ENV=karma karma start config/karma.config.js", "postinstall": "node ./scripts/frontend/postinstall.js", - "prettier-staged": "node ./scripts/frontend/prettier.js", + "prettier-staged": "node ./scripts/frontend/prettier.js check", "prettier-staged-save": "node ./scripts/frontend/prettier.js save", "prettier-all": "node ./scripts/frontend/prettier.js check-all", "prettier-all-save": "node ./scripts/frontend/prettier.js save-all", @@ -29,7 +29,6 @@ "autosize": "^4.0.0", "axios": "^0.17.1", "babel-loader": "^8.0.4", - "blackst0ne-mermaid": "^7.1.0-fixed", "bootstrap": "4.1.1", "brace-expansion": "^1.1.8", "cache-loader": "^1.2.2", @@ -71,6 +70,7 @@ "jszip-utils": "^0.0.2", "katex": "^0.9.0", "marked": "^0.3.12", + "mermaid": "^8.0.0-rc.8", "monaco-editor": "^0.14.3", "monaco-editor-webpack-plugin": "^1.5.4", "mousetrap": "^1.4.6", @@ -126,7 +126,6 @@ "eslint-plugin-jasmine": "^2.10.1", "gettext-extractor": "^3.3.2", "gettext-extractor-vue": "^4.0.1", - "ignore": "^3.3.7", "istanbul": "^0.4.5", "jasmine-core": "^2.9.0", "jasmine-diff": "^0.1.3", diff --git a/scripts/frontend/frontend_script_utils.js b/scripts/frontend/frontend_script_utils.js index e42b912d359..e3d357b4a40 100644 --- a/scripts/frontend/frontend_script_utils.js +++ b/scripts/frontend/frontend_script_utils.js @@ -13,7 +13,8 @@ const execGitCmd = args => exec('git', args) .trim() .toString() - .split('\n'); + .split('\n') + .filter(Boolean); module.exports = { getStagedFiles: fileExtensionFilter => { diff --git a/scripts/frontend/prettier.js b/scripts/frontend/prettier.js index b66ba885701..ce86a9f4601 100644 --- a/scripts/frontend/prettier.js +++ b/scripts/frontend/prettier.js @@ -1,126 +1,116 @@ const glob = require('glob'); const prettier = require('prettier'); const fs = require('fs'); -const path = require('path'); -const prettierIgnore = require('ignore')(); +const { getStagedFiles } = require('./frontend_script_utils'); -const getStagedFiles = require('./frontend_script_utils').getStagedFiles; +const matchExtensions = ['js', 'vue']; + +// This will improve glob performance by excluding certain directories. +// The .prettierignore file will also be respected, but after the glob has executed. +const globIgnore = ['**/node_modules/**', 'vendor/**', 'public/**']; + +const readFileAsync = (file, options) => + new Promise((resolve, reject) => { + fs.readFile(file, options, function(err, data) { + if (err) reject(err); + else resolve(data); + }); + }); + +const writeFileAsync = (file, data, options) => + new Promise((resolve, reject) => { + fs.writeFile(file, data, options, function(err) { + if (err) reject(err); + else resolve(); + }); + }); const mode = process.argv[2] || 'check'; const shouldSave = mode === 'save' || mode === 'save-all'; const allFiles = mode === 'check-all' || mode === 'save-all'; -let dirPath = process.argv[3] || ''; -if (dirPath && dirPath.charAt(dirPath.length - 1) !== '/') dirPath += '/'; - -const config = { - patterns: ['**/*.js', '**/*.vue', '**/*.scss'], - /* - * The ignore patterns below are just to reduce search time with glob, as it includes the - * folders with the most ignored assets, the actual `.prettierignore` will be used later on - */ - ignore: ['**/node_modules/**', '**/vendor/**', '**/public/**'], - parsers: { - js: 'babylon', - vue: 'vue', - scss: 'css', - }, -}; +let globDir = process.argv[3] || ''; +if (globDir && globDir.charAt(globDir.length - 1) !== '/') globDir += '/'; -/* - * Unfortunately the prettier API does not expose support for `.prettierignore` files, they however - * use the ignore package, so we do the same. We simply cannot use the glob package, because - * gitignore style is not compatible with globs ignore style. - */ -prettierIgnore.add( - fs - .readFileSync(path.join(__dirname, '../../', '.prettierignore')) - .toString() - .trim() - .split(/\r?\n/) +console.log( + `Loading all ${allFiles ? '' : 'staged '}files ${globDir ? `within ${globDir} ` : ''}...` ); -const availableExtensions = Object.keys(config.parsers); - -console.log(`Loading ${allFiles ? 'All' : 'Selected'} Files ...`); +const globPatterns = matchExtensions.map(ext => `${globDir}**/*.${ext}`); +const matchedFiles = allFiles + ? glob.sync(`{${globPatterns.join(',')}}`, { ignore: globIgnore }) + : getStagedFiles(globPatterns); +const matchedCount = matchedFiles.length; -const stagedFiles = - allFiles || dirPath ? null : getStagedFiles(availableExtensions.map(ext => `*.${ext}`)); - -if (stagedFiles) { - if (!stagedFiles.length || (stagedFiles.length === 1 && !stagedFiles[0])) { - console.log('No matching staged files.'); - process.exit(1); - } - console.log(`Matching staged Files : ${stagedFiles.length}`); +if (!matchedCount) { + console.log('No files found to process with prettier'); + process.exit(0); } let didWarn = false; -let didError = false; - -let files; -if (allFiles) { - const ignore = config.ignore; - const patterns = config.patterns; - const globPattern = patterns.length > 1 ? `{${patterns.join(',')}}` : `${patterns.join(',')}`; - files = glob.sync(globPattern, { ignore }).filter(f => allFiles || stagedFiles.includes(f)); -} else if (dirPath) { - const ignore = config.ignore; - const patterns = config.patterns.map(item => { - return dirPath + item; - }); - const globPattern = patterns.length > 1 ? `{${patterns.join(',')}}` : `${patterns.join(',')}`; - files = glob.sync(globPattern, { ignore }); -} else { - files = stagedFiles.filter(f => availableExtensions.includes(f.split('.').pop())); -} - -files = prettierIgnore.filter(files); - -if (!files.length) { - console.log('No Files found to process with Prettier'); - process.exit(1); -} - -console.log(`${shouldSave ? 'Updating' : 'Checking'} ${files.length} file(s)`); - -files.forEach(file => { - try { - prettier - .resolveConfig(file) - .then(options => { - const fileExtension = file.split('.').pop(); - Object.assign(options, { - parser: config.parsers[fileExtension], +let passedCount = 0; +let failedCount = 0; +let ignoredCount = 0; + +console.log(`${shouldSave ? 'Updating' : 'Checking'} ${matchedCount} file(s)`); + +const fixCommand = `yarn prettier-${allFiles ? 'all' : 'staged'}-save`; +const warningMessage = ` +=============================== +GitLab uses Prettier to format all JavaScript code. +Please format each file listed below or run "${fixCommand}" +=============================== +`; + +const checkFileWithOptions = (filePath, options) => + readFileAsync(filePath, 'utf8').then(input => { + if (shouldSave) { + const output = prettier.format(input, options); + if (input === output) { + passedCount += 1; + } else { + return writeFileAsync(filePath, output, 'utf8').then(() => { + console.log(`Prettified : ${filePath}`); + failedCount += 1; }); - - const input = fs.readFileSync(file, 'utf8'); - - if (shouldSave) { - const output = prettier.format(input, options); - if (output !== input) { - fs.writeFileSync(file, output, 'utf8'); - console.log(`Prettified : ${file}`); - } - } else if (!prettier.check(input, options)) { - if (!didWarn) { - console.log( - '\n===============================\nGitLab uses Prettier to format all JavaScript code.\nPlease format each file listed below or run "yarn prettier-staged-save"\n===============================\n' - ); - didWarn = true; - } - console.log(`Prettify Manually : ${file}`); + } + } else { + if (prettier.check(input, options)) { + passedCount += 1; + } else { + if (!didWarn) { + console.log(warningMessage); + didWarn = true; } - }) - .catch(e => { - console.log(`Error on loading the Config File: ${e.message}`); - process.exit(1); - }); - } catch (error) { - didError = true; - console.log(`\n\nError with ${file}: ${error.message}`); - } -}); + console.log(`Prettify Manually : ${filePath}`); + failedCount += 1; + } + } + }); -if (didWarn || didError) { - process.exit(1); -} +const checkFileWithPrettierConfig = filePath => + prettier + .getFileInfo(filePath, { ignorePath: '.prettierignore' }) + .then(({ ignored, inferredParser }) => { + if (ignored || !inferredParser) { + ignoredCount += 1; + return; + } + return prettier.resolveConfig(filePath).then(fileOptions => { + const options = { ...fileOptions, parser: inferredParser }; + return checkFileWithOptions(filePath, options); + }); + }); + +Promise.all(matchedFiles.map(checkFileWithPrettierConfig)) + .then(() => { + const failAction = shouldSave ? 'fixed' : 'failed'; + console.log( + `\nSummary:\n ${matchedCount} files processed (${passedCount} passed, ${failedCount} ${failAction}, ${ignoredCount} ignored)\n` + ); + + if (didWarn) process.exit(1); + }) + .catch(e => { + console.log(`\nAn error occured while processing files with prettier: ${e.message}\n`); + process.exit(1); + }); diff --git a/spec/factories/ci/runners.rb b/spec/factories/ci/runners.rb index f564e7bee47..24e70913b87 100644 --- a/spec/factories/ci/runners.rb +++ b/spec/factories/ci/runners.rb @@ -47,5 +47,15 @@ FactoryBot.define do trait :ref_protected do access_level :ref_protected end + + trait :tagged_only do + run_untagged false + + tag_list %w(tag1 tag2) + end + + trait :locked do + locked true + end end end diff --git a/spec/factories/clusters/kubernetes_namespaces.rb b/spec/factories/clusters/kubernetes_namespaces.rb new file mode 100644 index 00000000000..6fdada75a3d --- /dev/null +++ b/spec/factories/clusters/kubernetes_namespaces.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :cluster_kubernetes_namespace, class: Clusters::KubernetesNamespace do + cluster + project + cluster_project + end +end diff --git a/spec/factories/clusters/projects.rb b/spec/factories/clusters/projects.rb new file mode 100644 index 00000000000..6cda77c6f85 --- /dev/null +++ b/spec/factories/clusters/projects.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :cluster_project, class: Clusters::Project do + cluster + project + end +end diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 491c64fc329..cd6c37bf54d 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -68,6 +68,10 @@ describe 'Pipeline', :js do expect(page).to have_css('#js-tab-pipeline.active') end + it 'shows link to the pipeline ref' do + expect(page).to have_link(pipeline.ref) + end + it_behaves_like 'showing user status' do let(:user_with_status) { pipeline.user } @@ -236,6 +240,20 @@ describe 'Pipeline', :js do it { expect(page).not_to have_content('Cancel running') } end end + + context 'when pipeline ref does not exist in repository anymore' do + let(:pipeline) do + create(:ci_empty_pipeline, project: project, + ref: 'non-existent', + sha: project.commit.id, + user: user) + end + + it 'does not render link to the pipeline ref' do + expect(page).not_to have_link(pipeline.ref) + expect(page).to have_content(pipeline.ref) + end + end end context 'when user does not have access to read jobs' do diff --git a/spec/javascripts/jobs/components/job_container_item_spec.js b/spec/javascripts/jobs/components/job_container_item_spec.js new file mode 100644 index 00000000000..8588eda19c8 --- /dev/null +++ b/spec/javascripts/jobs/components/job_container_item_spec.js @@ -0,0 +1,73 @@ +import Vue from 'vue'; +import JobContainerItem from '~/jobs/components/job_container_item.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import job from '../mock_data'; + +describe('JobContainerItem', () => { + const Component = Vue.extend(JobContainerItem); + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + const sharedTests = () => { + it('displays a status icon', () => { + expect(vm.$el).toHaveSpriteIcon(job.status.icon); + }); + + it('displays the job name', () => { + expect(vm.$el).toContainText(job.name); + }); + + it('displays a link to the job', () => { + const link = vm.$el.querySelector('.js-job-link'); + + expect(link.href).toBe(job.status.details_path); + }); + }; + + describe('when a job is not active and not retied', () => { + beforeEach(() => { + vm = mountComponent(Component, { + job, + isActive: false, + }); + }); + + sharedTests(); + }); + + describe('when a job is active', () => { + beforeEach(() => { + vm = mountComponent(Component, { + job, + isActive: true, + }); + }); + + sharedTests(); + + it('displays an arrow', () => { + expect(vm.$el).toHaveSpriteIcon('arrow-right'); + }); + }); + + describe('when a job is retried', () => { + beforeEach(() => { + vm = mountComponent(Component, { + job: { + ...job, + retried: true, + }, + isActive: false, + }); + }); + + sharedTests(); + + it('displays an icon', () => { + expect(vm.$el).toHaveSpriteIcon('retry'); + }); + }); +}); diff --git a/spec/javascripts/jobs/mock_data.js b/spec/javascripts/jobs/mock_data.js index ca6fbabeeb6..0398f184c0a 100644 --- a/spec/javascripts/jobs/mock_data.js +++ b/spec/javascripts/jobs/mock_data.js @@ -1,3 +1,5 @@ +import { TEST_HOST } from 'spec/test_constants'; + const threeWeeksAgo = new Date(); threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21); @@ -19,7 +21,7 @@ export default { label: 'passed', group: 'success', has_details: true, - details_path: '/root/ci-mock/-/jobs/4757', + details_path: `${TEST_HOST}/root/ci-mock/-/jobs/4757`, favicon: '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', action: { diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb index e327399d82d..a9a4af1f455 100644 --- a/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb +++ b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb @@ -112,4 +112,34 @@ describe Gitlab::Ci::Build::Artifacts::Metadata do end end end + + context 'generated metadata' do + let(:tmpfile) { Tempfile.new('test-metadata') } + let(:generator) { CiArtifactMetadataGenerator.new(tmpfile) } + let(:entry_count) { 5 } + + before do + tmpfile.binmode + + (1..entry_count).each do |index| + generator.add_entry("public/test-#{index}.txt") + end + + generator.write + end + + after do + File.unlink(tmpfile.path) + end + + describe '#find_entries!' do + it 'reads expected number of entries' do + stream = File.open(tmpfile.path) + + metadata = described_class.new(stream, 'public', { recursive: true }) + + expect(metadata.find_entries!.count).to eq entry_count + end + end + end end diff --git a/spec/lib/gitlab/ci/config/external/file/base_spec.rb b/spec/lib/gitlab/ci/config/external/file/base_spec.rb new file mode 100644 index 00000000000..2e92d5204d6 --- /dev/null +++ b/spec/lib/gitlab/ci/config/external/file/base_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +describe Gitlab::Ci::Config::External::File::Base do + subject { described_class.new(location) } + + before do + allow_any_instance_of(described_class) + .to receive(:content).and_return('key: value') + end + + describe '#valid?' do + context 'when location is not a YAML file' do + let(:location) { 'some/file.txt' } + + it { is_expected.not_to be_valid } + end + + context 'when location has not a valid naming scheme' do + let(:location) { 'some/file/.yml' } + + it { is_expected.not_to be_valid } + end + + context 'when location is a valid .yml extension' do + let(:location) { 'some/file/config.yml' } + + it { is_expected.to be_valid } + end + + context 'when location is a valid .yaml extension' do + let(:location) { 'some/file/config.yaml' } + + it { is_expected.to be_valid } + end + + context 'when there are YAML syntax errors' do + let(:location) { 'some/file/config.yml' } + + before do + allow_any_instance_of(described_class) + .to receive(:content).and_return('invalid_syntax') + end + + it 'is not a valid file' do + expect(subject).not_to be_valid + expect(subject.error_message).to match /does not have valid YAML syntax/ + end + end + end +end diff --git a/spec/lib/gitlab/ci/external/file/local_spec.rb b/spec/lib/gitlab/ci/config/external/file/local_spec.rb index 73bb4ccf468..2708d8d5b6b 100644 --- a/spec/lib/gitlab/ci/external/file/local_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/local_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Gitlab::Ci::External::File::Local do +describe Gitlab::Ci::Config::External::File::Local do let(:project) { create(:project, :repository) } let(:local_file) { described_class.new(location, { project: project, sha: '12345' }) } @@ -72,7 +72,7 @@ describe Gitlab::Ci::External::File::Local do let(:location) { '/lib/gitlab/ci/templates/non-existent-file.yml' } it 'should return an error message' do - expect(local_file.error_message).to eq("Local file '#{location}' is not valid.") + expect(local_file.error_message).to eq("Local file `#{location}` does not exist!") end end end diff --git a/spec/lib/gitlab/ci/external/file/remote_spec.rb b/spec/lib/gitlab/ci/config/external/file/remote_spec.rb index b1819c8960b..7c1a1c38736 100644 --- a/spec/lib/gitlab/ci/external/file/remote_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/remote_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Gitlab::Ci::External::File::Remote do +describe Gitlab::Ci::Config::External::File::Remote do let(:remote_file) { described_class.new(location) } let(:location) { 'https://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' } let(:remote_file_content) do @@ -105,10 +105,53 @@ describe Gitlab::Ci::External::File::Remote do end describe "#error_message" do - let(:location) { 'not-valid://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' } + subject { remote_file.error_message } - it 'should return an error message' do - expect(remote_file.error_message).to eq("Remote file '#{location}' is not valid.") + context 'when remote file location is not valid' do + let(:location) { 'not-valid://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' } + + it 'returns an error message describing invalid address' do + expect(subject).to match /does not have a valid address!/ + end + end + + context 'when timeout error has been raised' do + before do + WebMock.stub_request(:get, location).to_timeout + end + + it 'should returns error message about a timeout' do + expect(subject).to match /could not be fetched because of a timeout error!/ + end + end + + context 'when HTTP error has been raised' do + before do + WebMock.stub_request(:get, location).to_raise(Gitlab::HTTP::Error) + end + + it 'should returns error message about a HTTP error' do + expect(subject).to match /could not be fetched because of HTTP error!/ + end + end + + context 'when response has 404 status' do + before do + WebMock.stub_request(:get, location).to_return(body: remote_file_content, status: 404) + end + + it 'should returns error message about a timeout' do + expect(subject).to match /could not be fetched because of HTTP code `404` error!/ + end + end + + context 'when the URL is blocked' do + let(:location) { 'http://127.0.0.1/some/path/to/config.yaml' } + + it 'should include details about blocked URL' do + expect(subject).to eq "Remote file could not be fetched because URL '#{location}' " \ + 'is blocked: Requests to localhost are not allowed!' + end end end end diff --git a/spec/lib/gitlab/ci/external/mapper_spec.rb b/spec/lib/gitlab/ci/config/external/mapper_spec.rb index d925d6af73d..5b236fe99f1 100644 --- a/spec/lib/gitlab/ci/external/mapper_spec.rb +++ b/spec/lib/gitlab/ci/config/external/mapper_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Gitlab::Ci::External::Mapper do +describe Gitlab::Ci::Config::External::Mapper do let(:project) { create(:project, :repository) } let(:file_content) do <<~HEREDOC @@ -27,7 +27,8 @@ describe Gitlab::Ci::External::Mapper do end it 'returns File instances' do - expect(subject.first).to be_an_instance_of(Gitlab::Ci::External::File::Local) + expect(subject.first) + .to be_an_instance_of(Gitlab::Ci::Config::External::File::Local) end end @@ -49,7 +50,8 @@ describe Gitlab::Ci::External::Mapper do end it 'returns File instances' do - expect(subject.first).to be_an_instance_of(Gitlab::Ci::External::File::Remote) + expect(subject.first) + .to be_an_instance_of(Gitlab::Ci::Config::External::File::Remote) end end end diff --git a/spec/lib/gitlab/ci/external/processor_spec.rb b/spec/lib/gitlab/ci/config/external/processor_spec.rb index 3c7394f53d2..1a05f716247 100644 --- a/spec/lib/gitlab/ci/external/processor_spec.rb +++ b/spec/lib/gitlab/ci/config/external/processor_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Gitlab::Ci::External::Processor do +describe Gitlab::Ci::Config::External::Processor do let(:project) { create(:project, :repository) } let(:processor) { described_class.new(values, project, '12345') } @@ -20,8 +20,8 @@ describe Gitlab::Ci::External::Processor do it 'should raise an error' do expect { processor.perform }.to raise_error( - described_class::FileError, - "Local file '/lib/gitlab/ci/templates/non-existent-file.yml' is not valid." + described_class::IncludeError, + "Local file `/lib/gitlab/ci/templates/non-existent-file.yml` does not exist!" ) end end @@ -36,8 +36,8 @@ describe Gitlab::Ci::External::Processor do it 'should raise an error' do expect { processor.perform }.to raise_error( - described_class::FileError, - "Remote file '#{remote_file}' is not valid." + described_class::IncludeError, + "Remote file `#{remote_file}` could not be fetched because of a socket error!" ) end end @@ -92,7 +92,8 @@ describe Gitlab::Ci::External::Processor do end before do - allow_any_instance_of(Gitlab::Ci::External::File::Local).to receive(:fetch_local_content).and_return(local_file_content) + allow_any_instance_of(Gitlab::Ci::Config::External::File::Local) + .to receive(:fetch_local_content).and_return(local_file_content) end it 'should append the file to the values' do @@ -131,7 +132,10 @@ describe Gitlab::Ci::External::Processor do before do local_file_content = File.read(Rails.root.join('spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml')) - allow_any_instance_of(Gitlab::Ci::External::File::Local).to receive(:fetch_local_content).and_return(local_file_content) + + allow_any_instance_of(Gitlab::Ci::Config::External::File::Local) + .to receive(:fetch_local_content).and_return(local_file_content) + WebMock.stub_request(:get, remote_file).to_return(body: remote_file_content) end @@ -150,11 +154,15 @@ describe Gitlab::Ci::External::Processor do let(:local_file_content) { 'invalid content file ////' } before do - allow_any_instance_of(Gitlab::Ci::External::File::Local).to receive(:fetch_local_content).and_return(local_file_content) + allow_any_instance_of(Gitlab::Ci::Config::External::File::Local) + .to receive(:fetch_local_content).and_return(local_file_content) end it 'should raise an error' do - expect { processor.perform }.to raise_error(Gitlab::Ci::Config::Loader::FormatError) + expect { processor.perform }.to raise_error( + described_class::IncludeError, + "Included file `/lib/gitlab/ci/templates/template.yml` does not have valid YAML syntax!" + ) end end diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb index b43aca8a354..975e11e8cc1 100644 --- a/spec/lib/gitlab/ci/config_spec.rb +++ b/spec/lib/gitlab/ci/config_spec.rb @@ -1,6 +1,4 @@ -require 'fast_spec_helper' - -require_dependency 'active_model' +require 'spec_helper' describe Gitlab::Ci::Config do let(:config) do @@ -202,8 +200,8 @@ describe Gitlab::Ci::Config do it 'raises error YamlProcessor validationError' do expect { config }.to raise_error( - ::Gitlab::Ci::YamlProcessor::ValidationError, - "Local file 'invalid' is not valid." + described_class::ConfigError, + "Included file `invalid` does not have YAML extension!" ) end end diff --git a/spec/migrations/rename_more_reserved_project_names_spec.rb b/spec/migrations/rename_more_reserved_project_names_spec.rb index 034e8a6a4e5..baf16c2ce53 100644 --- a/spec/migrations/rename_more_reserved_project_names_spec.rb +++ b/spec/migrations/rename_more_reserved_project_names_spec.rb @@ -31,7 +31,16 @@ describe RenameMoreReservedProjectNames, :delete do context 'when exception is raised during rename' do before do - allow(project).to receive(:rename_repo).and_raise(StandardError) + service = instance_double('service') + + allow(service) + .to receive(:execute) + .and_raise(Projects::AfterRenameService::RenameFailedError) + + allow(Projects::AfterRenameService) + .to receive(:new) + .with(project) + .and_return(service) end it 'captures exception from project rename' do diff --git a/spec/migrations/rename_reserved_project_names_spec.rb b/spec/migrations/rename_reserved_project_names_spec.rb index 592ac2b5fb9..7818aa0d560 100644 --- a/spec/migrations/rename_reserved_project_names_spec.rb +++ b/spec/migrations/rename_reserved_project_names_spec.rb @@ -35,7 +35,16 @@ describe RenameReservedProjectNames, :migration, schema: :latest do context 'when exception is raised during rename' do before do - allow(project).to receive(:rename_repo).and_raise(StandardError) + service = instance_double('service') + + allow(service) + .to receive(:execute) + .and_raise(Projects::AfterRenameService::RenameFailedError) + + allow(Projects::AfterRenameService) + .to receive(:new) + .with(project) + .and_return(service) end it 'captures exception from project rename' do diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 3b01b39ecab..153244b2159 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -779,6 +779,41 @@ describe Ci::Pipeline, :mailer do end end + describe 'ref_exists?' do + context 'when repository exists' do + using RSpec::Parameterized::TableSyntax + + let(:project) { create(:project, :repository) } + + where(:tag, :ref, :result) do + false | 'master' | true + false | 'non-existent-branch' | false + true | 'v1.1.0' | true + true | 'non-existent-tag' | false + end + + with_them do + let(:pipeline) do + create(:ci_empty_pipeline, project: project, tag: tag, ref: ref) + end + + it "correctly detects ref" do + expect(pipeline.ref_exists?).to be result + end + end + end + + context 'when repository does not exist' do + let(:pipeline) do + create(:ci_empty_pipeline, project: project, ref: 'master') + end + + it 'always returns false' do + expect(pipeline.ref_exists?).to eq false + end + end + end + context 'with non-empty project' do let(:project) { create(:project, :repository) } diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index 34d321ec604..f5c4b0b66ae 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -11,6 +11,9 @@ describe Clusters::Cluster do it { is_expected.to have_one(:application_ingress) } it { is_expected.to have_one(:application_prometheus) } it { is_expected.to have_one(:application_runner) } + it { is_expected.to have_many(:kubernetes_namespaces) } + it { is_expected.to have_one(:kubernetes_namespace) } + it { is_expected.to delegate_method(:status).to(:provider) } it { is_expected.to delegate_method(:status_reason).to(:provider) } it { is_expected.to delegate_method(:status_name).to(:provider) } @@ -20,6 +23,7 @@ describe Clusters::Cluster do it { is_expected.to delegate_method(:available?).to(:application_helm).with_prefix } it { is_expected.to delegate_method(:available?).to(:application_ingress).with_prefix } it { is_expected.to delegate_method(:available?).to(:application_prometheus).with_prefix } + it { is_expected.to respond_to :project } describe '.enabled' do diff --git a/spec/models/clusters/kubernetes_namespace_spec.rb b/spec/models/clusters/kubernetes_namespace_spec.rb new file mode 100644 index 00000000000..dea58fa26c7 --- /dev/null +++ b/spec/models/clusters/kubernetes_namespace_spec.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Clusters::KubernetesNamespace, type: :model do + it { is_expected.to belong_to(:cluster_project) } + it { is_expected.to belong_to(:project) } + it { is_expected.to belong_to(:cluster) } + it { is_expected.to have_one(:platform_kubernetes) } + + describe 'namespace uniqueness validation' do + let(:cluster_project) { create(:cluster_project) } + + let(:kubernetes_namespace) do + build(:cluster_kubernetes_namespace, + cluster: cluster_project.cluster, + project: cluster_project.project, + cluster_project: cluster_project) + end + + subject { kubernetes_namespace } + + context 'when cluster is using the namespace' do + before do + create(:cluster_kubernetes_namespace, + cluster: cluster_project.cluster, + project: cluster_project.project, + cluster_project: cluster_project, + namespace: kubernetes_namespace.namespace) + end + + it { is_expected.not_to be_valid } + end + + context 'when cluster is not using the namespace' do + it { is_expected.to be_valid } + end + end + + describe '#set_namespace_and_service_account_to_default' do + let(:cluster) { platform.cluster } + let(:cluster_project) { create(:cluster_project, cluster: cluster) } + let(:kubernetes_namespace) do + create(:cluster_kubernetes_namespace, + cluster: cluster_project.cluster, + project: cluster_project.project, + cluster_project: cluster_project) + end + + describe 'namespace' do + let(:platform) { create(:cluster_platform_kubernetes, namespace: namespace) } + + subject { kubernetes_namespace.namespace } + + context 'when platform has a namespace assigned' do + let(:namespace) { 'platform-namespace' } + + it 'should copy the namespace' do + is_expected.to eq('platform-namespace') + end + end + + context 'when platform does not have namespace assigned' do + let(:namespace) { nil } + + it 'should set default namespace' do + project_slug = "#{cluster_project.project.path}-#{cluster_project.project_id}" + + is_expected.to eq(project_slug) + end + end + end + + describe 'service_account_name' do + let(:platform) { create(:cluster_platform_kubernetes) } + + subject { kubernetes_namespace.service_account_name } + + it 'should set a service account name based on namespace' do + is_expected.to eq("#{kubernetes_namespace.namespace}-service-account") + end + end + end +end diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb index 66198d5ee2b..e13eb554add 100644 --- a/spec/models/clusters/platforms/kubernetes_spec.rb +++ b/spec/models/clusters/platforms/kubernetes_spec.rb @@ -9,6 +9,15 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching it { is_expected.to be_kind_of(ReactiveCaching) } it { is_expected.to respond_to :ca_pem } + it { is_expected.to validate_exclusion_of(:namespace).in_array(%w(gitlab-managed-apps)) } + it { is_expected.to validate_presence_of(:api_url) } + it { is_expected.to validate_presence_of(:token) } + + it { is_expected.to delegate_method(:project).to(:cluster) } + it { is_expected.to delegate_method(:enabled?).to(:cluster) } + it { is_expected.to delegate_method(:managed?).to(:cluster) } + it { is_expected.to delegate_method(:kubernetes_namespace).to(:cluster) } + describe 'before_validation' do context 'when namespace includes upper case' do let(:kubernetes) { create(:cluster_platform_kubernetes, :configured, namespace: namespace) } @@ -90,6 +99,28 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching it { expect(kubernetes.save).to be_falsey } end end + + describe 'when using reserved namespaces' do + subject { build(:cluster_platform_kubernetes, namespace: namespace) } + + context 'when no namespace is manually assigned' do + let(:namespace) { nil } + + it { is_expected.to be_valid } + end + + context 'when no reserved namespace is assigned' do + let(:namespace) { 'my-namespace' } + + it { is_expected.to be_valid } + end + + context 'when reserved namespace is assigned' do + let(:namespace) { 'gitlab-managed-apps' } + + it { is_expected.not_to be_valid } + end + end end describe '#kubeclient' do @@ -117,41 +148,39 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching end describe '#actual_namespace' do - subject { kubernetes.actual_namespace } - - let!(:cluster) { create(:cluster, :project, platform_kubernetes: kubernetes) } + let(:cluster) { create(:cluster, :project) } let(:project) { cluster.project } - let(:kubernetes) { create(:cluster_platform_kubernetes, :configured, namespace: namespace) } - context 'when namespace is present' do + let(:platform) do + create(:cluster_platform_kubernetes, + cluster: cluster, + namespace: namespace) + end + + subject { platform.actual_namespace } + + context 'with a namespace assigned' do let(:namespace) { 'namespace-123' } it { is_expected.to eq(namespace) } end - context 'when namespace is not present' do + context 'with no namespace assigned' do let(:namespace) { nil } - it { is_expected.to eq("#{project.path}-#{project.id}") } - end - end - - describe '#default_namespace' do - subject { kubernetes.send(:default_namespace) } + context 'when kubernetes namespace is present' do + let(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, cluster: cluster) } - let(:kubernetes) { create(:cluster_platform_kubernetes, :configured) } + before do + kubernetes_namespace + end - context 'when cluster belongs to a project' do - let!(:cluster) { create(:cluster, :project, platform_kubernetes: kubernetes) } - let(:project) { cluster.project } - - it { is_expected.to eq("#{project.path}-#{project.id}") } - end - - context 'when cluster belongs to nothing' do - let!(:cluster) { create(:cluster, platform_kubernetes: kubernetes) } + it { is_expected.to eq(kubernetes_namespace.namespace) } + end - it { is_expected.to be_nil } + context 'when kubernetes namespace is not present' do + it { is_expected.to eq("#{project.path}-#{project.id}") } + end end end diff --git a/spec/models/clusters/project_spec.rb b/spec/models/clusters/project_spec.rb index 7d75d6ab345..82ef5a23c18 100644 --- a/spec/models/clusters/project_spec.rb +++ b/spec/models/clusters/project_spec.rb @@ -3,4 +3,6 @@ require 'spec_helper' describe Clusters::Project do it { is_expected.to belong_to(:cluster) } it { is_expected.to belong_to(:project) } + it { is_expected.to have_many(:kubernetes_namespaces) } + it { is_expected.to have_one(:kubernetes_namespace) } end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index b42c7c6e62d..62a38c66d99 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -2965,88 +2965,6 @@ describe Project do end end - describe '#rename_repo' do - before do - # Project#gitlab_shell returns a new instance of Gitlab::Shell on every - # call. This makes testing a bit easier. - allow(project).to receive(:gitlab_shell).and_return(gitlab_shell) - allow(project).to receive(:previous_changes).and_return('path' => ['foo']) - stub_feature_flags(skip_hashed_storage_upgrade: false) - end - - it 'renames a repository' do - stub_container_registry_config(enabled: false) - - expect(gitlab_shell).to receive(:mv_repository) - .ordered - .with(project.repository_storage, "#{project.namespace.full_path}/foo", "#{project.full_path}") - .and_return(true) - - expect(gitlab_shell).to receive(:mv_repository) - .ordered - .with(project.repository_storage, "#{project.namespace.full_path}/foo.wiki", "#{project.full_path}.wiki") - .and_return(true) - - expect_any_instance_of(SystemHooksService) - .to receive(:execute_hooks_for) - .with(project, :rename) - - expect_any_instance_of(Gitlab::UploadsTransfer) - .to receive(:rename_project) - .with('foo', project.path, project.namespace.full_path) - - expect(project).to receive(:expire_caches_before_rename) - - project.rename_repo - end - - context 'container registry with images' do - let(:container_repository) { create(:container_repository) } - - before do - stub_container_registry_config(enabled: true) - stub_container_registry_tags(repository: :any, tags: ['tag']) - project.container_repositories << container_repository - end - - subject { project.rename_repo } - - it { expect { subject }.to raise_error(StandardError) } - end - - context 'gitlab pages' do - before do - expect(project_storage).to receive(:rename_repo) { true } - end - - it 'moves pages folder to new location' do - expect_any_instance_of(Gitlab::PagesTransfer).to receive(:rename_project) - - project.rename_repo - end - end - - context 'attachments' do - before do - expect(project_storage).to receive(:rename_repo) { true } - end - - it 'moves uploads folder to new location' do - expect_any_instance_of(Gitlab::UploadsTransfer).to receive(:rename_project) - - project.rename_repo - end - end - - it 'updates project full path in .git/config' do - allow(project_storage).to receive(:rename_repo).and_return(true) - - project.rename_repo - - expect(rugged_config['gitlab.fullpath']).to eq(project.full_path) - end - end - describe '#pages_path' do it 'returns a path where pages are stored' do expect(project.pages_path).to eq(File.join(Settings.pages.path, project.namespace.full_path, project.path)) @@ -3137,91 +3055,6 @@ describe Project do end end - describe '#rename_repo' do - before do - # Project#gitlab_shell returns a new instance of Gitlab::Shell on every - # call. This makes testing a bit easier. - allow(project).to receive(:gitlab_shell).and_return(gitlab_shell) - allow(project).to receive(:previous_changes).and_return('path' => ['foo']) - stub_feature_flags(skip_hashed_storage_upgrade: false) - end - - context 'migration to hashed storage' do - it 'calls HashedStorageMigrationService with correct options' do - project = create(:project, :repository, :legacy_storage) - allow(project).to receive(:previous_changes).and_return('path' => ['foo']) - - expect_next_instance_of(::Projects::HashedStorageMigrationService) do |service| - expect(service).to receive(:execute).and_return(true) - end - - project.rename_repo - end - end - - it 'renames a repository' do - stub_container_registry_config(enabled: false) - - expect(gitlab_shell).not_to receive(:mv_repository) - - expect_any_instance_of(SystemHooksService) - .to receive(:execute_hooks_for) - .with(project, :rename) - - expect(project).to receive(:expire_caches_before_rename) - - project.rename_repo - end - - context 'container registry with images' do - let(:container_repository) { create(:container_repository) } - - before do - stub_container_registry_config(enabled: true) - stub_container_registry_tags(repository: :any, tags: ['tag']) - project.container_repositories << container_repository - end - - subject { project.rename_repo } - - it { expect { subject }.to raise_error(StandardError) } - end - - context 'gitlab pages' do - it 'moves pages folder to new location' do - expect_any_instance_of(Gitlab::PagesTransfer).to receive(:rename_project) - - project.rename_repo - end - end - - context 'attachments' do - it 'keeps uploads folder location unchanged' do - expect_any_instance_of(Gitlab::UploadsTransfer).not_to receive(:rename_project) - - project.rename_repo - end - - context 'when not rolled out' do - let(:project) { create(:project, :repository, storage_version: 1, skip_disk_validation: true) } - - it 'moves pages folder to hashed storage' do - expect_next_instance_of(Projects::HashedStorage::MigrateAttachmentsService) do |service| - expect(service).to receive(:execute) - end - - project.rename_repo - end - end - end - - it 'updates project full path in .git/config' do - project.rename_repo - - expect(rugged_config['gitlab.fullpath']).to eq(project.full_path) - end - end - describe '#pages_path' do it 'returns a path where pages are stored' do expect(project.pages_path).to eq(File.join(Settings.pages.path, project.namespace.full_path, project.path)) diff --git a/spec/services/clusters/create_service_spec.rb b/spec/services/clusters/create_service_spec.rb index 5373ad7f4b2..3959295c13e 100644 --- a/spec/services/clusters/create_service_spec.rb +++ b/spec/services/clusters/create_service_spec.rb @@ -2,104 +2,33 @@ require 'spec_helper' describe Clusters::CreateService do let(:access_token) { 'xxx' } + let(:project) { create(:project) } let(:user) { create(:user) } - let(:service) { described_class.new(user, params) } - describe '#execute' do - before do - allow(ClusterProvisionWorker).to receive(:perform_async) - end + subject { described_class.new(user, params).execute(project: project, access_token: access_token) } - shared_context 'valid cluster create params' do - let(:params) do - { - name: 'test-cluster', - provider_type: :gcp, - provider_gcp_attributes: { - gcp_project_id: 'gcp-project', - zone: 'us-central1-a', - num_nodes: 1, - machine_type: 'machine_type-a', - legacy_abac: 'true' - } - } - end - end + context 'when provider is gcp' do + context 'when project has no clusters' do + context 'when correct params' do + include_context 'valid cluster create params' - shared_context 'invalid cluster create params' do - let(:params) do - { - name: 'test-cluster', - provider_type: :gcp, - provider_gcp_attributes: { - gcp_project_id: '!!!!!!!', - zone: 'us-central1-a', - num_nodes: 1, - machine_type: 'machine_type-a' - } - } + include_examples 'create cluster service success' end - end - shared_examples 'create cluster service success' do - it 'creates a cluster object and performs a worker' do - expect(ClusterProvisionWorker).to receive(:perform_async) + context 'when invalid params' do + include_context 'invalid cluster create params' - expect { subject } - .to change { Clusters::Cluster.count }.by(1) - .and change { Clusters::Providers::Gcp.count }.by(1) - - expect(subject.name).to eq('test-cluster') - expect(subject.user).to eq(user) - expect(subject.provider.gcp_project_id).to eq('gcp-project') - expect(subject.provider.zone).to eq('us-central1-a') - expect(subject.provider.num_nodes).to eq(1) - expect(subject.provider.machine_type).to eq('machine_type-a') - expect(subject.provider.access_token).to eq(access_token) - expect(subject.provider).to be_legacy_abac - expect(subject.platform).to be_nil - end - end - - shared_examples 'create cluster service error' do - it 'returns an error' do - expect(ClusterProvisionWorker).not_to receive(:perform_async) - expect { subject }.to change { Clusters::Cluster.count }.by(0) - expect(subject.errors[:"provider_gcp.gcp_project_id"]).to be_present + include_examples 'create cluster service error' end end - context 'create cluster for project' do - let(:project) { create(:project) } + context 'when project has a cluster' do + include_context 'valid cluster create params' + let!(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, projects: [project]) } - subject { service.execute(project: project, access_token: access_token) } - - context 'when project has no clusters' do - context 'when correct params' do - include_context 'valid cluster create params' - - include_examples 'create cluster service success' - - it 'associates project to the cluster' do - expect(subject.project).to eq(project) - end - end - - context 'when invalid params' do - include_context 'invalid cluster create params' - - include_examples 'create cluster service error' - end - end - - context 'when project has a cluster' do - include_context 'valid cluster create params' - let!(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, projects: [project]) } - - it 'does not create a cluster' do - expect(ClusterProvisionWorker).not_to receive(:perform_async) - expect { subject }.to raise_error(ArgumentError).and change { Clusters::Cluster.count }.by(0) - end + it 'does not create a cluster' do + expect(ClusterProvisionWorker).not_to receive(:perform_async) + expect { subject }.to raise_error(ArgumentError).and change { Clusters::Cluster.count }.by(0) end end end diff --git a/spec/services/projects/after_rename_service_spec.rb b/spec/services/projects/after_rename_service_spec.rb new file mode 100644 index 00000000000..b4718a07204 --- /dev/null +++ b/spec/services/projects/after_rename_service_spec.rb @@ -0,0 +1,198 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Projects::AfterRenameService do + let(:rugged_config) { rugged_repo(project.repository).config } + + describe '#execute' do + context 'using legacy storage' do + let(:project) { create(:project, :repository, :legacy_storage) } + let(:gitlab_shell) { Gitlab::Shell.new } + let(:project_storage) { project.send(:storage) } + + before do + # Project#gitlab_shell returns a new instance of Gitlab::Shell on every + # call. This makes testing a bit easier. + allow(project).to receive(:gitlab_shell).and_return(gitlab_shell) + + allow(project) + .to receive(:previous_changes) + .and_return('path' => ['foo']) + + allow(project) + .to receive(:path_was) + .and_return('foo') + + stub_feature_flags(skip_hashed_storage_upgrade: false) + end + + it 'renames a repository' do + stub_container_registry_config(enabled: false) + + expect(gitlab_shell).to receive(:mv_repository) + .ordered + .with(project.repository_storage, "#{project.namespace.full_path}/foo", "#{project.full_path}") + .and_return(true) + + expect(gitlab_shell).to receive(:mv_repository) + .ordered + .with(project.repository_storage, "#{project.namespace.full_path}/foo.wiki", "#{project.full_path}.wiki") + .and_return(true) + + expect_any_instance_of(SystemHooksService) + .to receive(:execute_hooks_for) + .with(project, :rename) + + expect_any_instance_of(Gitlab::UploadsTransfer) + .to receive(:rename_project) + .with('foo', project.path, project.namespace.full_path) + + expect(project).to receive(:expire_caches_before_rename) + + described_class.new(project).execute + end + + context 'container registry with images' do + let(:container_repository) { create(:container_repository) } + + before do + stub_container_registry_config(enabled: true) + stub_container_registry_tags(repository: :any, tags: ['tag']) + project.container_repositories << container_repository + end + + it 'raises a RenameFailedError' do + expect { described_class.new(project).execute } + .to raise_error(described_class::RenameFailedError) + end + end + + context 'gitlab pages' do + before do + expect(project_storage).to receive(:rename_repo) { true } + end + + it 'moves pages folder to new location' do + expect_any_instance_of(Gitlab::PagesTransfer).to receive(:rename_project) + + described_class.new(project).execute + end + end + + context 'attachments' do + before do + expect(project_storage).to receive(:rename_repo) { true } + end + + it 'moves uploads folder to new location' do + expect_any_instance_of(Gitlab::UploadsTransfer).to receive(:rename_project) + + described_class.new(project).execute + end + end + + it 'updates project full path in .git/config' do + allow(project_storage).to receive(:rename_repo).and_return(true) + + described_class.new(project).execute + + expect(rugged_config['gitlab.fullpath']).to eq(project.full_path) + end + end + + context 'using hashed storage' do + let(:project) { create(:project, :repository, skip_disk_validation: true) } + let(:gitlab_shell) { Gitlab::Shell.new } + let(:hash) { Digest::SHA2.hexdigest(project.id.to_s) } + let(:hashed_prefix) { File.join('@hashed', hash[0..1], hash[2..3]) } + let(:hashed_path) { File.join(hashed_prefix, hash) } + + before do + # Project#gitlab_shell returns a new instance of Gitlab::Shell on every + # call. This makes testing a bit easier. + allow(project).to receive(:gitlab_shell).and_return(gitlab_shell) + allow(project).to receive(:previous_changes).and_return('path' => ['foo']) + + stub_feature_flags(skip_hashed_storage_upgrade: false) + stub_application_setting(hashed_storage_enabled: true) + end + + context 'migration to hashed storage' do + it 'calls HashedStorageMigrationService with correct options' do + project = create(:project, :repository, :legacy_storage) + allow(project).to receive(:previous_changes).and_return('path' => ['foo']) + + expect_next_instance_of(::Projects::HashedStorageMigrationService) do |service| + expect(service).to receive(:execute).and_return(true) + end + + described_class.new(project).execute + end + end + + it 'renames a repository' do + stub_container_registry_config(enabled: false) + + expect(gitlab_shell).not_to receive(:mv_repository) + + expect_any_instance_of(SystemHooksService) + .to receive(:execute_hooks_for) + .with(project, :rename) + + expect(project).to receive(:expire_caches_before_rename) + + described_class.new(project).execute + end + + context 'container registry with images' do + let(:container_repository) { create(:container_repository) } + + before do + stub_container_registry_config(enabled: true) + stub_container_registry_tags(repository: :any, tags: ['tag']) + project.container_repositories << container_repository + end + + it 'raises a RenameFailedError' do + expect { described_class.new(project).execute } + .to raise_error(described_class::RenameFailedError) + end + end + + context 'gitlab pages' do + it 'moves pages folder to new location' do + expect_any_instance_of(Gitlab::PagesTransfer).to receive(:rename_project) + + described_class.new(project).execute + end + end + + context 'attachments' do + it 'keeps uploads folder location unchanged' do + expect_any_instance_of(Gitlab::UploadsTransfer).not_to receive(:rename_project) + + described_class.new(project).execute + end + + context 'when not rolled out' do + let(:project) { create(:project, :repository, storage_version: 1, skip_disk_validation: true) } + + it 'moves pages folder to hashed storage' do + expect_next_instance_of(Projects::HashedStorage::MigrateAttachmentsService) do |service| + expect(service).to receive(:execute) + end + + described_class.new(project).execute + end + end + end + + it 'updates project full path in .git/config' do + described_class.new(project).execute + + expect(rugged_config['gitlab.fullpath']).to eq(project.full_path) + end + end + end +end diff --git a/spec/support/helpers/ci_artifact_metadata_generator.rb b/spec/support/helpers/ci_artifact_metadata_generator.rb new file mode 100644 index 00000000000..ef638d59d2d --- /dev/null +++ b/spec/support/helpers/ci_artifact_metadata_generator.rb @@ -0,0 +1,48 @@ +# frozen_sting_literal: true + +# This generates fake CI metadata .gz for testing +# Based off https://gitlab.com/gitlab-org/gitlab-workhorse/blob/master/internal/zipartifacts/metadata.go +class CiArtifactMetadataGenerator + attr_accessor :entries, :output + + ARTIFACT_METADATA = "GitLab Build Artifacts Metadata 0.0.2\n".freeze + + def initialize(stream) + @entries = {} + @output = Zlib::GzipWriter.new(stream) + end + + def add_entry(filename) + @entries[filename] = { CRC: rand(0xfffffff), Comment: FFaker::Lorem.sentence(10) } + end + + def write + write_version + write_errors + write_entries + output.close + end + + private + + def write_version + write_string(ARTIFACT_METADATA) + end + + def write_errors + write_string('{}') + end + + def write_entries + entries.each do |filename, metadata| + write_string(filename) + write_string(metadata.to_json + "\n") + end + end + + def write_string(data) + bytes = [data.length].pack('L>') + output.write(bytes) + output.write(data) + end +end diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb index c077ca9f15b..a03d9c4045f 100644 --- a/spec/support/helpers/kubernetes_helpers.rb +++ b/spec/support/helpers/kubernetes_helpers.rb @@ -33,10 +33,11 @@ module KubernetesHelpers WebMock.stub_request(:get, deployments_url).to_return(response || kube_deployments_response) end - def stub_kubeclient_get_secret(api_url, namespace: 'default', **options) + def stub_kubeclient_get_secret(api_url, **options) options[:metadata_name] ||= "default-token-1" + options[:namespace] ||= "default" - WebMock.stub_request(:get, api_url + "/api/v1/namespaces/#{namespace}/secrets/#{options[:metadata_name]}") + WebMock.stub_request(:get, api_url + "/api/v1/namespaces/#{options[:namespace]}/secrets/#{options[:metadata_name]}") .to_return(kube_response(kube_v1_secret_body(options))) end @@ -65,6 +66,21 @@ module KubernetesHelpers .to_return(kube_response({})) end + def stub_kubeclient_create_role_binding(api_url, namespace: 'default') + WebMock.stub_request(:post, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings") + .to_return(kube_response({})) + end + + def stub_kubeclient_create_namespace(api_url) + WebMock.stub_request(:post, api_url + "/api/v1/namespaces") + .to_return(kube_response({})) + end + + def stub_kubeclient_get_namespace(api_url, namespace: 'default') + WebMock.stub_request(:get, api_url + "/api/v1/namespaces/#{namespace}") + .to_return(kube_response({})) + end + def kube_v1_secret_body(**options) { "kind" => "SecretList", @@ -87,7 +103,8 @@ module KubernetesHelpers { "name" => "deployments", "namespaced" => true, "kind" => "Deployment" }, { "name" => "secrets", "namespaced" => true, "kind" => "Secret" }, { "name" => "serviceaccounts", "namespaced" => true, "kind" => "ServiceAccount" }, - { "name" => "services", "namespaced" => true, "kind" => "Service" } + { "name" => "services", "namespaced" => true, "kind" => "Service" }, + { "name" => "namespaces", "namespaced" => true, "kind" => "Namespace" } ] } end diff --git a/spec/support/services/clusters/create_service_shared.rb b/spec/support/services/clusters/create_service_shared.rb new file mode 100644 index 00000000000..b0bf942aa09 --- /dev/null +++ b/spec/support/services/clusters/create_service_shared.rb @@ -0,0 +1,59 @@ +shared_context 'valid cluster create params' do + let(:params) do + { + name: 'test-cluster', + provider_type: :gcp, + provider_gcp_attributes: { + gcp_project_id: 'gcp-project', + zone: 'us-central1-a', + num_nodes: 1, + machine_type: 'machine_type-a', + legacy_abac: 'true' + } + } + end +end + +shared_context 'invalid cluster create params' do + let(:params) do + { + name: 'test-cluster', + provider_type: :gcp, + provider_gcp_attributes: { + gcp_project_id: '!!!!!!!', + zone: 'us-central1-a', + num_nodes: 1, + machine_type: 'machine_type-a' + } + } + end +end + +shared_examples 'create cluster service success' do + it 'creates a cluster object and performs a worker' do + expect(ClusterProvisionWorker).to receive(:perform_async) + + expect { subject } + .to change { Clusters::Cluster.count }.by(1) + .and change { Clusters::Providers::Gcp.count }.by(1) + + expect(subject.name).to eq('test-cluster') + expect(subject.user).to eq(user) + expect(subject.project).to eq(project) + expect(subject.provider.gcp_project_id).to eq('gcp-project') + expect(subject.provider.zone).to eq('us-central1-a') + expect(subject.provider.num_nodes).to eq(1) + expect(subject.provider.machine_type).to eq('machine_type-a') + expect(subject.provider.access_token).to eq(access_token) + expect(subject.provider).to be_legacy_abac + expect(subject.platform).to be_nil + end +end + +shared_examples 'create cluster service error' do + it 'returns an error' do + expect(ClusterProvisionWorker).not_to receive(:perform_async) + expect { subject }.to change { Clusters::Cluster.count }.by(0) + expect(subject.errors[:"provider_gcp.gcp_project_id"]).to be_present + end +end diff --git a/spec/views/shared/runners/show.html.haml_spec.rb b/spec/views/shared/runners/show.html.haml_spec.rb new file mode 100644 index 00000000000..5e92928b143 --- /dev/null +++ b/spec/views/shared/runners/show.html.haml_spec.rb @@ -0,0 +1,155 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'shared/runners/show.html.haml' do + include PageLayoutHelper + + let(:runner) do + create(:ci_runner, name: 'test runner', + version: '11.4.0', + ip_address: '127.1.2.3', + revision: 'abcd1234', + architecture: 'amd64' ) + end + + before do + assign(:runner, runner) + end + + subject do + render + rendered + end + + describe 'Page title' do + before do + expect_any_instance_of(PageLayoutHelper).to receive(:page_title).with("#{runner.description} ##{runner.id}", 'Runners') + end + + it 'sets proper page title' do + render + end + end + + describe 'Runner id and type' do + context 'when runner is of type instance' do + it { is_expected.to have_content("Runner ##{runner.id} Shared") } + end + + context 'when runner is of type group' do + let(:runner) { create(:ci_runner, :group) } + + it { is_expected.to have_content("Runner ##{runner.id} Group") } + end + + context 'when runner is of type project' do + let(:runner) { create(:ci_runner, :project) } + + it { is_expected.to have_content("Runner ##{runner.id} Specific") } + end + end + + describe 'Active value' do + context 'when runner is active' do + it { is_expected.to have_content('Active Yes') } + end + + context 'when runner is inactive' do + let(:runner) { create(:ci_runner, :inactive) } + + it { is_expected.to have_content('Active No') } + end + end + + describe 'Protected value' do + context 'when runner is not protected' do + it { is_expected.to have_content('Protected No') } + end + + context 'when runner is protected' do + let(:runner) { create(:ci_runner, :ref_protected) } + + it { is_expected.to have_content('Protected Yes') } + end + end + + describe 'Can run untagged jobs value' do + context 'when runner run untagged job is set' do + it { is_expected.to have_content('Can run untagged jobs Yes') } + end + + context 'when runner run untagged job is unset' do + let(:runner) { create(:ci_runner, :tagged_only) } + + it { is_expected.to have_content('Can run untagged jobs No') } + end + end + + describe 'Locked to this project value' do + context 'when runner locked is not set' do + it { is_expected.to have_content('Locked to this project No') } + + context 'when runner is of type group' do + let(:runner) { create(:ci_runner, :group) } + + it { is_expected.not_to have_content('Locked to this project') } + end + end + + context 'when runner locked is set' do + let(:runner) { create(:ci_runner, :locked) } + + it { is_expected.to have_content('Locked to this project Yes') } + + context 'when runner is of type group' do + let(:runner) { create(:ci_runner, :group, :locked) } + + it { is_expected.not_to have_content('Locked to this project') } + end + end + end + + describe 'Tags value' do + context 'when runner does not have tags' do + it { is_expected.to have_content('Tags') } + it { is_expected.not_to have_selector('span.badge.badge-primary')} + end + + context 'when runner have tags' do + let(:runner) { create(:ci_runner, tag_list: %w(tag2 tag3 tag1)) } + + it { is_expected.to have_content('Tags tag1 tag2 tag3') } + it { is_expected.to have_selector('span.badge.badge-primary')} + end + end + + describe 'Metadata values' do + it { is_expected.to have_content("Name #{runner.name}") } + it { is_expected.to have_content("Version #{runner.version}") } + it { is_expected.to have_content("IP Address #{runner.ip_address}") } + it { is_expected.to have_content("Revision #{runner.revision}") } + it { is_expected.to have_content("Platform #{runner.platform}") } + it { is_expected.to have_content("Architecture #{runner.architecture}") } + it { is_expected.to have_content("Description #{runner.description}") } + end + + describe 'Maximum job timeout value' do + let(:runner) { create(:ci_runner, maximum_timeout: 5400) } + + it { is_expected.to have_content('Maximum job timeout 1h 30m') } + end + + describe 'Last contact value' do + context 'when runner have not contacted yet' do + it { is_expected.to have_content('Last contact Never') } + end + + context 'when runner have already contacted' do + let(:runner) { create(:ci_runner, contacted_at: DateTime.now - 6.days) } + let(:expected_contacted_at) { I18n.localize(runner.contacted_at, format: "%b %d, %Y") } + + it { is_expected.to have_content("Last contact #{expected_contacted_at}") } + end + end +end diff --git a/yarn.lock b/yarn.lock index 292c7128d18..544bd4a05bd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1325,18 +1325,6 @@ binaryextensions@2: resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.1.1.tgz#3209a51ca4a4ad541a3b8d3d6a6d5b83a2485935" integrity sha512-XBaoWE9RW8pPdPQNibZsW2zh8TW6gcarXp1FZPwT8Uop8ScSNldJEWf2k9l3HeTqdrEwsOsFcq74RiJECW34yA== -blackst0ne-mermaid@^7.1.0-fixed: - version "7.1.0-fixed" - resolved "https://registry.yarnpkg.com/blackst0ne-mermaid/-/blackst0ne-mermaid-7.1.0-fixed.tgz#3707b3a113d78610e3068e18a588f46b4688de49" - integrity sha1-NwezoRPXhhDjBo4YpYj0a0aI3kk= - dependencies: - d3 "3.5.17" - dagre-d3-renderer "^0.4.24" - dagre-layout "^0.8.0" - he "^1.1.1" - lodash "^4.17.4" - moment "^2.18.1" - blob@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.4.tgz#bcf13052ca54463f30f9fc7e95b9a47630a94921" @@ -2304,6 +2292,11 @@ d3-format@1, d3-format@1.2.1: resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.2.1.tgz#4e19ecdb081a341dafaf5f555ee956bcfdbf167f" integrity sha512-U4zRVLDXW61bmqoo+OJ/V687e1T5nVd3TAKAJKgtpZ/P1JsMgyod0y9br+mlQOryTAACdiXI3wCjuERHFNp91w== +d3-format@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.2.2.tgz#1a39c479c8a57fe5051b2e67a3bee27061a74e7a" + integrity sha512-zH9CfF/3C8zUI47nsiKfD0+AGDEuM8LwBIP7pBVpyR4l/sKkZqITmMtxRp04rwBrlshIZ17XeFAaovN3++wzkw== + d3-geo@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-1.9.1.tgz#157e3b0f917379d0f73bebfff3be537f49fa7356" @@ -2376,6 +2369,11 @@ d3-selection@1, d3-selection@1.2.0, d3-selection@^1.1.0, d3-selection@^1.2.0: resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.2.0.tgz#1b8ec1c7cedadfb691f2ba20a4a3cfbeb71bbc88" integrity sha512-xW2Pfcdzh1gOaoI+LGpPsLR2VpBQxuFoxvrvguK8ZmrJbPIVvfNG6pU6GNfK41D6Qz15sj61sbW/AFYuukwaLQ== +d3-selection@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.3.0.tgz#d53772382d3dc4f7507bfb28bcd2d6aed2a0ad6d" + integrity sha512-qgpUOg9tl5CirdqESUAu0t9MU/t3O9klYfGfyKsXEmhyxyzLpzpeh08gaxBUTQw1uXIOkr/30Ut2YRjSSxlmHA== + d3-shape@1.2.0, d3-shape@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.2.0.tgz#45d01538f064bafd05ea3d6d2cb748fd8c41f777" @@ -2428,11 +2426,6 @@ d3-zoom@1.7.1: d3-selection "1" d3-transition "1" -d3@3.5.17: - version "3.5.17" - resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.17.tgz#bc46748004378b21a360c9fc7cf5231790762fb8" - integrity sha1-vEZ0gAQ3iyGjYMn8fPUjF5B2L7g= - d3@4.12.2: version "4.12.2" resolved "https://registry.yarnpkg.com/d3/-/d3-4.12.2.tgz#12f775564c6a9de229f63db03446e2cb7bb56c8f" @@ -2469,23 +2462,57 @@ d3@4.12.2: d3-voronoi "1.1.2" d3-zoom "1.7.1" -dagre-d3-renderer@^0.4.24: - version "0.4.24" - resolved "https://registry.yarnpkg.com/dagre-d3-renderer/-/dagre-d3-renderer-0.4.24.tgz#b36ce2fe4ea20de43e7698627c6ede2a9f15ec45" - integrity sha512-QCrYq80NTyKph+m/+kNeEq2exw5HPo/x7XprJem3wDGJbEAJDKXI2pJpqe0R4k6AsjWVd5NMVL0X7feF24Zh6Q== +d3@^4.13.0: + version "4.13.0" + resolved "https://registry.yarnpkg.com/d3/-/d3-4.13.0.tgz#ab236ff8cf0cfc27a81e69bf2fb7518bc9b4f33d" + integrity sha512-l8c4+0SldjVKLaE2WG++EQlqD7mh/dmQjvi2L2lKPadAVC+TbJC4ci7Uk9bRi+To0+ansgsS0iWfPjD7DBy+FQ== dependencies: - d3 "3.5.17" - dagre-layout "^0.8.0" - graphlib "^2.1.1" - lodash "^4.17.4" + d3-array "1.2.1" + d3-axis "1.0.8" + d3-brush "1.0.4" + d3-chord "1.0.4" + d3-collection "1.0.4" + d3-color "1.0.3" + d3-dispatch "1.0.3" + d3-drag "1.2.1" + d3-dsv "1.0.8" + d3-ease "1.0.3" + d3-force "1.1.0" + d3-format "1.2.2" + d3-geo "1.9.1" + d3-hierarchy "1.1.5" + d3-interpolate "1.1.6" + d3-path "1.0.5" + d3-polygon "1.0.3" + d3-quadtree "1.0.3" + d3-queue "3.0.7" + d3-random "1.1.0" + d3-request "1.0.6" + d3-scale "1.0.7" + d3-selection "1.3.0" + d3-shape "1.2.0" + d3-time "1.0.8" + d3-time-format "2.1.1" + d3-timer "1.0.7" + d3-transition "1.1.1" + d3-voronoi "1.1.2" + d3-zoom "1.7.1" -dagre-layout@^0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/dagre-layout/-/dagre-layout-0.8.0.tgz#7147b6afb655602f855158dfea171db9aa98d4ff" - integrity sha512-vK4WiR6h3whkoW9aM/FCjZTTx10V2YnLOLEj2+uvOQmiEjGmUvkme+Qrjj/7Tq0+AI54yFHT/tbbqM9AadsK4A== +dagre-d3-renderer@^0.5.8: + version "0.5.8" + resolved "https://registry.yarnpkg.com/dagre-d3-renderer/-/dagre-d3-renderer-0.5.8.tgz#aa071bb71d3c4d67426925906f3f6ddead49c1a3" + integrity sha512-XH2a86isUHRxzIYbjQVEuZtJnWEufb64H5DuXIUmn8esuB40jgLEbUUclulWOW62/ZoXlj2ZDyL8SJ+YRxs+jQ== dependencies: - graphlib "^2.1.1" - lodash "^4.17.4" + dagre-layout "^0.8.8" + lodash "^4.17.5" + +dagre-layout@^0.8.8: + version "0.8.8" + resolved "https://registry.yarnpkg.com/dagre-layout/-/dagre-layout-0.8.8.tgz#9b6792f24229f402441c14162c1049e3f261f6d9" + integrity sha512-ZNV15T9za7X+fV8Z07IZquUKugCxm5owoiPPxfEx6OJRD331nkiIaF3vSt0JEY5FkrY0KfRQxcpQ3SpXB7pLPQ== + dependencies: + graphlibrary "^2.2.0" + lodash "^4.17.5" date-format@^1.2.0: version "1.2.0" @@ -3004,6 +3031,11 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= +escaper@^2.5.3: + version "2.5.3" + resolved "https://registry.yarnpkg.com/escaper/-/escaper-2.5.3.tgz#8b8fe90ba364054151ab7eff18b4ce43b1e13ab5" + integrity sha512-QGb9sFxBVpbzMggrKTX0ry1oiI4CSDAl9vIL702hzl1jGW8VZs7qfqTRX7WDOjoNDoEVGcEtu1ZOQgReSfT2kQ== + escodegen@1.8.x: version "1.8.1" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.8.1.tgz#5a5b53af4693110bebb0867aa3430dd3b70a1018" @@ -3893,12 +3925,12 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" integrity sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg= -graphlib@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/graphlib/-/graphlib-2.1.1.tgz#42352c52ba2f4d035cb566eb91f7395f76ebc951" - integrity sha1-QjUsUrovTQNctWbrkfc5X3bryVE= +graphlibrary@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/graphlibrary/-/graphlibrary-2.2.0.tgz#017a14899775228dec4497a39babfdd6bf56eac6" + integrity sha512-XTcvT55L8u4MBZrM37zXoUxsgxs/7sow7YSygd9CIwfWTVO8RVu7AYXhhCiTuFEf+APKgx6Jk4SuQbYR0vYKmQ== dependencies: - lodash "^4.11.1" + lodash "^4.17.5" gzip-size@^5.0.0: version "5.0.0" @@ -4181,11 +4213,6 @@ ignore-walk@^3.0.1: dependencies: minimatch "^3.0.4" -ignore@^3.3.7: - version "3.3.8" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.8.tgz#3f8e9c35d38708a3a7e0e9abb6c73e7ee7707b2b" - integrity sha512-pUh+xUQQhQzevjRHHFqqcTy0/dP/kS9I8HSrUydhihjuD09W6ldVWFtIrwhXdUJHis3i2rZNqEHpZH/cbinFbg== - ignore@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" @@ -4567,6 +4594,11 @@ is-regex@^1.0.4: dependencies: has "^1.0.1" +is-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" + integrity sha1-/S2INUXEa6xaYz57mgnof6LLUGk= + is-resolvable@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" @@ -5134,7 +5166,7 @@ lodash.upperfirst@4.3.1: resolved "https://registry.yarnpkg.com/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz#1365edf431480481ef0d1c68957a5ed99d49f7ce" integrity sha1-E2Xt9DFIBIHvDRxolXpe2Z1J984= -lodash@^4.11.1, lodash@^4.17.10, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0, lodash@^4.5.0: +lodash@^4.17.10, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0, lodash@^4.5.0: version "4.17.11" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== @@ -5293,6 +5325,20 @@ merge-source-map@^1.1.0: dependencies: source-map "^0.6.1" +mermaid@^8.0.0-rc.8: + version "8.0.0-rc.8" + resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-8.0.0-rc.8.tgz#74ed54d0d46e9ee71c4db2730b2d83d516a21e72" + integrity sha512-GbF9jHWfqE7YGx9vQySmBxy2Ahlclxmpk4tJ9ntNyafENl96s96ggUK/NQS5ydYoFab6MavTm4YMTIPKqWVvPQ== + dependencies: + d3 "^4.13.0" + dagre-d3-renderer "^0.5.8" + dagre-layout "^0.8.8" + graphlibrary "^2.2.0" + he "^1.1.1" + lodash "^4.17.5" + moment "^2.21.0" + scope-css "^1.0.5" + methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" @@ -5451,11 +5497,16 @@ mkdirp@0.5.x, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0: dependencies: minimist "0.0.8" -moment@2.x, moment@^2.18.1: +moment@2.x: version "2.19.2" resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.2.tgz#8a7f774c95a64550b4c7ebd496683908f9419dbe" integrity sha512-Rf6jiHPEfxp9+dlzxPTmRHbvoFXsh2L/U8hOupUMpnuecHQmI6cF6lUbJl3QqKPko1u6ujO+FxtcajLVfLpAtA== +moment@^2.21.0: + version "2.22.2" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66" + integrity sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y= + monaco-editor-webpack-plugin@^1.5.4: version "1.5.4" resolved "https://registry.yarnpkg.com/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-1.5.4.tgz#6781a130e3e1379bb8f4cd190132f4af6dcd2c16" @@ -6893,6 +6944,15 @@ schema-utils@^1.0.0: ajv-errors "^1.0.0" ajv-keywords "^3.1.0" +scope-css@^1.0.5: + version "1.2.1" + resolved "https://registry.yarnpkg.com/scope-css/-/scope-css-1.2.1.tgz#c35768bc900cad030a3e0d663a818c0f6a57f40e" + integrity sha512-UjLRmyEYaDNiOS673xlVkZFlVCtckJR/dKgr434VMm7Lb+AOOqXKdAcY7PpGlJYErjXXJzKN7HWo4uRPiZZG0Q== + dependencies: + escaper "^2.5.3" + slugify "^1.3.1" + strip-css-comments "^3.0.0" + select-hose@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" @@ -7066,6 +7126,11 @@ slice-ansi@1.0.0: dependencies: is-fullwidth-code-point "^2.0.0" +slugify@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.3.1.tgz#f572127e8535329fbc6c1edb74ab856b61ad7de2" + integrity sha512-6BwyhjF5tG5P8s+0DPNyJmBSBePG6iMyhjvIW5zGdA3tFik9PtK+yNkZgTeiroCRGZYgkHftFA62tGVK1EI9Kw== + smooshpack@^0.0.48: version "0.0.48" resolved "https://registry.yarnpkg.com/smooshpack/-/smooshpack-0.0.48.tgz#6fbeaaf59226a1fe500f56aa17185eed377d2823" @@ -7439,6 +7504,13 @@ strip-bom@^3.0.0: resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= +strip-css-comments@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-css-comments/-/strip-css-comments-3.0.0.tgz#7a5625eff8a2b226cf8947a11254da96e13dae89" + integrity sha1-elYl7/iisibPiUehElTaluE9rok= + dependencies: + is-regexp "^1.0.0" + strip-eof@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" |