From 1ea2a85cbe61222765dc3ef50e8c0e4c1b2f5bfa Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Mon, 25 Sep 2017 23:38:34 +0200 Subject: Make Prometheus metrics endpoint return empty response when metrics are disabled --- app/controllers/metrics_controller.rb | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) (limited to 'app') diff --git a/app/controllers/metrics_controller.rb b/app/controllers/metrics_controller.rb index 37587a52eaf..d81ad135198 100644 --- a/app/controllers/metrics_controller.rb +++ b/app/controllers/metrics_controller.rb @@ -3,10 +3,16 @@ class MetricsController < ActionController::Base protect_from_forgery with: :exception - before_action :validate_prometheus_metrics - def index - render text: metrics_service.metrics_text, content_type: 'text/plain; version=0.0.4' + response = if Gitlab::Metrics.prometheus_metrics_enabled? + metrics_service.metrics_text + else + help_page = help_page_url('administration/monitoring/prometheus/gitlab_metrics', + anchor: 'gitlab-prometheus-metrics' + ) + "# Metrics are disabled, see: #{help_page}\n" + end + render text: response, content_type: 'text/plain; version=0.0.4' end private @@ -14,8 +20,4 @@ class MetricsController < ActionController::Base def metrics_service @metrics_service ||= MetricsService.new end - - def validate_prometheus_metrics - render_404 unless Gitlab::Metrics.prometheus_metrics_enabled? - end end -- cgit v1.2.1 From 6d3eea7b46d4b363b39a59e1fa17264de33d14d1 Mon Sep 17 00:00:00 2001 From: Brett Walker Date: Wed, 11 Oct 2017 14:15:28 +0200 Subject: if the branch/tag count is over a certain limit, don't execute the long running git query --- app/controllers/projects/commit_controller.rb | 4 ++-- app/models/repository.rb | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) (limited to 'app') diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index a62f05db7db..893763862ba 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -56,8 +56,8 @@ class Projects::CommitController < Projects::ApplicationController end def branches - @branches = @project.repository.branch_names_contains(commit.id) - @tags = @project.repository.tag_names_contains(commit.id) + @branches = @project.repository.branch_names_contains(commit.id, 1000) + @tags = @project.repository.tag_names_contains(commit.id, 1000) render layout: false end diff --git a/app/models/repository.rb b/app/models/repository.rb index d725c65081d..3a083b76202 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -715,12 +715,12 @@ class Repository end end - def branch_names_contains(sha) - refs_contains_sha('branch', sha) + def branch_names_contains(sha, limit = nil) + limit && branch_count > limit ? [] : refs_contains_sha('branch', sha) end - def tag_names_contains(sha) - refs_contains_sha('tag', sha) + def tag_names_contains(sha, limit = nil) + limit && tag_count > limit ? [] : refs_contains_sha('tag', sha) end def local_branches -- cgit v1.2.1 From 528f9cde0588b0a6e70b1fa971a99eca439d0aa6 Mon Sep 17 00:00:00 2001 From: Brett Walker Date: Thu, 12 Oct 2017 15:31:43 +0200 Subject: moved throttling into the controller. if we hit the throttling threshhold, a message is shown indicating we didn't perform the search --- app/controllers/projects/commit_controller.rb | 7 +++++-- app/helpers/commits_helper.rb | 26 +++++++++++++++++-------- app/models/repository.rb | 8 ++++---- app/views/projects/commit/branches.html.haml | 28 +++++++++++++++------------ 4 files changed, 43 insertions(+), 26 deletions(-) (limited to 'app') diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 893763862ba..5b8a8159123 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -56,8 +56,11 @@ class Projects::CommitController < Projects::ApplicationController end def branches - @branches = @project.repository.branch_names_contains(commit.id, 1000) - @tags = @project.repository.tag_names_contains(commit.id, 1000) + # branch_names_contains/tag_names_contains can take a long time when there are thousands of + # branches/tags - each `git branch --contains xxx` request can consume a cpu core. + # so only do the query when there are a manageable number of branches/tags + @branches = @project.repository.branch_count > 1000 ? [:limit_exceeded] : @project.repository.branch_names_contains(commit.id) + @tags = @project.repository.tag_count > 1000 ? [:limit_exceeded] : @project.repository.tag_names_contains(commit.id) render layout: false end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index ef22cafc2e2..8aaf3318f90 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -60,23 +60,33 @@ module CommitsHelper branches.include?(project.default_branch) ? branches.delete(project.default_branch) : branches.pop end + # returns a link formatted as a commit branch link + def commit_branch_link(url, text) + link_to(url, class: 'label label-gray ref-name') do + icon('code-fork') + " #{text}" + end + end + # Returns the sorted alphabetically links to branches, separated by a comma def commit_branches_links(project, branches) branches.sort.map do |branch| - link_to(project_ref_path(project, branch), class: "label label-gray ref-name") do - icon('code-fork') + " #{branch}" - end - end.join(" ").html_safe + commit_branch_link(project_ref_path(project, branch), branch) + end.join(' ').html_safe + end + + # returns a link formatted as a commit tag link + def commit_tag_link(url, text) + link_to(url, class: 'label label-gray ref-name') do + icon('tag') + " #{text}" + end end # Returns the sorted links to tags, separated by a comma def commit_tags_links(project, tags) sorted = VersionSorter.rsort(tags) sorted.map do |tag| - link_to(project_ref_path(project, tag), class: "label label-gray ref-name") do - icon('tag') + " #{tag}" - end - end.join(" ").html_safe + commit_tag_link(project_ref_path(project, tag), tag) + end.join(' ').html_safe end def link_to_browse_code(project, commit) diff --git a/app/models/repository.rb b/app/models/repository.rb index 3a083b76202..d725c65081d 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -715,12 +715,12 @@ class Repository end end - def branch_names_contains(sha, limit = nil) - limit && branch_count > limit ? [] : refs_contains_sha('branch', sha) + def branch_names_contains(sha) + refs_contains_sha('branch', sha) end - def tag_names_contains(sha, limit = nil) - limit && tag_count > limit ? [] : refs_contains_sha('tag', sha) + def tag_names_contains(sha) + refs_contains_sha('tag', sha) end def local_branches diff --git a/app/views/projects/commit/branches.html.haml b/app/views/projects/commit/branches.html.haml index 911c9ddce06..398927fb50d 100644 --- a/app/views/projects/commit/branches.html.haml +++ b/app/views/projects/commit/branches.html.haml @@ -1,15 +1,19 @@ -- if @branches.any? || @tags.any? +- if @branches.any? - branch = commit_default_branch(@project, @branches) - = link_to(project_ref_path(@project, branch), class: "label label-gray ref-name") do - = icon('code-fork') - = branch + - if branch == :limit_exceeded + = commit_branch_link('#', _('Too many branches to search')) + - else + = commit_branch_link(project_ref_path(@project, branch), branch) - -# `commit_default_branch` deletes the default branch from `@branches`, - -# so only render this if we have more branches left - - if @branches.any? || @tags.any? - %span - = link_to "…", "#", class: "js-details-expand label label-gray" +-# `commit_default_branch` deletes the default branch from `@branches`, +-# so only render this if we have more branches or tags left +- if @branches.any? || @tags.any? + %span + = link_to "…", "#", class: "js-details-expand label label-gray" - %span.js-details-content.hide - = commit_branches_links(@project, @branches) if @branches.any? - = commit_tags_links(@project, @tags) if @tags.any? + %span.js-details-content.hide + = commit_branches_links(@project, @branches) if @branches.any? + - if @tags.first == :limit_exceeded + = commit_tag_link('#', _('Too many tags to search')) + - elsif @tags.any? + = commit_tags_links(@project, @tags) -- cgit v1.2.1 From cbdf372eb8e6d38c4f47a1c2f6bff76b4b2c659f Mon Sep 17 00:00:00 2001 From: Brett Walker Date: Fri, 13 Oct 2017 19:11:11 +0200 Subject: implemented using an ivar, and added specs --- app/controllers/projects/commit_controller.rb | 9 +++++++-- app/views/projects/commit/branches.html.haml | 10 +++++----- 2 files changed, 12 insertions(+), 7 deletions(-) (limited to 'app') diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 5b8a8159123..494d412b532 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -16,6 +16,8 @@ class Projects::CommitController < Projects::ApplicationController before_action :define_note_vars, only: [:show, :diff_for_path] before_action :authorize_edit_tree!, only: [:revert, :cherry_pick] + BRANCH_SEARCH_LIMIT = 1000 + def show apply_diff_view_cookie! @@ -59,8 +61,11 @@ class Projects::CommitController < Projects::ApplicationController # branch_names_contains/tag_names_contains can take a long time when there are thousands of # branches/tags - each `git branch --contains xxx` request can consume a cpu core. # so only do the query when there are a manageable number of branches/tags - @branches = @project.repository.branch_count > 1000 ? [:limit_exceeded] : @project.repository.branch_names_contains(commit.id) - @tags = @project.repository.tag_count > 1000 ? [:limit_exceeded] : @project.repository.tag_names_contains(commit.id) + @branches_limit_exceeded = @project.repository.branch_count > BRANCH_SEARCH_LIMIT + @branches = @branches_limit_exceeded ? [] : @project.repository.branch_names_contains(commit.id) + + @tags_limit_exceeded = @project.repository.tag_count > BRANCH_SEARCH_LIMIT + @tags = @tags_limit_exceeded ? [] : @project.repository.tag_names_contains(commit.id) render layout: false end diff --git a/app/views/projects/commit/branches.html.haml b/app/views/projects/commit/branches.html.haml index 398927fb50d..933cb2f73ce 100644 --- a/app/views/projects/commit/branches.html.haml +++ b/app/views/projects/commit/branches.html.haml @@ -1,19 +1,19 @@ -- if @branches.any? - - branch = commit_default_branch(@project, @branches) - - if branch == :limit_exceeded +- if @branches.any? || @branches_limit_exceeded + - if @branches_limit_exceeded = commit_branch_link('#', _('Too many branches to search')) - else + - branch = commit_default_branch(@project, @branches) = commit_branch_link(project_ref_path(@project, branch), branch) -# `commit_default_branch` deletes the default branch from `@branches`, -# so only render this if we have more branches or tags left -- if @branches.any? || @tags.any? +- if @branches.any? || @tags.any? || @tags_limit_exceeded %span = link_to "…", "#", class: "js-details-expand label label-gray" %span.js-details-content.hide = commit_branches_links(@project, @branches) if @branches.any? - - if @tags.first == :limit_exceeded + - if @tags_limit_exceeded = commit_tag_link('#', _('Too many tags to search')) - elsif @tags.any? = commit_tags_links(@project, @tags) -- cgit v1.2.1 From 90baab7e21e6d1f327f2675dcc5516feffb3729f Mon Sep 17 00:00:00 2001 From: Brett Walker Date: Tue, 17 Oct 2017 13:16:42 +0200 Subject: optimize branching logic --- app/views/projects/commit/branches.html.haml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) (limited to 'app') diff --git a/app/views/projects/commit/branches.html.haml b/app/views/projects/commit/branches.html.haml index 933cb2f73ce..31d10ff9040 100644 --- a/app/views/projects/commit/branches.html.haml +++ b/app/views/projects/commit/branches.html.haml @@ -1,9 +1,8 @@ -- if @branches.any? || @branches_limit_exceeded - - if @branches_limit_exceeded - = commit_branch_link('#', _('Too many branches to search')) - - else - - branch = commit_default_branch(@project, @branches) - = commit_branch_link(project_ref_path(@project, branch), branch) +- if @branches_limit_exceeded + = commit_branch_link('#', _('Too many branches to search')) +- elsif @branches.any? + - branch = commit_default_branch(@project, @branches) + = commit_branch_link(project_ref_path(@project, branch), branch) -# `commit_default_branch` deletes the default branch from `@branches`, -# so only render this if we have more branches or tags left -- cgit v1.2.1 From e1d12ba9b988e61afb9317f3a132d6e2caa93923 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 13 Oct 2017 19:21:23 +0200 Subject: Refactor Clusters to be consisted from GcpProvider and KubernetesPlatform --- app/controllers/projects/clusters_controller.rb | 28 ++-- app/models/clusters/cluster.rb | 56 +++++++ app/models/clusters/cluster_project.rb | 6 + app/models/clusters/platforms/kubernetes.rb | 172 +++++++++++++++++++++ app/models/clusters/providers/gcp.rb | 79 ++++++++++ app/models/gcp/cluster.rb | 116 -------------- app/models/project.rb | 4 +- app/services/ci/create_cluster_service.rb | 15 -- app/services/ci/fetch_gcp_operation_service.rb | 17 -- app/services/ci/fetch_kubernetes_token_service.rb | 72 --------- .../ci/finalize_cluster_creation_service.rb | 33 ---- app/services/ci/integrate_cluster_service.rb | 26 ---- app/services/ci/provision_cluster_service.rb | 36 ----- app/services/ci/update_cluster_service.rb | 22 --- app/services/clusters/create_service.rb | 14 ++ .../clusters/gcp/fetch_operation_service.rb | 16 ++ .../clusters/gcp/finalize_creation_service.rb | 66 ++++++++ app/services/clusters/gcp/provision_service.rb | 49 ++++++ .../gcp/verify_provision_status_service.rb | 44 ++++++ app/services/clusters/update_service.rb | 7 + app/workers/cluster_provision_worker.rb | 6 +- app/workers/wait_for_cluster_creation_worker.rb | 21 +-- 22 files changed, 537 insertions(+), 368 deletions(-) create mode 100644 app/models/clusters/cluster.rb create mode 100644 app/models/clusters/cluster_project.rb create mode 100644 app/models/clusters/platforms/kubernetes.rb create mode 100644 app/models/clusters/providers/gcp.rb delete mode 100644 app/models/gcp/cluster.rb delete mode 100644 app/services/ci/create_cluster_service.rb delete mode 100644 app/services/ci/fetch_gcp_operation_service.rb delete mode 100644 app/services/ci/fetch_kubernetes_token_service.rb delete mode 100644 app/services/ci/finalize_cluster_creation_service.rb delete mode 100644 app/services/ci/integrate_cluster_service.rb delete mode 100644 app/services/ci/provision_cluster_service.rb delete mode 100644 app/services/ci/update_cluster_service.rb create mode 100644 app/services/clusters/create_service.rb create mode 100644 app/services/clusters/gcp/fetch_operation_service.rb create mode 100644 app/services/clusters/gcp/finalize_creation_service.rb create mode 100644 app/services/clusters/gcp/provision_service.rb create mode 100644 app/services/clusters/gcp/verify_provision_status_service.rb create mode 100644 app/services/clusters/update_service.rb (limited to 'app') diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb index 03019b0becc..0679d8c69f0 100644 --- a/app/controllers/projects/clusters_controller.rb +++ b/app/controllers/projects/clusters_controller.rb @@ -31,7 +31,7 @@ class Projects::ClustersController < Projects::ApplicationController end def create - @cluster = Ci::CreateClusterService + @cluster = Ci::CreateService .new(project, current_user, create_params) .execute(token_in_session) @@ -88,19 +88,27 @@ class Projects::ClustersController < Projects::ApplicationController def create_params params.require(:cluster).permit( - :gcp_project_id, - :gcp_cluster_zone, - :gcp_cluster_name, - :gcp_cluster_size, - :gcp_machine_type, - :project_namespace, - :enabled) + :enabled, + :platform_type, + :provider_type, + kubernetes_platform: [ + :namespace + ], + gcp_provider: [ + :project_id, + :cluster_zone, + :cluster_name, + :cluster_size, + :machine_type + ]) end def update_params params.require(:cluster).permit( - :project_namespace, - :enabled) + :enabled, + kubernetes_platform: [ + :namespace + ]) end def authorize_google_api diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb new file mode 100644 index 00000000000..d7b13ac88f2 --- /dev/null +++ b/app/models/clusters/cluster.rb @@ -0,0 +1,56 @@ +module Clusters + class Cluster < ActiveRecord::Base + include Presentable + + belongs_to :user + belongs_to :service + + enum :platform_type { + kubernetes: 1 + } + + enum :provider_type { + user: 0, + gcp: 1 + } + + has_many :cluster_projects + has_many :projects, through: :cluster_projects + + has_one :gcp_provider + has_one :kubernetes_platform + + accepts_nested_attributes_for :gcp_provider + accepts_nested_attributes_for :kubernetes_platform + + validates :kubernetes_platform, presence: true, if: :kubernetes? + validates :gcp_provider, presence: true, if: :gcp? + validate :restrict_modification, on: :update + + delegate :status, to: :provider, allow_nil: true + delegate :status_reason, to: :provider, allow_nil: true + + def restrict_modification + if provider&.on_creation? + errors.add(:base, "cannot modify during creation") + return false + end + + true + end + + def provider + return gcp_provider if gcp? + end + + def platform + return kubernetes_platform if kubernetes? + end + + def first_project + return @first_project if defined?(@first_project) + + @first_project = projects.first + end + end +end diff --git a/app/models/clusters/cluster_project.rb b/app/models/clusters/cluster_project.rb new file mode 100644 index 00000000000..7b139c2bb08 --- /dev/null +++ b/app/models/clusters/cluster_project.rb @@ -0,0 +1,6 @@ +module Clusters + class ClusterProject < ActiveRecord::Base + belongs_to :cluster + belongs_to :project + end +end diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb new file mode 100644 index 00000000000..aed6f733487 --- /dev/null +++ b/app/models/clusters/platforms/kubernetes.rb @@ -0,0 +1,172 @@ +module Clusters + module Platforms + class Kubernetes < ActiveRecord::Base + include Gitlab::Kubernetes + include ReactiveCaching + + TEMPLATE_PLACEHOLDER = 'Kubernetes namespace'.freeze + + self.reactive_cache_key = ->(service) { [service.class.model_name.singular, service.project_id] } + + belongs_to :cluster + + attr_encrypted :password, + mode: :per_attribute_iv, + key: Gitlab::Application.secrets.db_key_base, + algorithm: 'aes-256-cbc' + + attr_encrypted :token, + mode: :per_attribute_iv, + key: Gitlab::Application.secrets.db_key_base, + algorithm: 'aes-256-cbc' + + validates :namespace, + allow_blank: true, + length: 1..63, + format: { + with: Gitlab::Regex.kubernetes_namespace_regex, + message: Gitlab::Regex.kubernetes_namespace_regex_message + } + + validates :api_url, url: true, presence: true + validates :token, presence: true + + after_save :clear_reactive_cache! + + before_validation :enforce_namespace_to_lower_case + + def actual_namespace + if namespace.present? + namespace + else + default_namespace + end + end + + def predefined_variables + config = YAML.dump(kubeconfig) + + variables = [ + { key: 'KUBE_URL', value: api_url, public: true }, + { key: 'KUBE_TOKEN', value: token, public: false }, + { key: 'KUBE_NAMESPACE', value: actual_namespace, public: true }, + { key: 'KUBECONFIG', value: config, public: false, file: true } + ] + + if ca_pem.present? + variables << { key: 'KUBE_CA_PEM', value: ca_pem, public: true } + variables << { key: 'KUBE_CA_PEM_FILE', value: ca_pem, public: true, file: true } + end + + variables + end + + # Constructs a list of terminals from the reactive cache + # + # Returns nil if the cache is empty, in which case you should try again a + # short time later + def terminals(environment) + with_reactive_cache do |data| + pods = filter_by_label(data[:pods], app: environment.slug) + terminals = pods.flat_map { |pod| terminals_for_pod(api_url, actual_namespace, pod) } + terminals.each { |terminal| add_terminal_auth(terminal, terminal_auth) } + end + end + + # Caches resources in the namespace so other calls don't need to block on + # network access + def calculate_reactive_cache + return unless active? && project && !project.pending_delete? + + # We may want to cache extra things in the future + { pods: read_pods } + end + + def kubeconfig + to_kubeconfig( + url: api_url, + namespace: actual_namespace, + token: token, + ca_pem: ca_pem) + end + + def namespace_placeholder + default_namespace || TEMPLATE_PLACEHOLDER + end + + def default_namespace + "#{cluster.first_project.path}-#{cluster.first_project.id}" if cluster.first_project + end + + def read_secrets + kubeclient = build_kubeclient! + + kubeclient.get_secrets.as_json + rescue KubeException => err + raise err unless err.error_code == 404 + [] + end + + # Returns a hash of all pods in the namespace + def read_pods + kubeclient = build_kubeclient! + + kubeclient.get_pods(namespace: actual_namespace).as_json + rescue KubeException => err + raise err unless err.error_code == 404 + [] + end + + def kubeclient_ssl_options + opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER } + + if ca_pem.present? + opts[:cert_store] = OpenSSL::X509::Store.new + opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem)) + end + + opts + end + + private + + def build_kubeclient!(api_path: 'api', api_version: 'v1') + raise "Incomplete settings" unless api_url && actual_namespace && token + + ::Kubeclient::Client.new( + join_api_url(api_path), + api_version, + auth_options: kubeclient_auth_options, + ssl_options: kubeclient_ssl_options, + http_proxy_uri: ENV['http_proxy'] + ) + end + + def kubeclient_auth_options + return { username: username, password: password } if username + return { bearer_token: token } if token + end + + def join_api_url(api_path) + url = URI.parse(api_url) + prefix = url.path.sub(%r{/+\z}, '') + + url.path = [prefix, api_path].join("/") + + url.to_s + end + + def terminal_auth + { + token: token, + ca_pem: ca_pem, + max_session_time: current_application_settings.terminal_max_session_time + } + end + + def enforce_namespace_to_lower_case + self.namespace = self.namespace&.downcase + end + end + end +end diff --git a/app/models/clusters/providers/gcp.rb b/app/models/clusters/providers/gcp.rb new file mode 100644 index 00000000000..5d4618cfe87 --- /dev/null +++ b/app/models/clusters/providers/gcp.rb @@ -0,0 +1,79 @@ +module Clusters + module Providers + class Gcp < ActiveRecord::Base + belongs_to :cluster + + default_value_for :cluster_zone, 'us-central1-a' + default_value_for :cluster_size, 3 + default_value_for :machine_type, 'n1-standard-4' + + attr_encrypted :access_token, + mode: :per_attribute_iv, + key: Gitlab::Application.secrets.db_key_base, + algorithm: 'aes-256-cbc' + + validates :project_id, + length: 1..63, + format: { + with: Gitlab::Regex.kubernetes_namespace_regex, + message: Gitlab::Regex.kubernetes_namespace_regex_message + } + + validates :cluster_name, + length: 1..63, + format: { + with: Gitlab::Regex.kubernetes_namespace_regex, + message: Gitlab::Regex.kubernetes_namespace_regex_message + } + + validates :cluster_zone, presence: true + + validates :cluster_size, + presence: true, + numericality: { + only_integer: true, + greater_than: 0 + } + + state_machine :status, initial: :scheduled do + state :scheduled, value: 1 + state :creating, value: 2 + state :created, value: 3 + state :errored, value: 4 + + event :make_creating do + transition any - [:creating] => :creating + end + + event :make_created do + transition any - [:created] => :created + end + + event :make_errored do + transition any - [:errored] => :errored + end + + before_transition any => [:errored, :created] do |provider| + provider.token = nil + provider.operation_id = nil + provider.save! + end + + before_transition any => [:errored] do |provider, transition| + status_reason = transition.args.first + provider.status_reason = status_reason if status_reason + end + end + + def on_creation? + scheduled? || creating? + end + + def api_client + return unless access_token + + @api_client ||= GoogleApi::CloudPlatform::Client.new(access_token, nil) + end + end + end +end diff --git a/app/models/gcp/cluster.rb b/app/models/gcp/cluster.rb deleted file mode 100644 index 162a690c0e3..00000000000 --- a/app/models/gcp/cluster.rb +++ /dev/null @@ -1,116 +0,0 @@ -module Gcp - class Cluster < ActiveRecord::Base - extend Gitlab::Gcp::Model - include Presentable - - belongs_to :project, inverse_of: :cluster - belongs_to :user - belongs_to :service - - scope :enabled, -> { where(enabled: true) } - scope :disabled, -> { where(enabled: false) } - - default_value_for :gcp_cluster_zone, 'us-central1-a' - default_value_for :gcp_cluster_size, 3 - default_value_for :gcp_machine_type, 'n1-standard-4' - - attr_encrypted :password, - mode: :per_attribute_iv, - key: Gitlab::Application.secrets.db_key_base, - algorithm: 'aes-256-cbc' - - attr_encrypted :kubernetes_token, - mode: :per_attribute_iv, - key: Gitlab::Application.secrets.db_key_base, - algorithm: 'aes-256-cbc' - - attr_encrypted :gcp_token, - mode: :per_attribute_iv, - key: Gitlab::Application.secrets.db_key_base, - algorithm: 'aes-256-cbc' - - validates :gcp_project_id, - length: 1..63, - format: { - with: Gitlab::Regex.kubernetes_namespace_regex, - message: Gitlab::Regex.kubernetes_namespace_regex_message - } - - validates :gcp_cluster_name, - length: 1..63, - format: { - with: Gitlab::Regex.kubernetes_namespace_regex, - message: Gitlab::Regex.kubernetes_namespace_regex_message - } - - validates :gcp_cluster_zone, presence: true - - validates :gcp_cluster_size, - presence: true, - numericality: { - only_integer: true, - greater_than: 0 - } - - validates :project_namespace, - allow_blank: true, - length: 1..63, - format: { - with: Gitlab::Regex.kubernetes_namespace_regex, - message: Gitlab::Regex.kubernetes_namespace_regex_message - } - - # if we do not do status transition we prevent change - validate :restrict_modification, on: :update, unless: :status_changed? - - state_machine :status, initial: :scheduled do - state :scheduled, value: 1 - state :creating, value: 2 - state :created, value: 3 - state :errored, value: 4 - - event :make_creating do - transition any - [:creating] => :creating - end - - event :make_created do - transition any - [:created] => :created - end - - event :make_errored do - transition any - [:errored] => :errored - end - - before_transition any => [:errored, :created] do |cluster| - cluster.gcp_token = nil - cluster.gcp_operation_id = nil - end - - before_transition any => [:errored] do |cluster, transition| - status_reason = transition.args.first - cluster.status_reason = status_reason if status_reason - end - end - - def project_namespace_placeholder - "#{project.path}-#{project.id}" - end - - def on_creation? - scheduled? || creating? - end - - def api_url - 'https://' + endpoint if endpoint - end - - def restrict_modification - if on_creation? - errors.add(:base, "cannot modify during creation") - return false - end - - true - end - end -end diff --git a/app/models/project.rb b/app/models/project.rb index 4689b588906..bc263b63881 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -177,7 +177,9 @@ class Project < ActiveRecord::Base has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true has_one :project_feature, inverse_of: :project has_one :statistics, class_name: 'ProjectStatistics' - has_one :cluster, class_name: 'Gcp::Cluster', inverse_of: :project + + has_many :cluster_projects, class_name: 'Clusters::ClusterProject' + has_one :cluster, through: :cluster_projects # Container repositories need to remove data from the container registry, # which is not managed by the DB. Hence we're still using dependent: :destroy diff --git a/app/services/ci/create_cluster_service.rb b/app/services/ci/create_cluster_service.rb deleted file mode 100644 index f7ee0e468e2..00000000000 --- a/app/services/ci/create_cluster_service.rb +++ /dev/null @@ -1,15 +0,0 @@ -module Ci - class CreateClusterService < BaseService - def execute(access_token) - params['gcp_machine_type'] ||= GoogleApi::CloudPlatform::Client::DEFAULT_MACHINE_TYPE - - cluster_params = - params.merge(user: current_user, - gcp_token: access_token) - - project.create_cluster(cluster_params).tap do |cluster| - ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted? - end - end - end -end diff --git a/app/services/ci/fetch_gcp_operation_service.rb b/app/services/ci/fetch_gcp_operation_service.rb deleted file mode 100644 index 0b68e4d6ea9..00000000000 --- a/app/services/ci/fetch_gcp_operation_service.rb +++ /dev/null @@ -1,17 +0,0 @@ -module Ci - class FetchGcpOperationService - def execute(cluster) - api_client = - GoogleApi::CloudPlatform::Client.new(cluster.gcp_token, nil) - - operation = api_client.projects_zones_operations( - cluster.gcp_project_id, - cluster.gcp_cluster_zone, - cluster.gcp_operation_id) - - yield(operation) if block_given? - rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e - return cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}") - end - end -end diff --git a/app/services/ci/fetch_kubernetes_token_service.rb b/app/services/ci/fetch_kubernetes_token_service.rb deleted file mode 100644 index 44da87cb00c..00000000000 --- a/app/services/ci/fetch_kubernetes_token_service.rb +++ /dev/null @@ -1,72 +0,0 @@ -## -# TODO: -# Almost components in this class were copied from app/models/project_services/kubernetes_service.rb -# We should dry up those classes not to repeat the same code. -# Maybe we should have a special facility (e.g. lib/kubernetes_api) to maintain all Kubernetes API caller. -module Ci - class FetchKubernetesTokenService - attr_reader :api_url, :ca_pem, :username, :password - - def initialize(api_url, ca_pem, username, password) - @api_url = api_url - @ca_pem = ca_pem - @username = username - @password = password - end - - def execute - read_secrets.each do |secret| - name = secret.dig('metadata', 'name') - if /default-token/ =~ name - token_base64 = secret.dig('data', 'token') - return Base64.decode64(token_base64) if token_base64 - end - end - - nil - end - - private - - def read_secrets - kubeclient = build_kubeclient! - - kubeclient.get_secrets.as_json - rescue KubeException => err - raise err unless err.error_code == 404 - [] - end - - def build_kubeclient!(api_path: 'api', api_version: 'v1') - raise "Incomplete settings" unless api_url && username && password - - ::Kubeclient::Client.new( - join_api_url(api_path), - api_version, - auth_options: { username: username, password: password }, - ssl_options: kubeclient_ssl_options, - http_proxy_uri: ENV['http_proxy'] - ) - end - - def join_api_url(api_path) - url = URI.parse(api_url) - prefix = url.path.sub(%r{/+\z}, '') - - url.path = [prefix, api_path].join("/") - - url.to_s - end - - def kubeclient_ssl_options - opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER } - - if ca_pem.present? - opts[:cert_store] = OpenSSL::X509::Store.new - opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem)) - end - - opts - end - end -end diff --git a/app/services/ci/finalize_cluster_creation_service.rb b/app/services/ci/finalize_cluster_creation_service.rb deleted file mode 100644 index 347875c5697..00000000000 --- a/app/services/ci/finalize_cluster_creation_service.rb +++ /dev/null @@ -1,33 +0,0 @@ -module Ci - class FinalizeClusterCreationService - def execute(cluster) - api_client = - GoogleApi::CloudPlatform::Client.new(cluster.gcp_token, nil) - - begin - gke_cluster = api_client.projects_zones_clusters_get( - cluster.gcp_project_id, - cluster.gcp_cluster_zone, - cluster.gcp_cluster_name) - rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e - return cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}") - end - - endpoint = gke_cluster.endpoint - api_url = 'https://' + endpoint - ca_cert = Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate) - username = gke_cluster.master_auth.username - password = gke_cluster.master_auth.password - - kubernetes_token = Ci::FetchKubernetesTokenService.new( - api_url, ca_cert, username, password).execute - - unless kubernetes_token - return cluster.make_errored!('Failed to get a default token of kubernetes') - end - - Ci::IntegrateClusterService.new.execute( - cluster, endpoint, ca_cert, kubernetes_token, username, password) - end - end -end diff --git a/app/services/ci/integrate_cluster_service.rb b/app/services/ci/integrate_cluster_service.rb deleted file mode 100644 index d123ce8d26b..00000000000 --- a/app/services/ci/integrate_cluster_service.rb +++ /dev/null @@ -1,26 +0,0 @@ -module Ci - class IntegrateClusterService - def execute(cluster, endpoint, ca_cert, token, username, password) - Gcp::Cluster.transaction do - cluster.update!( - enabled: true, - endpoint: endpoint, - ca_cert: ca_cert, - kubernetes_token: token, - username: username, - password: password, - service: cluster.project.find_or_initialize_service('kubernetes'), - status_event: :make_created) - - cluster.service.update!( - active: true, - api_url: cluster.api_url, - ca_pem: ca_cert, - namespace: cluster.project_namespace, - token: token) - end - rescue ActiveRecord::RecordInvalid => e - cluster.make_errored!("Failed to integrate cluster into kubernetes_service: #{e.message}") - end - end -end diff --git a/app/services/ci/provision_cluster_service.rb b/app/services/ci/provision_cluster_service.rb deleted file mode 100644 index 52d80b01813..00000000000 --- a/app/services/ci/provision_cluster_service.rb +++ /dev/null @@ -1,36 +0,0 @@ -module Ci - class ProvisionClusterService - def execute(cluster) - api_client = - GoogleApi::CloudPlatform::Client.new(cluster.gcp_token, nil) - - begin - operation = api_client.projects_zones_clusters_create( - cluster.gcp_project_id, - cluster.gcp_cluster_zone, - cluster.gcp_cluster_name, - cluster.gcp_cluster_size, - machine_type: cluster.gcp_machine_type) - rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e - return cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}") - end - - unless operation.status == 'RUNNING' || operation.status == 'PENDING' - return cluster.make_errored!("Operation status is unexpected; #{operation.status_message}") - end - - cluster.gcp_operation_id = api_client.parse_operation_id(operation.self_link) - - unless cluster.gcp_operation_id - return cluster.make_errored!('Can not find operation_id from self_link') - end - - if cluster.make_creating - WaitForClusterCreationWorker.perform_in( - WaitForClusterCreationWorker::INITIAL_INTERVAL, cluster.id) - else - return cluster.make_errored!("Failed to update cluster record; #{cluster.errors}") - end - end - end -end diff --git a/app/services/ci/update_cluster_service.rb b/app/services/ci/update_cluster_service.rb deleted file mode 100644 index 70d88fca660..00000000000 --- a/app/services/ci/update_cluster_service.rb +++ /dev/null @@ -1,22 +0,0 @@ -module Ci - class UpdateClusterService < BaseService - def execute(cluster) - Gcp::Cluster.transaction do - cluster.update!(params) - - if params['enabled'] == 'true' - cluster.service.update!( - active: true, - api_url: cluster.api_url, - ca_pem: cluster.ca_cert, - namespace: cluster.project_namespace, - token: cluster.kubernetes_token) - else - cluster.service.update!(active: false) - end - end - rescue ActiveRecord::RecordInvalid => e - cluster.errors.add(:base, e.message) - end - end -end diff --git a/app/services/clusters/create_service.rb b/app/services/clusters/create_service.rb new file mode 100644 index 00000000000..5429bc21256 --- /dev/null +++ b/app/services/clusters/create_service.rb @@ -0,0 +1,14 @@ +module Clusters + class CreateService < BaseService + def execute(access_token) + params['gcp_machine_type'] ||= GoogleApi::CloudPlatform::Client::DEFAULT_MACHINE_TYPE + + cluster_params = + params.merge(user: current_user) + + project.create_cluster(cluster_params).tap do |cluster| + ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted? + end + end + end +end diff --git a/app/services/clusters/gcp/fetch_operation_service.rb b/app/services/clusters/gcp/fetch_operation_service.rb new file mode 100644 index 00000000000..013225efac4 --- /dev/null +++ b/app/services/clusters/gcp/fetch_operation_service.rb @@ -0,0 +1,16 @@ +module Clusters + module Gcp + class FetchOperationService + def execute(provider) + operation = provider.api_client.projects_zones_operations( + provider.project_id, + provider.cluster_zone, + provider.operation_id) + + yield(operation) if block_given? + rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e + return provider.make_errored!("Failed to request to CloudPlatform; #{e.message}") + end + end + end +end diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb new file mode 100644 index 00000000000..b536285b368 --- /dev/null +++ b/app/services/clusters/gcp/finalize_creation_service.rb @@ -0,0 +1,66 @@ +module Clusters + module Gcp + class FinalizeCreationService + attr_reader :provider + + def execute(provider) + @provider = provider + + configure_provider + configure_kubernetes_platform + request_kuberenetes_platform_token + + ActiveRecord::Base.transaction do + kubernetes_platform.update! + provider.make_created! + end + rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e + return cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}") + rescue ActiveRecord::RecordInvalid => e + cluster.make_errored!("Failed to configure GKE Cluster: #{e.message}") + end + + private + + def configure_provider + provider.endpoint = gke_cluster.endpoint + end + + def configure_kubernetes_platform + kubernetes_platform = cluster.kubernetes_platform + kubernetes_platform.api_url = 'https://' + endpoint + kubernetes_platform.ca_cert = Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate) + kubernetes_platform.username = gke_cluster.master_auth.username + kubernetes_platform.password = gke_cluster.master_auth.password + end + + def request_kuberenetes_platform_token + kubernetes_platform.read_secrets.each do |secret| + name = secret.dig('metadata', 'name') + if /default-token/ =~ name + token_base64 = secret.dig('data', 'token') + if token_base64 + kubernetes_platform.token = Base64.decode64(token_base64) + break + end + end + end + end + + def gke_cluster + @gke_cluster ||= provider.api_client.projects_zones_clusters_get( + provider.gcp_project_id, + provider.gcp_cluster_zone, + provider.gcp_cluster_name) + end + + def cluster + provider.cluster + end + + def kubernetes_platform + cluster.kubernetes_platform + end + end + end +end diff --git a/app/services/clusters/gcp/provision_service.rb b/app/services/clusters/gcp/provision_service.rb new file mode 100644 index 00000000000..269705000ac --- /dev/null +++ b/app/services/clusters/gcp/provision_service.rb @@ -0,0 +1,49 @@ +module Clusters + module Gcp + class ProvisionService + attr_reader :provider + + def execute(provider) + @provider = provider + + unless operation.status == 'RUNNING' || operation.status == 'PENDING' + return provider.make_errored!("Operation status is unexpected; #{operation.status_message}") + end + + provider.operation_id = operation_id + + unless provider.operation_id + return provider.make_errored!('Can not find operation_id from self_link') + end + + if provider.make_creating + WaitForClusterCreationWorker.perform_in( + WaitForClusterCreationWorker::INITIAL_INTERVAL, provider.id) + else + return provider.make_errored!("Failed to update provider record; #{provider.errors}") + end + rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e + return provider.make_errored!("Failed to request to CloudPlatform; #{e.message}") + end + + private + + def operation_id + api_client.parse_operation_id(operation.self_link) + end + + def operation + @operation ||= api_client.projects_zones_providers_create( + provider.project_id, + provider.provider_zone, + provider.provider_name, + provider.provider_size, + machine_type: provider.machine_type) + end + + def api_client + provider.api_client + end + end + end +end diff --git a/app/services/clusters/gcp/verify_provision_status_service.rb b/app/services/clusters/gcp/verify_provision_status_service.rb new file mode 100644 index 00000000000..466ea986516 --- /dev/null +++ b/app/services/clusters/gcp/verify_provision_status_service.rb @@ -0,0 +1,44 @@ +module Clusters + module Gcp + class VerifyProvisionStatusService + attr_reader :provider + + INITIAL_INTERVAL = 2.minutes + EAGER_INTERVAL = 10.seconds + TIMEOUT = 20.minutes + + def execute(provider) + @provider = provider + + request_operation do |operation| + case operation.status + when 'RUNNING' + continue_creation(operation) + when 'DONE' + finalize_creation + else + return provider.make_errored!("Unexpected operation status; #{operation.status} #{operation.status_message}") + end + end + end + + private + + def continue_creation(operation) + if TIMEOUT < Time.now.utc - operation.start_time.to_time.utc + return provider.make_errored!("Cluster creation time exceeds timeout; #{TIMEOUT}") + end + + WaitForClusterCreationWorker.perform_in(EAGER_INTERVAL, provider.cluster_id) + end + + def finalize_creation + Clusters::Gcp::FinalizeCreationService.new.execute(provider) + end + + def request_operation(&blk) + Clusters::FetchGcpOperationService.new.execute(provider, &blk) + end + end + end +end diff --git a/app/services/clusters/update_service.rb b/app/services/clusters/update_service.rb new file mode 100644 index 00000000000..989218e32a2 --- /dev/null +++ b/app/services/clusters/update_service.rb @@ -0,0 +1,7 @@ +module Clusters + class UpdateService < BaseService + def execute(cluster) + cluster.update(params) + end + end +end diff --git a/app/workers/cluster_provision_worker.rb b/app/workers/cluster_provision_worker.rb index 63300b58a25..79f0d73c396 100644 --- a/app/workers/cluster_provision_worker.rb +++ b/app/workers/cluster_provision_worker.rb @@ -3,8 +3,10 @@ class ClusterProvisionWorker include ClusterQueue def perform(cluster_id) - Gcp::Cluster.find_by_id(cluster_id).try do |cluster| - Ci::ProvisionClusterService.new.execute(cluster) + Clusters::Cluster.find_by_id(cluster_id).try do |cluster| + cluster.gcp_provider.try do |provider| + Clusters::Gcp::ProvisionService.new.execute(provider) + end end end end diff --git a/app/workers/wait_for_cluster_creation_worker.rb b/app/workers/wait_for_cluster_creation_worker.rb index 5aa3bbdaa9d..d8c42c6bd55 100644 --- a/app/workers/wait_for_cluster_creation_worker.rb +++ b/app/workers/wait_for_cluster_creation_worker.rb @@ -2,25 +2,10 @@ class WaitForClusterCreationWorker include Sidekiq::Worker include ClusterQueue - INITIAL_INTERVAL = 2.minutes - EAGER_INTERVAL = 10.seconds - TIMEOUT = 20.minutes - def perform(cluster_id) - Gcp::Cluster.find_by_id(cluster_id).try do |cluster| - Ci::FetchGcpOperationService.new.execute(cluster) do |operation| - case operation.status - when 'RUNNING' - if TIMEOUT < Time.now.utc - operation.start_time.to_time.utc - return cluster.make_errored!("Cluster creation time exceeds timeout; #{TIMEOUT}") - end - - WaitForClusterCreationWorker.perform_in(EAGER_INTERVAL, cluster.id) - when 'DONE' - Ci::FinalizeClusterCreationService.new.execute(cluster) - else - return cluster.make_errored!("Unexpected operation status; #{operation.status} #{operation.status_message}") - end + Clusters::Cluster.find_by_id(cluster_id).try do |cluster| + cluster.gcp_provider.try do |provider| + Clusters::Gcp::VerifyProvisionStatusService.new.execute(provider) end end end -- cgit v1.2.1 From d0cff7f5855f91b5479f9fdaa39d8d95ec691a9e Mon Sep 17 00:00:00 2001 From: Shinya Maeda Date: Mon, 23 Oct 2017 11:36:35 +0300 Subject: This works --- app/controllers/projects/clusters_controller.rb | 26 +++++++---- app/models/clusters/cluster.rb | 52 ++++++++++++--------- app/models/clusters/cluster_project.rb | 6 --- app/models/clusters/platforms/kubernetes.rb | 39 +++++++++------- app/models/clusters/project.rb | 8 ++++ app/models/clusters/providers/gcp.rb | 29 ++++++------ app/models/project.rb | 4 +- app/policies/clusters/cluster_policy.rb | 12 +++++ app/policies/gcp/cluster_policy.rb | 12 ----- app/presenters/clusters/cluster_presenter.rb | 9 ++++ app/presenters/gcp/cluster_presenter.rb | 9 ---- app/services/clusters/create_service.rb | 35 ++++++++++++-- .../clusters/gcp/fetch_operation_service.rb | 6 +-- .../clusters/gcp/finalize_creation_service.rb | 38 +++++++-------- app/services/clusters/gcp/provision_service.rb | 54 +++++++++++----------- .../gcp/verify_provision_status_service.rb | 14 ++++-- app/validators/cluster_name_validator.rb | 24 ++++++++++ app/views/projects/clusters/_form.html.haml | 44 ++++++++++-------- app/views/projects/clusters/show.html.haml | 6 +-- app/workers/cluster_provision_worker.rb | 2 +- app/workers/wait_for_cluster_creation_worker.rb | 2 +- 21 files changed, 252 insertions(+), 179 deletions(-) delete mode 100644 app/models/clusters/cluster_project.rb create mode 100644 app/models/clusters/project.rb create mode 100644 app/policies/clusters/cluster_policy.rb delete mode 100644 app/policies/gcp/cluster_policy.rb create mode 100644 app/presenters/clusters/cluster_presenter.rb delete mode 100644 app/presenters/gcp/cluster_presenter.rb create mode 100644 app/validators/cluster_name_validator.rb (limited to 'app') diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb index 0679d8c69f0..c07d955f148 100644 --- a/app/controllers/projects/clusters_controller.rb +++ b/app/controllers/projects/clusters_controller.rb @@ -27,11 +27,17 @@ class Projects::ClustersController < Projects::ApplicationController end def new - @cluster = project.build_cluster + @cluster = Clusters::Cluster.new( + platform_type: :kubernetes, + provider_type: :gcp).tap do |cluster| + cluster.build_provider_gcp + cluster.build_platform_kubernetes + cluster.projects << project + end end def create - @cluster = Ci::CreateService + @cluster = Clusters::CreateService .new(project, current_user, create_params) .execute(token_in_session) @@ -58,7 +64,7 @@ class Projects::ClustersController < Projects::ApplicationController end def update - Ci::UpdateClusterService + Clusters::UpdateService .new(project, current_user, update_params) .execute(cluster) @@ -89,16 +95,16 @@ class Projects::ClustersController < Projects::ApplicationController def create_params params.require(:cluster).permit( :enabled, + :name, :platform_type, :provider_type, - kubernetes_platform: [ + platform_kubernetes_attributes: [ :namespace ], - gcp_provider: [ - :project_id, - :cluster_zone, - :cluster_name, - :cluster_size, + provider_gcp_attributes: [ + :gcp_project_id, + :zone, + :num_nodes, :machine_type ]) end @@ -106,7 +112,7 @@ class Projects::ClustersController < Projects::ApplicationController def update_params params.require(:cluster).permit( :enabled, - kubernetes_platform: [ + platform_kubernetes_attributes: [ :namespace ]) end diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index d7b13ac88f2..f1eedad8795 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -2,49 +2,46 @@ module Clusters class Cluster < ActiveRecord::Base include Presentable + self.table_name = 'clusters' + belongs_to :user - belongs_to :service - enum :platform_type { + enum platform_type: { kubernetes: 1 } - enum :provider_type { + enum provider_type: { user: 0, gcp: 1 } - has_many :cluster_projects - has_many :projects, through: :cluster_projects + has_many :cluster_projects, class_name: 'Clusters::Project' + has_many :projects, through: :cluster_projects, class_name: '::Project' - has_one :gcp_provider - has_one :kubernetes_platform + has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp' + has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes' - accepts_nested_attributes_for :gcp_provider - accepts_nested_attributes_for :kubernetes_platform + accepts_nested_attributes_for :provider_gcp + accepts_nested_attributes_for :platform_kubernetes - validates :kubernetes_platform, presence: true, if: :kubernetes? - validates :gcp_provider, presence: true, if: :gcp? + validates :name, cluster_name: true validate :restrict_modification, on: :update delegate :status, to: :provider, allow_nil: true delegate :status_reason, to: :provider, allow_nil: true - - def restrict_modification - if provider&.on_creation? - errors.add(:base, "cannot modify during creation") - return false - end - - true - end + delegate :status_name, to: :provider, allow_nil: true + delegate :on_creation?, to: :provider, allow_nil: true def provider - return gcp_provider if gcp? + return provider_gcp if gcp? end def platform - return kubernetes_platform if kubernetes? + return platform_kubernetes if kubernetes? + end + + def project + first_project end def first_project @@ -52,5 +49,16 @@ module Clusters @first_project = projects.first end + + private + + def restrict_modification + if provider&.on_creation? + errors.add(:base, "cannot modify during creation") + return false + end + + true + end end end diff --git a/app/models/clusters/cluster_project.rb b/app/models/clusters/cluster_project.rb deleted file mode 100644 index 7b139c2bb08..00000000000 --- a/app/models/clusters/cluster_project.rb +++ /dev/null @@ -1,6 +0,0 @@ -module Clusters - class ClusterProject < ActiveRecord::Base - belongs_to :cluster - belongs_to :project - end -end diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index aed6f733487..d9f8927f7cc 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -4,11 +4,13 @@ module Clusters include Gitlab::Kubernetes include ReactiveCaching + self.table_name = 'cluster_platforms_kubernetes' + TEMPLATE_PLACEHOLDER = 'Kubernetes namespace'.freeze - self.reactive_cache_key = ->(service) { [service.class.model_name.singular, service.project_id] } + self.reactive_cache_key = ->(kubernetes) { [kubernetes.class.model_name.singular, kubernetes.cluster_id] } - belongs_to :cluster + belongs_to :cluster, inverse_of: :platform_kubernetes, class_name: 'Clusters::Cluster' attr_encrypted :password, mode: :per_attribute_iv, @@ -28,8 +30,8 @@ module Clusters message: Gitlab::Regex.kubernetes_namespace_regex_message } - validates :api_url, url: true, presence: true - validates :token, presence: true + validates :api_url, url: true, presence: true, on: :update + validates :token, presence: true, on: :update after_save :clear_reactive_cache! @@ -53,9 +55,9 @@ module Clusters { key: 'KUBECONFIG', value: config, public: false, file: true } ] - if ca_pem.present? - variables << { key: 'KUBE_CA_PEM', value: ca_pem, public: true } - variables << { key: 'KUBE_CA_PEM_FILE', value: ca_pem, public: true, file: true } + if ca_cert.present? + variables << { key: 'KUBE_CA_PEM', value: ca_cert, public: true } + variables << { key: 'KUBE_CA_PEM_FILE', value: ca_cert, public: true, file: true } end variables @@ -76,7 +78,7 @@ module Clusters # Caches resources in the namespace so other calls don't need to block on # network access def calculate_reactive_cache - return unless active? && project && !project.pending_delete? + return unless active? && cluster.project && !cluster.project.pending_delete? # We may want to cache extra things in the future { pods: read_pods } @@ -87,15 +89,16 @@ module Clusters url: api_url, namespace: actual_namespace, token: token, - ca_pem: ca_pem) + ca_pem: ca_cert) end def namespace_placeholder default_namespace || TEMPLATE_PLACEHOLDER end - def default_namespace - "#{cluster.first_project.path}-#{cluster.first_project.id}" if cluster.first_project + def default_namespace(project = nil) + project ||= cluster&.project + "#{project.path}-#{project.id}" if project end def read_secrets @@ -120,9 +123,9 @@ module Clusters def kubeclient_ssl_options opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER } - if ca_pem.present? + if ca_cert.present? opts[:cert_store] = OpenSSL::X509::Store.new - opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem)) + opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_cert)) end opts @@ -131,7 +134,11 @@ module Clusters private def build_kubeclient!(api_path: 'api', api_version: 'v1') - raise "Incomplete settings" unless api_url && actual_namespace && token + raise "Incomplete settings" unless api_url && actual_namespace + + unless (username && password) || token + raise "Either username/password or token is required to access API" + end ::Kubeclient::Client.new( join_api_url(api_path), @@ -143,7 +150,7 @@ module Clusters end def kubeclient_auth_options - return { username: username, password: password } if username + return { username: username, password: password } if username && password return { bearer_token: token } if token end @@ -159,7 +166,7 @@ module Clusters def terminal_auth { token: token, - ca_pem: ca_pem, + ca_pem: ca_cert, max_session_time: current_application_settings.terminal_max_session_time } end diff --git a/app/models/clusters/project.rb b/app/models/clusters/project.rb new file mode 100644 index 00000000000..69088100420 --- /dev/null +++ b/app/models/clusters/project.rb @@ -0,0 +1,8 @@ +module Clusters + class Project < ActiveRecord::Base + self.table_name = 'cluster_projects' + + belongs_to :cluster, inverse_of: :projects, class_name: 'Clusters::Cluster' + belongs_to :project, inverse_of: :project, class_name: 'Project' + end +end diff --git a/app/models/clusters/providers/gcp.rb b/app/models/clusters/providers/gcp.rb index 5d4618cfe87..e4f109d2794 100644 --- a/app/models/clusters/providers/gcp.rb +++ b/app/models/clusters/providers/gcp.rb @@ -1,10 +1,12 @@ module Clusters module Providers class Gcp < ActiveRecord::Base - belongs_to :cluster + self.table_name = 'cluster_providers_gcp' - default_value_for :cluster_zone, 'us-central1-a' - default_value_for :cluster_size, 3 + belongs_to :cluster, inverse_of: :provider_gcp, class_name: 'Clusters::Cluster' + + default_value_for :zone, 'us-central1-a' + default_value_for :num_nodes, 3 default_value_for :machine_type, 'n1-standard-4' attr_encrypted :access_token, @@ -12,23 +14,16 @@ module Clusters key: Gitlab::Application.secrets.db_key_base, algorithm: 'aes-256-cbc' - validates :project_id, + validates :gcp_project_id, length: 1..63, format: { with: Gitlab::Regex.kubernetes_namespace_regex, message: Gitlab::Regex.kubernetes_namespace_regex_message } - validates :cluster_name, - length: 1..63, - format: { - with: Gitlab::Regex.kubernetes_namespace_regex, - message: Gitlab::Regex.kubernetes_namespace_regex_message - } - - validates :cluster_zone, presence: true + validates :zone, presence: true - validates :cluster_size, + validates :num_nodes, presence: true, numericality: { only_integer: true, @@ -54,9 +49,13 @@ module Clusters end before_transition any => [:errored, :created] do |provider| - provider.token = nil + provider.access_token = nil provider.operation_id = nil - provider.save! + end + + before_transition any => [:creating] do |provider, transition| + operation_id = transition.args.first + provider.operation_id = operation_id if operation_id end before_transition any => [:errored] do |provider, transition| diff --git a/app/models/project.rb b/app/models/project.rb index bc263b63881..70c75edcda3 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -178,8 +178,8 @@ class Project < ActiveRecord::Base has_one :project_feature, inverse_of: :project has_one :statistics, class_name: 'ProjectStatistics' - has_many :cluster_projects, class_name: 'Clusters::ClusterProject' - has_one :cluster, through: :cluster_projects + has_one :cluster_project, class_name: 'Clusters::Project' + has_one :cluster, through: :cluster_project, class_name: 'Clusters::Cluster' # Container repositories need to remove data from the container registry, # which is not managed by the DB. Hence we're still using dependent: :destroy diff --git a/app/policies/clusters/cluster_policy.rb b/app/policies/clusters/cluster_policy.rb new file mode 100644 index 00000000000..1f7c13072b9 --- /dev/null +++ b/app/policies/clusters/cluster_policy.rb @@ -0,0 +1,12 @@ +module Clusters + class ClusterPolicy < BasePolicy + alias_method :cluster, :subject + + delegate { cluster.project } + + rule { can?(:master_access) }.policy do + enable :update_cluster + enable :admin_cluster + end + end +end diff --git a/app/policies/gcp/cluster_policy.rb b/app/policies/gcp/cluster_policy.rb deleted file mode 100644 index e77173ea6e1..00000000000 --- a/app/policies/gcp/cluster_policy.rb +++ /dev/null @@ -1,12 +0,0 @@ -module Gcp - class ClusterPolicy < BasePolicy - alias_method :cluster, :subject - - delegate { @subject.project } - - rule { can?(:master_access) }.policy do - enable :update_cluster - enable :admin_cluster - end - end -end diff --git a/app/presenters/clusters/cluster_presenter.rb b/app/presenters/clusters/cluster_presenter.rb new file mode 100644 index 00000000000..01cb59d0d44 --- /dev/null +++ b/app/presenters/clusters/cluster_presenter.rb @@ -0,0 +1,9 @@ +module Clusters + class ClusterPresenter < Gitlab::View::Presenter::Delegated + presents :cluster + + def gke_cluster_url + "https://console.cloud.google.com/kubernetes/clusters/details/#{provider.zone}/#{name}" if gcp? + end + end +end diff --git a/app/presenters/gcp/cluster_presenter.rb b/app/presenters/gcp/cluster_presenter.rb deleted file mode 100644 index f7908f92a37..00000000000 --- a/app/presenters/gcp/cluster_presenter.rb +++ /dev/null @@ -1,9 +0,0 @@ -module Gcp - class ClusterPresenter < Gitlab::View::Presenter::Delegated - presents :cluster - - def gke_cluster_url - "https://console.cloud.google.com/kubernetes/clusters/details/#{gcp_cluster_zone}/#{gcp_cluster_name}" - end - end -end diff --git a/app/services/clusters/create_service.rb b/app/services/clusters/create_service.rb index 5429bc21256..3f458e25c14 100644 --- a/app/services/clusters/create_service.rb +++ b/app/services/clusters/create_service.rb @@ -1,14 +1,39 @@ module Clusters class CreateService < BaseService - def execute(access_token) - params['gcp_machine_type'] ||= GoogleApi::CloudPlatform::Client::DEFAULT_MACHINE_TYPE + attr_reader :access_token - cluster_params = - params.merge(user: current_user) + def execute(access_token) + @access_token = access_token - project.create_cluster(cluster_params).tap do |cluster| + create_cluster.tap do |cluster| ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted? end end + + private + + def create_cluster + cluster = nil + + ActiveRecord::Base.transaction do + cluster = Clusters::Cluster.create!(cluster_params) + cluster.projects << project + end + + cluster + rescue ActiveRecord::RecordInvalid => e + e.record + end + + def cluster_params + return @cluster_params if defined?(@cluster_params) + + params[:provider_gcp_attributes][:machine_type] ||= + GoogleApi::CloudPlatform::Client::DEFAULT_MACHINE_TYPE + + params[:provider_gcp_attributes][:access_token] ||= access_token + + @cluster_params = params.merge(user: current_user) + end end end diff --git a/app/services/clusters/gcp/fetch_operation_service.rb b/app/services/clusters/gcp/fetch_operation_service.rb index 013225efac4..a4cd3ca5c11 100644 --- a/app/services/clusters/gcp/fetch_operation_service.rb +++ b/app/services/clusters/gcp/fetch_operation_service.rb @@ -3,13 +3,13 @@ module Clusters class FetchOperationService def execute(provider) operation = provider.api_client.projects_zones_operations( - provider.project_id, - provider.cluster_zone, + provider.gcp_project_id, + provider.zone, provider.operation_id) yield(operation) if block_given? rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e - return provider.make_errored!("Failed to request to CloudPlatform; #{e.message}") + provider.make_errored!("Failed to request to CloudPlatform; #{e.message}") end end end diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb index b536285b368..9f257bd0e9a 100644 --- a/app/services/clusters/gcp/finalize_creation_service.rb +++ b/app/services/clusters/gcp/finalize_creation_service.rb @@ -7,15 +7,14 @@ module Clusters @provider = provider configure_provider - configure_kubernetes_platform - request_kuberenetes_platform_token + configure_kubernetes ActiveRecord::Base.transaction do - kubernetes_platform.update! + kubernetes.save! provider.make_created! end rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e - return cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}") + cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}") rescue ActiveRecord::RecordInvalid => e cluster.make_errored!("Failed to configure GKE Cluster: #{e.message}") end @@ -26,23 +25,20 @@ module Clusters provider.endpoint = gke_cluster.endpoint end - def configure_kubernetes_platform - kubernetes_platform = cluster.kubernetes_platform - kubernetes_platform.api_url = 'https://' + endpoint - kubernetes_platform.ca_cert = Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate) - kubernetes_platform.username = gke_cluster.master_auth.username - kubernetes_platform.password = gke_cluster.master_auth.password + def configure_kubernetes + kubernetes.api_url = 'https://' + gke_cluster.endpoint + kubernetes.ca_cert = Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate) + kubernetes.username = gke_cluster.master_auth.username + kubernetes.password = gke_cluster.master_auth.password + kubernetes.token = request_kuberenetes_token end - def request_kuberenetes_platform_token - kubernetes_platform.read_secrets.each do |secret| + def request_kuberenetes_token + kubernetes.read_secrets.each do |secret| name = secret.dig('metadata', 'name') if /default-token/ =~ name token_base64 = secret.dig('data', 'token') - if token_base64 - kubernetes_platform.token = Base64.decode64(token_base64) - break - end + return Base64.decode64(token_base64) if token_base64 end end end @@ -50,16 +46,16 @@ module Clusters def gke_cluster @gke_cluster ||= provider.api_client.projects_zones_clusters_get( provider.gcp_project_id, - provider.gcp_cluster_zone, - provider.gcp_cluster_name) + provider.zone, + cluster.name) end def cluster - provider.cluster + @cluster ||= provider.cluster end - def kubernetes_platform - cluster.kubernetes_platform + def kubernetes + @kubernetes ||= cluster.platform_kubernetes end end end diff --git a/app/services/clusters/gcp/provision_service.rb b/app/services/clusters/gcp/provision_service.rb index 269705000ac..8ec81e22203 100644 --- a/app/services/clusters/gcp/provision_service.rb +++ b/app/services/clusters/gcp/provision_service.rb @@ -6,43 +6,41 @@ module Clusters def execute(provider) @provider = provider - unless operation.status == 'RUNNING' || operation.status == 'PENDING' - return provider.make_errored!("Operation status is unexpected; #{operation.status_message}") + get_operation_id do |operation_id| + if provider.make_creating(operation_id) + WaitForClusterCreationWorker.perform_in( + Clusters::Gcp::VerifyProvisionStatusService::INITIAL_INTERVAL, + provider.id) + else + provider.make_errored!("Failed to update provider record; #{provider.errors}") + end end + end - provider.operation_id = operation_id + private - unless provider.operation_id - return provider.make_errored!('Can not find operation_id from self_link') - end + def get_operation_id + operation = provider.api_client.projects_zones_clusters_create( + provider.gcp_project_id, + provider.zone, + provider.cluster.name, + provider.num_nodes, + machine_type: provider.machine_type) - if provider.make_creating - WaitForClusterCreationWorker.perform_in( - WaitForClusterCreationWorker::INITIAL_INTERVAL, provider.id) - else - return provider.make_errored!("Failed to update provider record; #{provider.errors}") + unless operation.status == 'PENDING' || operation.status == 'RUNNING' + return provider.make_errored!("Operation status is unexpected; #{operation.status_message}") end - rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e - return provider.make_errored!("Failed to request to CloudPlatform; #{e.message}") - end - private + operation_id = provider.api_client.parse_operation_id(operation.self_link) - def operation_id - api_client.parse_operation_id(operation.self_link) - end + unless operation_id + return provider.make_errored!('Can not find operation_id from self_link') + end - def operation - @operation ||= api_client.projects_zones_providers_create( - provider.project_id, - provider.provider_zone, - provider.provider_name, - provider.provider_size, - machine_type: provider.machine_type) - end + yield(operation_id) - def api_client - provider.api_client + rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e + provider.make_errored!("Failed to request to CloudPlatform; #{e.message}") end end end diff --git a/app/services/clusters/gcp/verify_provision_status_service.rb b/app/services/clusters/gcp/verify_provision_status_service.rb index 466ea986516..bc33756f27c 100644 --- a/app/services/clusters/gcp/verify_provision_status_service.rb +++ b/app/services/clusters/gcp/verify_provision_status_service.rb @@ -12,7 +12,7 @@ module Clusters request_operation do |operation| case operation.status - when 'RUNNING' + when 'PENDING', 'RUNNING' continue_creation(operation) when 'DONE' finalize_creation @@ -25,11 +25,15 @@ module Clusters private def continue_creation(operation) - if TIMEOUT < Time.now.utc - operation.start_time.to_time.utc - return provider.make_errored!("Cluster creation time exceeds timeout; #{TIMEOUT}") + if elapsed_time_from_creation(operation) < TIMEOUT + WaitForClusterCreationWorker.perform_in(EAGER_INTERVAL, provider.cluster_id) + else + provider.make_errored!("Cluster creation time exceeds timeout; #{TIMEOUT}") end + end - WaitForClusterCreationWorker.perform_in(EAGER_INTERVAL, provider.cluster_id) + def elapsed_time_from_creation(operation) + Time.now.utc - operation.start_time.to_time.utc end def finalize_creation @@ -37,7 +41,7 @@ module Clusters end def request_operation(&blk) - Clusters::FetchGcpOperationService.new.execute(provider, &blk) + Clusters::Gcp::FetchOperationService.new.execute(provider, &blk) end end end diff --git a/app/validators/cluster_name_validator.rb b/app/validators/cluster_name_validator.rb new file mode 100644 index 00000000000..6c9850af30f --- /dev/null +++ b/app/validators/cluster_name_validator.rb @@ -0,0 +1,24 @@ +# ClusterNameValidator +# +# Custom validator for ClusterName. +class ClusterNameValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + if record.user? + unless value.present? + record.errors.add(attribute, " has to be present") + end + elsif record.gcp? + if record.persisted? && record.name != value + record.errors.add(attribute, " can not be changed because it's synchronized with provider") + end + + unless value.length >= 1 && value.length <= 63 + record.errors.add(attribute, " is invalid syntax") + end + + unless value =~ Gitlab::Regex.kubernetes_namespace_regex + record.errors.add(attribute, Gitlab::Regex.kubernetes_namespace_regex_message) + end + end + end +end diff --git a/app/views/projects/clusters/_form.html.haml b/app/views/projects/clusters/_form.html.haml index 371cdb1e403..b3020513abf 100644 --- a/app/views/projects/clusters/_form.html.haml +++ b/app/views/projects/clusters/_form.html.haml @@ -4,34 +4,38 @@ - link_to_help_page = link_to(s_('ClusterIntegration|help page'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer') = s_('ClusterIntegration|Read our %{link_to_help_page} on cluster integration.').html_safe % { link_to_help_page: link_to_help_page} - = form_for [@project.namespace.becomes(Namespace), @project, @cluster] do |field| + = form_for @cluster, url: namespace_project_clusters_path(@project.namespace, @project, @cluster), as: :cluster do |field| + = field.hidden_field :platform_type, :value => 'kubernetes' + = field.hidden_field :provider_type, :value => 'gcp' = form_errors(@cluster) .form-group - = field.label :gcp_cluster_name, s_('ClusterIntegration|Cluster name') - = field.text_field :gcp_cluster_name, class: 'form-control' + = field.label :name, s_('ClusterIntegration|Cluster name') + = field.text_field :name, class: 'form-control' - .form-group - = field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project ID') - = link_to(s_('ClusterIntegration|See your projects'), 'https://console.cloud.google.com/home/dashboard', target: '_blank', rel: 'noopener noreferrer') - = field.text_field :gcp_project_id, class: 'form-control' + = field.fields_for :provider_gcp, @cluster.provider_gcp do |provider_gcp_field| + .form-group + = provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project ID') + = link_to(s_('ClusterIntegration|See your projects'), 'https://console.cloud.google.com/home/dashboard', target: '_blank', rel: 'noopener noreferrer') + = provider_gcp_field.text_field :gcp_project_id, class: 'form-control' - .form-group - = field.label :gcp_cluster_zone, s_('ClusterIntegration|Zone') - = link_to(s_('ClusterIntegration|See zones'), 'https://cloud.google.com/compute/docs/regions-zones/regions-zones', target: '_blank', rel: 'noopener noreferrer') - = field.text_field :gcp_cluster_zone, class: 'form-control', placeholder: 'us-central1-a' + .form-group + = provider_gcp_field.label :zone, s_('ClusterIntegration|Zone') + = link_to(s_('ClusterIntegration|See zones'), 'https://cloud.google.com/compute/docs/regions-zones/regions-zones', target: '_blank', rel: 'noopener noreferrer') + = provider_gcp_field.text_field :zone, class: 'form-control', placeholder: 'us-central1-a' - .form-group - = field.label :gcp_cluster_size, s_('ClusterIntegration|Number of nodes') - = field.text_field :gcp_cluster_size, class: 'form-control', placeholder: '3' + .form-group + = provider_gcp_field.label :num_nodes, s_('ClusterIntegration|Number of nodes') + = provider_gcp_field.text_field :num_nodes, class: 'form-control', placeholder: '3' - .form-group - = field.label :gcp_machine_type, s_('ClusterIntegration|Machine type') - = link_to(s_('ClusterIntegration|See machine types'), 'https://cloud.google.com/compute/docs/machine-types', target: '_blank', rel: 'noopener noreferrer') - = field.text_field :gcp_machine_type, class: 'form-control', placeholder: 'n1-standard-4' + .form-group + = provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type') + = link_to(s_('ClusterIntegration|See machine types'), 'https://cloud.google.com/compute/docs/machine-types', target: '_blank', rel: 'noopener noreferrer') + = provider_gcp_field.text_field :machine_type, class: 'form-control', placeholder: 'n1-standard-4' + = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field| .form-group - = field.label :project_namespace, s_('ClusterIntegration|Project namespace (optional, unique)') - = field.text_field :project_namespace, class: 'form-control', placeholder: @cluster.project_namespace_placeholder + = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)') + = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: @cluster.platform_kubernetes.default_namespace(@project) .form-group = field.submit s_('ClusterIntegration|Create cluster'), class: 'btn btn-save' diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml index ff76abc3553..49adae82454 100644 --- a/app/views/projects/clusters/show.html.haml +++ b/app/views/projects/clusters/show.html.haml @@ -33,7 +33,7 @@ - else = s_('ClusterIntegration|Cluster integration is disabled for this project.') - = form_for [@project.namespace.becomes(Namespace), @project, @cluster] do |field| + = form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field| = form_errors(@cluster) .form-group.append-bottom-20 %label.append-bottom-10 @@ -62,9 +62,9 @@ %label.append-bottom-10{ for: 'cluter-name' } = s_('ClusterIntegration|Cluster name') .input-group - %input.form-control.cluster-name{ value: @cluster.gcp_cluster_name, disabled: true } + %input.form-control.cluster-name{ value: @cluster.name, disabled: true } %span.input-group-addon.clipboard-addon - = clipboard_button(text: @cluster.gcp_cluster_name, title: s_('ClusterIntegration|Copy cluster name')) + = clipboard_button(text: @cluster.name, title: s_('ClusterIntegration|Copy cluster name')) %section.settings#js-cluster-advanced-settings .settings-header diff --git a/app/workers/cluster_provision_worker.rb b/app/workers/cluster_provision_worker.rb index 79f0d73c396..0929fffc444 100644 --- a/app/workers/cluster_provision_worker.rb +++ b/app/workers/cluster_provision_worker.rb @@ -4,7 +4,7 @@ class ClusterProvisionWorker def perform(cluster_id) Clusters::Cluster.find_by_id(cluster_id).try do |cluster| - cluster.gcp_provider.try do |provider| + cluster.provider_gcp.try do |provider| Clusters::Gcp::ProvisionService.new.execute(provider) end end diff --git a/app/workers/wait_for_cluster_creation_worker.rb b/app/workers/wait_for_cluster_creation_worker.rb index d8c42c6bd55..b2f04869636 100644 --- a/app/workers/wait_for_cluster_creation_worker.rb +++ b/app/workers/wait_for_cluster_creation_worker.rb @@ -4,7 +4,7 @@ class WaitForClusterCreationWorker def perform(cluster_id) Clusters::Cluster.find_by_id(cluster_id).try do |cluster| - cluster.gcp_provider.try do |provider| + cluster.provider_gcp.try do |provider| Clusters::Gcp::VerifyProvisionStatusService.new.execute(provider) end end -- cgit v1.2.1 From 9101cce2a7c30a832b3d97d84cd53ca503a163d1 Mon Sep 17 00:00:00 2001 From: Simon Knox Date: Tue, 24 Oct 2017 18:58:05 +0300 Subject: don't re-run smart interval callback if there is already one in progress because some things take time --- app/assets/javascripts/smart_interval.js | 22 ++++++++++++++++++---- .../vue_merge_request_widget/mr_widget_options.js | 4 ++-- 2 files changed, 20 insertions(+), 6 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/smart_interval.js b/app/assets/javascripts/smart_interval.js index 2bf7a3a5d61..cee2d2ba1e2 100644 --- a/app/assets/javascripts/smart_interval.js +++ b/app/assets/javascripts/smart_interval.js @@ -42,13 +42,16 @@ class SmartInterval { const cfg = this.cfg; const state = this.state; - if (cfg.immediateExecution) { + if (cfg.immediateExecution && !this.isLoading) { cfg.immediateExecution = false; - cfg.callback(); + this.triggerCallback(); } state.intervalId = window.setInterval(() => { - cfg.callback(); + if (this.isLoading) { + return; + } + this.triggerCallback(); if (this.getCurrentInterval() === cfg.maxInterval) { return; @@ -76,7 +79,7 @@ class SmartInterval { // start a timer, using the existing interval resume() { - this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped + this.stopTimer(); // stop existing timer, in case timer was not previously stopped this.start(); } @@ -104,6 +107,17 @@ class SmartInterval { this.initPageUnloadHandling(); } + triggerCallback() { + this.isLoading = true; + this.cfg.callback() + .then(() => { + this.isLoading = false; + }) + .catch(() => { + this.isLoading = false; + }); + } + initVisibilityChangeHandling() { // cancel interval when tab no longer shown (prevents cached pages from polling) document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this)); diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js index 4f497b204a3..45bb9111b9b 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js @@ -81,7 +81,7 @@ export default { return new MRWidgetService(endpoints); }, checkStatus(cb) { - this.service.checkStatus() + return this.service.checkStatus() .then(res => res.json()) .then((res) => { this.handleNotification(res); @@ -121,7 +121,7 @@ export default { } }, fetchDeployments() { - this.service.fetchDeployments() + return this.service.fetchDeployments() .then(res => res.json()) .then((res) => { if (res.length) { -- cgit v1.2.1 From 5754908fee556062ab2e42a418bf192a0bf56402 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 27 Oct 2017 16:48:02 +0100 Subject: Adds buttons to the first page. Moves first page into a new one --- app/controllers/projects/clusters_controller.rb | 7 ++++++- app/views/projects/clusters/new.html.haml | 11 +++++++++-- app/views/projects/clusters/new_kubernetes_form.html.haml | 9 +++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 app/views/projects/clusters/new_kubernetes_form.html.haml (limited to 'app') diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb index 03019b0becc..95e2f6a1257 100644 --- a/app/controllers/projects/clusters_controller.rb +++ b/app/controllers/projects/clusters_controller.rb @@ -26,10 +26,15 @@ class Projects::ClustersController < Projects::ApplicationController end end - def new + # TODO fix this + def new_kubernetes_form @cluster = project.build_cluster end + def new + # First page! + end + def create @cluster = Ci::CreateClusterService .new(project, current_user, create_params) diff --git a/app/views/projects/clusters/new.html.haml b/app/views/projects/clusters/new.html.haml index c538d41ffad..3963e4741f3 100644 --- a/app/views/projects/clusters/new.html.haml +++ b/app/views/projects/clusters/new.html.haml @@ -5,5 +5,12 @@ .col-sm-4 = render 'sidebar' .col-sm-8 - = render 'header' -= render 'form' + %h4= s_('ClusterIntegration|Choose how to set up cluster integration') + + %p= s_('ClusterIntegration| Create a new cluster on Google Engine right from GitLab') + -# TODO FIX PATH + = link_to s_('ClusterIntegration|Create on GKE'), providers_gcp_login_project_clusters_path(@project), class: 'btn append-bottom-20' + -# TODO FIX PATH + %p= s_('ClusterIntegration| Enter the details for an existing Kubernetes cluster') + = link_to s_('ClusterIntegration|Add an existing cluster'), project_settings_integrations_path(@project), class: 'btn append-bottom-20' + diff --git a/app/views/projects/clusters/new_kubernetes_form.html.haml b/app/views/projects/clusters/new_kubernetes_form.html.haml new file mode 100644 index 00000000000..c538d41ffad --- /dev/null +++ b/app/views/projects/clusters/new_kubernetes_form.html.haml @@ -0,0 +1,9 @@ +- breadcrumb_title "Cluster" +- page_title _("New Cluster") + +.row.prepend-top-default + .col-sm-4 + = render 'sidebar' + .col-sm-8 + = render 'header' += render 'form' -- cgit v1.2.1 From 478e59fe8d82b99800a2613aa4d153bf692fbd6b Mon Sep 17 00:00:00 2001 From: Shinya Maeda Date: Mon, 30 Oct 2017 03:48:45 +0900 Subject: specs for models. Improved details. --- app/models/clusters/cluster.rb | 26 +++++++-------- app/models/clusters/platforms/kubernetes.rb | 50 ++++++++++++++++------------- app/models/clusters/project.rb | 4 +-- app/models/clusters/providers/gcp.rb | 3 +- app/validators/cluster_name_validator.rb | 2 +- app/views/projects/clusters/_form.html.haml | 2 +- 6 files changed, 47 insertions(+), 40 deletions(-) (limited to 'app') diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index f1eedad8795..4260fadb46d 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -6,15 +6,6 @@ module Clusters belongs_to :user - enum platform_type: { - kubernetes: 1 - } - - enum provider_type: { - user: 0, - gcp: 1 - } - has_many :cluster_projects, class_name: 'Clusters::Project' has_many :projects, through: :cluster_projects, class_name: '::Project' @@ -32,6 +23,18 @@ module Clusters delegate :status_name, to: :provider, allow_nil: true delegate :on_creation?, to: :provider, allow_nil: true + enum platform_type: { + kubernetes: 1 + } + + enum provider_type: { + user: 0, + gcp: 1 + } + + scope :enabled, -> { where(enabled: true) } + scope :disabled, -> { where(enabled: false) } + def provider return provider_gcp if gcp? end @@ -40,15 +43,12 @@ module Clusters return platform_kubernetes if kubernetes? end - def project - first_project - end - def first_project return @first_project if defined?(@first_project) @first_project = projects.first end + alias_method :project, :first_project private diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index d9f8927f7cc..b20b00ff51b 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -1,13 +1,11 @@ module Clusters module Platforms class Kubernetes < ActiveRecord::Base + include Gitlab::CurrentSettings include Gitlab::Kubernetes include ReactiveCaching self.table_name = 'cluster_platforms_kubernetes' - - TEMPLATE_PLACEHOLDER = 'Kubernetes namespace'.freeze - self.reactive_cache_key = ->(kubernetes) { [kubernetes.class.model_name.singular, kubernetes.cluster_id] } belongs_to :cluster, inverse_of: :platform_kubernetes, class_name: 'Clusters::Cluster' @@ -22,6 +20,8 @@ module Clusters key: Gitlab::Application.secrets.db_key_base, algorithm: 'aes-256-cbc' + before_validation :enforce_namespace_to_lower_case + validates :namespace, allow_blank: true, length: 1..63, @@ -34,8 +34,19 @@ module Clusters validates :token, presence: true, on: :update after_save :clear_reactive_cache! - - before_validation :enforce_namespace_to_lower_case + + alias_attribute :ca_pem, :ca_cert + + delegate :project, to: :cluster, allow_nil: true + delegate :enabled?, to: :cluster, allow_nil: true + + alias_method :active?, :enabled? + + class << self + def namespace_for_project(project) + "#{project.path}-#{project.id}" + end + end def actual_namespace if namespace.present? @@ -45,6 +56,10 @@ module Clusters end end + def default_namespace + self.class.namespace_for_project(project) if project + end + def predefined_variables config = YAML.dump(kubeconfig) @@ -55,9 +70,9 @@ module Clusters { key: 'KUBECONFIG', value: config, public: false, file: true } ] - if ca_cert.present? - variables << { key: 'KUBE_CA_PEM', value: ca_cert, public: true } - variables << { key: 'KUBE_CA_PEM_FILE', value: ca_cert, public: true, file: true } + if ca_pem.present? + variables << { key: 'KUBE_CA_PEM', value: ca_pem, public: true } + variables << { key: 'KUBE_CA_PEM_FILE', value: ca_pem, public: true, file: true } end variables @@ -78,7 +93,7 @@ module Clusters # Caches resources in the namespace so other calls don't need to block on # network access def calculate_reactive_cache - return unless active? && cluster.project && !cluster.project.pending_delete? + return unless active? && project && !project.pending_delete? # We may want to cache extra things in the future { pods: read_pods } @@ -89,16 +104,7 @@ module Clusters url: api_url, namespace: actual_namespace, token: token, - ca_pem: ca_cert) - end - - def namespace_placeholder - default_namespace || TEMPLATE_PLACEHOLDER - end - - def default_namespace(project = nil) - project ||= cluster&.project - "#{project.path}-#{project.id}" if project + ca_pem: ca_pem) end def read_secrets @@ -123,9 +129,9 @@ module Clusters def kubeclient_ssl_options opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER } - if ca_cert.present? + if ca_pem.present? opts[:cert_store] = OpenSSL::X509::Store.new - opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_cert)) + opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem)) end opts @@ -166,7 +172,7 @@ module Clusters def terminal_auth { token: token, - ca_pem: ca_cert, + ca_pem: ca_pem, max_session_time: current_application_settings.terminal_max_session_time } end diff --git a/app/models/clusters/project.rb b/app/models/clusters/project.rb index 69088100420..eeb734b20b8 100644 --- a/app/models/clusters/project.rb +++ b/app/models/clusters/project.rb @@ -2,7 +2,7 @@ module Clusters class Project < ActiveRecord::Base self.table_name = 'cluster_projects' - belongs_to :cluster, inverse_of: :projects, class_name: 'Clusters::Cluster' - belongs_to :project, inverse_of: :project, class_name: 'Project' + belongs_to :cluster, class_name: 'Clusters::Cluster' + belongs_to :project, class_name: '::Project' end end diff --git a/app/models/clusters/providers/gcp.rb b/app/models/clusters/providers/gcp.rb index e4f109d2794..7700ba86f1a 100644 --- a/app/models/clusters/providers/gcp.rb +++ b/app/models/clusters/providers/gcp.rb @@ -55,7 +55,8 @@ module Clusters before_transition any => [:creating] do |provider, transition| operation_id = transition.args.first - provider.operation_id = operation_id if operation_id + raise 'operation_id is required' unless operation_id + provider.operation_id = operation_id end before_transition any => [:errored] do |provider, transition| diff --git a/app/validators/cluster_name_validator.rb b/app/validators/cluster_name_validator.rb index 6c9850af30f..13ec342f399 100644 --- a/app/validators/cluster_name_validator.rb +++ b/app/validators/cluster_name_validator.rb @@ -8,7 +8,7 @@ class ClusterNameValidator < ActiveModel::EachValidator record.errors.add(attribute, " has to be present") end elsif record.gcp? - if record.persisted? && record.name != value + if record.persisted? && record.name_changed? record.errors.add(attribute, " can not be changed because it's synchronized with provider") end diff --git a/app/views/projects/clusters/_form.html.haml b/app/views/projects/clusters/_form.html.haml index b3020513abf..6b9f63b7515 100644 --- a/app/views/projects/clusters/_form.html.haml +++ b/app/views/projects/clusters/_form.html.haml @@ -35,7 +35,7 @@ = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field| .form-group = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)') - = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: @cluster.platform_kubernetes.default_namespace(@project) + = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: Clusters::Platforms::Kubernetes.namespace_for_project(@project) .form-group = field.submit s_('ClusterIntegration|Create cluster'), class: 'btn btn-save' -- cgit v1.2.1 From d6744d98384192799c9b3a97ad0eaf69cb4d25ee Mon Sep 17 00:00:00 2001 From: Shinya Maeda Date: Mon, 30 Oct 2017 21:55:18 +0900 Subject: specs for services. Improved details. --- app/models/clusters/cluster.rb | 2 +- app/models/clusters/platforms/kubernetes.rb | 3 --- app/services/clusters/gcp/finalize_creation_service.rb | 8 ++++++-- 3 files changed, 7 insertions(+), 6 deletions(-) (limited to 'app') diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 4260fadb46d..7af56adb613 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -13,7 +13,7 @@ module Clusters has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes' accepts_nested_attributes_for :provider_gcp - accepts_nested_attributes_for :platform_kubernetes + accepts_nested_attributes_for :platform_kubernetes, update_only: true validates :name, cluster_name: true validate :restrict_modification, on: :update diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index b20b00ff51b..1a4e293be65 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -111,9 +111,6 @@ module Clusters kubeclient = build_kubeclient! kubeclient.get_secrets.as_json - rescue KubeException => err - raise err unless err.error_code == 404 - [] end # Returns a hash of all pods in the namespace diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb index 9f257bd0e9a..d379870924a 100644 --- a/app/services/clusters/gcp/finalize_creation_service.rb +++ b/app/services/clusters/gcp/finalize_creation_service.rb @@ -14,9 +14,11 @@ module Clusters provider.make_created! end rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e - cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}") + provider.make_errored!("Failed to request to CloudPlatform; #{e.message}") + rescue KubeException => e + provider.make_errored!("Failed to request to Kubernetes; #{e.message}") rescue ActiveRecord::RecordInvalid => e - cluster.make_errored!("Failed to configure GKE Cluster: #{e.message}") + provider.make_errored!("Failed to configure GKE Cluster: #{e.message}") end private @@ -41,6 +43,8 @@ module Clusters return Base64.decode64(token_base64) if token_base64 end end + + nil end def gke_cluster -- cgit v1.2.1 From 4934f6078b12072fd62f8065a1b25d961aa2d825 Mon Sep 17 00:00:00 2001 From: Shinya Maeda Date: Tue, 31 Oct 2017 00:18:15 +0900 Subject: specs for workers. --- app/workers/cluster_provision_worker.rb | 4 ++-- app/workers/wait_for_cluster_creation_worker.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) (limited to 'app') diff --git a/app/workers/cluster_provision_worker.rb b/app/workers/cluster_provision_worker.rb index 0929fffc444..b01f9708424 100644 --- a/app/workers/cluster_provision_worker.rb +++ b/app/workers/cluster_provision_worker.rb @@ -4,8 +4,8 @@ class ClusterProvisionWorker def perform(cluster_id) Clusters::Cluster.find_by_id(cluster_id).try do |cluster| - cluster.provider_gcp.try do |provider| - Clusters::Gcp::ProvisionService.new.execute(provider) + cluster.provider.try do |provider| + Clusters::Gcp::ProvisionService.new.execute(provider) if cluster.gcp? end end end diff --git a/app/workers/wait_for_cluster_creation_worker.rb b/app/workers/wait_for_cluster_creation_worker.rb index b2f04869636..241ed3901dc 100644 --- a/app/workers/wait_for_cluster_creation_worker.rb +++ b/app/workers/wait_for_cluster_creation_worker.rb @@ -4,8 +4,8 @@ class WaitForClusterCreationWorker def perform(cluster_id) Clusters::Cluster.find_by_id(cluster_id).try do |cluster| - cluster.provider_gcp.try do |provider| - Clusters::Gcp::VerifyProvisionStatusService.new.execute(provider) + cluster.provider.try do |provider| + Clusters::Gcp::VerifyProvisionStatusService.new.execute(provider) if cluster.gcp? end end end -- cgit v1.2.1 From 6a65e2f5f94781a69f3f7fb329483ead6bc81fd9 Mon Sep 17 00:00:00 2001 From: Shinya Maeda Date: Tue, 31 Oct 2017 17:47:48 +0900 Subject: specs for controller. Improved validation --- app/controllers/projects/clusters_controller.rb | 15 ++++++++------- app/models/clusters/cluster.rb | 6 ++++-- app/models/clusters/platforms/kubernetes.rb | 4 ++-- app/services/clusters/create_service.rb | 8 ++++---- 4 files changed, 18 insertions(+), 15 deletions(-) (limited to 'app') diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb index c07d955f148..b204bd17eec 100644 --- a/app/controllers/projects/clusters_controller.rb +++ b/app/controllers/projects/clusters_controller.rb @@ -27,13 +27,14 @@ class Projects::ClustersController < Projects::ApplicationController end def new - @cluster = Clusters::Cluster.new( - platform_type: :kubernetes, - provider_type: :gcp).tap do |cluster| - cluster.build_provider_gcp - cluster.build_platform_kubernetes - cluster.projects << project - end + # @cluster = Clusters::Cluster.new( + # platform_type: :kubernetes, + # provider_type: :gcp).tap do |cluster| + # cluster.build_provider_gcp + # cluster.build_platform_kubernetes + # cluster.projects << project + # end + @cluster = Clusters::Cluster.new end def create diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 7af56adb613..091c91e3fb9 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -10,11 +10,13 @@ module Clusters has_many :projects, through: :cluster_projects, class_name: '::Project' has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp' - has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes' + has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes', validate: { if: :update } - accepts_nested_attributes_for :provider_gcp + accepts_nested_attributes_for :provider_gcp, update_only: true accepts_nested_attributes_for :platform_kubernetes, update_only: true + validates :provider_type, presence: true + validates :platform_type, presence: true validates :name, cluster_name: true validate :restrict_modification, on: :update diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index 1a4e293be65..52022509d49 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -30,8 +30,8 @@ module Clusters message: Gitlab::Regex.kubernetes_namespace_regex_message } - validates :api_url, url: true, presence: true, on: :update - validates :token, presence: true, on: :update + validates :api_url, url: true, presence: true + validates :token, presence: true after_save :clear_reactive_cache! diff --git a/app/services/clusters/create_service.rb b/app/services/clusters/create_service.rb index 3f458e25c14..94b889895ba 100644 --- a/app/services/clusters/create_service.rb +++ b/app/services/clusters/create_service.rb @@ -28,10 +28,10 @@ module Clusters def cluster_params return @cluster_params if defined?(@cluster_params) - params[:provider_gcp_attributes][:machine_type] ||= - GoogleApi::CloudPlatform::Client::DEFAULT_MACHINE_TYPE - - params[:provider_gcp_attributes][:access_token] ||= access_token + params[:provider_gcp_attributes].try do |h| + h[:machine_type] ||= GoogleApi::CloudPlatform::Client::DEFAULT_MACHINE_TYPE + h[:access_token] ||= access_token + end @cluster_params = params.merge(user: current_user) end -- cgit v1.2.1 From 84f5aaa729d6286252602800a1f9e1bf1e5b47d3 Mon Sep 17 00:00:00 2001 From: Alessio Caiazza Date: Tue, 31 Oct 2017 11:29:56 +0100 Subject: WIP: mock cluster apps status API --- app/serializers/cluster_entity.rb | 12 ++++++++++++ app/serializers/cluster_serializer.rb | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/serializers/cluster_entity.rb b/app/serializers/cluster_entity.rb index 08a113c4d8a..84ce34afb32 100644 --- a/app/serializers/cluster_entity.rb +++ b/app/serializers/cluster_entity.rb @@ -3,4 +3,16 @@ class ClusterEntity < Grape::Entity expose :status_name, as: :status expose :status_reason + expose :applications do |cluster, options| + if cluster.created? + { + helm: { status: 'installed' }, + ingress: { status: 'error', status_reason: 'Missing namespace' }, + runner: { status: 'installing' }, + prometheus: { status: 'installable' } + } + else + {} + end + end end diff --git a/app/serializers/cluster_serializer.rb b/app/serializers/cluster_serializer.rb index 2c87202a105..2e13c1501e7 100644 --- a/app/serializers/cluster_serializer.rb +++ b/app/serializers/cluster_serializer.rb @@ -2,6 +2,6 @@ class ClusterSerializer < BaseSerializer entity ClusterEntity def represent_status(resource) - represent(resource, { only: [:status, :status_reason] }) + represent(resource, { only: [:status, :status_reason, :applications] }) end end -- cgit v1.2.1 From 6fbd430fbecc5a4d086a81d5d50ce023165b1c51 Mon Sep 17 00:00:00 2001 From: Oswaldo Ferreira Date: Mon, 23 Oct 2017 19:03:28 +0300 Subject: Initial CE backport for saved issue board --- app/helpers/boards_helper.rb | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) (limited to 'app') diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb index 7112c6ee470..2b4e89c50dd 100644 --- a/app/helpers/boards_helper.rb +++ b/app/helpers/boards_helper.rb @@ -21,13 +21,8 @@ module BoardsHelper end def current_board_json - board = @board || @boards.first - board.to_json( - only: [:id, :name, :milestone_id], - include: { - milestone: { only: [:title] } - } + only: [:id, :name, :milestone_id] ) end -- cgit v1.2.1 From 18db15244813c4ff1286e1297c9da42798b61b99 Mon Sep 17 00:00:00 2001 From: Oswaldo Ferreira Date: Tue, 24 Oct 2017 16:03:46 +0300 Subject: Remove unnecessary helper method --- app/helpers/boards_helper.rb | 6 ------ 1 file changed, 6 deletions(-) (limited to 'app') diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb index 2b4e89c50dd..c4a621160af 100644 --- a/app/helpers/boards_helper.rb +++ b/app/helpers/boards_helper.rb @@ -20,12 +20,6 @@ module BoardsHelper project_issues_path(@project) end - def current_board_json - board.to_json( - only: [:id, :name, :milestone_id] - ) - end - def board_base_url project_boards_path(@project) end -- cgit v1.2.1 From b04b3d1de783ea70ca81c5f80c86d656ee6d82ea Mon Sep 17 00:00:00 2001 From: Simon Knox Date: Tue, 24 Oct 2017 16:48:41 +0300 Subject: apply changes for JS and CSS from gitlab-ee!2912 https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2912/ --- .../javascripts/boards/filtered_search_boards.js | 9 ++- .../javascripts/boards/stores/boards_store.js | 8 +- .../javascripts/filtered_search/dropdown_utils.js | 10 +++ .../filtered_search/filtered_search_manager.js | 10 +-- .../filtered_search_visual_tokens.js | 15 +--- app/assets/javascripts/labels_select.js | 13 ++- app/assets/javascripts/milestone_select.js | 17 ++-- app/assets/javascripts/users_select.js | 7 +- .../vue_shared/components/loading_icon.vue | 8 +- .../vue_shared/components/popup_dialog.vue | 93 +++++++++++++++++----- app/assets/stylesheets/framework/common.scss | 3 + app/assets/stylesheets/framework/dropdowns.scss | 1 + app/assets/stylesheets/framework/modal.scss | 8 ++ .../framework/tw_bootstrap_variables.scss | 33 ++++++++ app/assets/stylesheets/pages/repo.scss | 13 --- 15 files changed, 183 insertions(+), 65 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js index 3f083655f95..184665f395c 100644 --- a/app/assets/javascripts/boards/filtered_search_boards.js +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -11,7 +11,8 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { // Issue boards is slightly different, we handle all the requests async // instead or reloading the page, we just re-fire the list ajax requests this.isHandledAsync = true; - this.cantEdit = cantEdit; + this.cantEdit = cantEdit.filter(i => typeof i === 'string'); + this.cantEditWithValue = cantEdit.filter(i => typeof i === 'object'); } updateObject(path) { @@ -42,7 +43,9 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { this.filteredSearchInput.dispatchEvent(new Event('input')); } - canEdit(tokenName) { - return this.cantEdit.indexOf(tokenName) === -1; + canEdit(tokenName, tokenValue) { + if (this.cantEdit.includes(tokenName)) return false; + return this.cantEditWithValue.findIndex(token => token.name === tokenName && + token.value === tokenValue) === -1; } } diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index ea82958e80d..798d7e0d147 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -14,16 +14,18 @@ gl.issueBoards.BoardsStore = { }, state: {}, detail: { - issue: {} + issue: {}, }, moving: { issue: {}, - list: {} + list: {}, }, create () { this.state.lists = []; this.filter.path = getUrlParamsArray().join('&'); - this.detail = { issue: {} }; + this.detail = { + issue: {}, + }; }, addList (listObj, defaultAvatar) { const list = new List(listObj, defaultAvatar); diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js index 8d711e3213c..cf8a9b0402b 100644 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js @@ -147,6 +147,16 @@ class DropdownUtils { return dataValue !== null; } + static getVisualTokenValues(visualToken) { + const tokenName = visualToken && visualToken.querySelector('.name').textContent.trim(); + let tokenValue = visualToken && visualToken.querySelector('.value') && visualToken.querySelector('.value').textContent.trim(); + if (tokenName === 'label' && tokenValue) { + // remove leading symbol and wrapping quotes + tokenValue = tokenValue.replace(/^~("|')?(.*)/, '$2').replace(/("|')$/, ''); + } + return { tokenName, tokenValue }; + } + // Determines the full search query (visual tokens + input) static getSearchQuery(untilInput = false) { const container = FilteredSearchContainer.container; diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 7b233842d5a..69c57f923b6 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -185,8 +185,8 @@ class FilteredSearchManager { if (e.keyCode === 8 || e.keyCode === 46) { const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); - const sanitizedTokenName = lastVisualToken && lastVisualToken.querySelector('.name').textContent.trim(); - const canEdit = sanitizedTokenName && this.canEdit && this.canEdit(sanitizedTokenName); + const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(lastVisualToken); + const canEdit = tokenName && this.canEdit && this.canEdit(tokenName, tokenValue); if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) { this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial(); gl.FilteredSearchVisualTokens.removeLastTokenPartial(); @@ -336,8 +336,8 @@ class FilteredSearchManager { let canClearToken = t.classList.contains('js-visual-token'); if (canClearToken) { - const tokenKey = t.querySelector('.name').textContent.trim(); - canClearToken = this.canEdit && this.canEdit(tokenKey); + const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(t); + canClearToken = this.canEdit && this.canEdit(tokenName, tokenValue); } if (canClearToken) { @@ -469,7 +469,7 @@ class FilteredSearchManager { } hasFilteredSearch = true; - const canEdit = this.canEdit && this.canEdit(sanitizedKey); + const canEdit = this.canEdit && this.canEdit(sanitizedKey, sanitizedValue); gl.FilteredSearchVisualTokens.addFilterVisualToken( sanitizedKey, `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`, diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js index d2f92929b8a..6139e81fe6d 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js +++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js @@ -38,21 +38,14 @@ class FilteredSearchVisualTokens { } static createVisualTokenElementHTML(canEdit = true) { - let removeTokenMarkup = ''; - if (canEdit) { - removeTokenMarkup = ` -
- -
- `; - } - return ` -
+
- ${removeTokenMarkup} +
+ +
`; diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 84602cf9207..1e52963b1dd 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -8,7 +8,7 @@ import CreateLabelDropdown from './create_label'; (function() { this.LabelsSelect = (function() { - function LabelsSelect(els) { + function LabelsSelect(els, options = {}) { var _this, $els; _this = this; @@ -58,6 +58,7 @@ import CreateLabelDropdown from './create_label'; labelHTMLTemplate = _.template('<% _.each(labels, function(label){ %> issues?label_name[]=<%- encodeURIComponent(label.title) %>"> <%- label.title %> <% }); %>'); labelNoneHTMLTemplate = 'None'; } + const handleClick = options.handleClick; $sidebarLabelTooltip.tooltip(); @@ -316,9 +317,9 @@ import CreateLabelDropdown from './create_label'; }, multiSelect: $dropdown.hasClass('js-multiselect'), vue: $dropdown.hasClass('js-issue-board-sidebar'), - clicked: function(options) { - const { $el, e, isMarking } = options; - const label = options.selectedObj; + clicked: function(clickEvent) { + const { $el, e, isMarking } = clickEvent; + const label = clickEvent.selectedObj; var isIssueIndex, isMRIndex, page, boardsModel; var fadeOutLoader = () => { @@ -391,6 +392,10 @@ import CreateLabelDropdown from './create_label'; .then(fadeOutLoader) .catch(fadeOutLoader); } + else if (handleClick) { + e.preventDefault(); + handleClick(label); + } else { if ($dropdown.hasClass('js-multiselect')) { diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index e7d5325a509..74e5a4f1cea 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -5,7 +5,7 @@ import _ from 'underscore'; (function() { this.MilestoneSelect = (function() { - function MilestoneSelect(currentProject, els) { + function MilestoneSelect(currentProject, els, options = {}) { var _this, $els; if (currentProject != null) { _this = this; @@ -136,19 +136,26 @@ import _ from 'underscore'; }, opened: function(e) { const $el = $(e.currentTarget); - if ($dropdown.hasClass('js-issue-board-sidebar')) { + if ($dropdown.hasClass('js-issue-board-sidebar') || options.handleClick) { selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault; } $('a.is-active', $el).removeClass('is-active'); $(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active'); }, vue: $dropdown.hasClass('js-issue-board-sidebar'), - clicked: function(options) { - const { $el, e } = options; - let selected = options.selectedObj; + clicked: function(clickEvent) { + const { $el, e } = clickEvent; + let selected = clickEvent.selectedObj; var data, isIssueIndex, isMRIndex, isSelecting, page, boardsStore; if (!selected) return; + + if (options.handleClick) { + e.preventDefault(); + options.handleClick(selected); + return; + } + page = $('body').attr('data-page'); isIssueIndex = page === 'projects:issues:index'; isMRIndex = (page === page && page === 'projects:merge_requests:index'); diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index a0883b32593..759cc9925f4 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -6,7 +6,7 @@ import _ from 'underscore'; // TODO: remove eventHub hack after code splitting refactor window.emitSidebarEvent = window.emitSidebarEvent || $.noop; -function UsersSelect(currentUser, els) { +function UsersSelect(currentUser, els, options = {}) { var $els; this.users = this.users.bind(this); this.user = this.user.bind(this); @@ -20,6 +20,8 @@ function UsersSelect(currentUser, els) { } } + const { handleClick } = options; + $els = $(els); if (!els) { @@ -442,6 +444,9 @@ function UsersSelect(currentUser, els) { } if ($el.closest('.add-issues-modal').length) { gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id; + } else if (handleClick) { + e.preventDefault(); + handleClick(user, isMarking); } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { return Issuable.filterResults($dropdown.closest('form')); } else if ($dropdown.hasClass('js-filter-submit')) { diff --git a/app/assets/javascripts/vue_shared/components/loading_icon.vue b/app/assets/javascripts/vue_shared/components/loading_icon.vue index 15581d5c2a0..494fe4468d9 100644 --- a/app/assets/javascripts/vue_shared/components/loading_icon.vue +++ b/app/assets/javascripts/vue_shared/components/loading_icon.vue @@ -18,6 +18,12 @@ required: false, default: false, }, + + class: { + type: String, + required: false, + default: '', + }, }, computed: { @@ -25,7 +31,7 @@ return this.inline ? 'span' : 'div'; }, cssClass() { - return `fa-${this.size}x`; + return `fa-${this.size}x ${this.class}`.trim(); }, }, }; diff --git a/app/assets/javascripts/vue_shared/components/popup_dialog.vue b/app/assets/javascripts/vue_shared/components/popup_dialog.vue index 9e8c10bdc1a..d326277d1d6 100644 --- a/app/assets/javascripts/vue_shared/components/popup_dialog.vue +++ b/app/assets/javascripts/vue_shared/components/popup_dialog.vue @@ -5,17 +5,30 @@ export default { props: { title: { type: String, - required: true, + required: false, }, text: { type: String, required: false, +<<<<<<< HEAD +======= + }, + hideFooter: { + type: Boolean, + required: false, + default: false, +>>>>>>> e8a46294c0... apply changes for JS and CSS from gitlab-ee!2912 }, kind: { type: String, required: false, default: 'primary', }, + modalDialogClass: { + type: String, + required: false, + default: '', + }, closeKind: { type: String, required: false, @@ -30,6 +43,11 @@ export default { type: String, required: true, }, + submitDisabled: { + type: Boolean, + required: false, + default: false, + }, }, computed: { @@ -57,26 +75,60 @@ export default { diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 96f9dda26c4..1cfd7ef01a8 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -4,6 +4,9 @@ .cred { color: $common-red; } .cgreen { color: $common-green; } .cdark { color: $common-gray-dark; } +.text-secondary { + color: $gl-text-color-secondary; +} /** COMMON CLASSES **/ .prepend-top-0 { margin-top: 0; } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 1aa53b8f8cf..3ff124496d2 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -37,6 +37,7 @@ .dropdown-menu-nav { @include set-visible; display: block; + min-height: 40px; @media (max-width: $screen-xs-max) { width: 100%; diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index 1cebd02df48..d218fb6d702 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -42,3 +42,11 @@ body.modal-open { width: 98%; } } + +.modal.popup-dialog { + display: block; +} + +.modal-body { + background-color: $modal-body-bg; +} diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss index 3ea77eb7a43..a23131e0818 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss @@ -164,3 +164,36 @@ $pre-border-color: $border-color; $table-bg-accent: $gray-light; $zindex-popover: 900; + +//== Modals +// +//## + +//** Padding applied to the modal body +$modal-inner-padding: $gl-padding; + +//** Padding applied to the modal title +$modal-title-padding: $gl-padding; +//** Modal title line-height +// $modal-title-line-height: $line-height-base + +//** Background color of modal content area +$modal-content-bg: $gray-light; +$modal-body-bg: $white-light; +//** Modal content border color +// $modal-content-border-color: rgba(0,0,0,.2) +//** Modal content border color **for IE8** +// $modal-content-fallback-border-color: #999 + +//** Modal backdrop background color +// $modal-backdrop-bg: #000 +//** Modal backdrop opacity +// $modal-backdrop-opacity: .5 +//** Modal header border color +// $modal-header-border-color: #e5e5e5 +//** Modal footer border color +// $modal-footer-border-color: $modal-header-border-color + +// $modal-lg: 900px +// $modal-md: 600px +// $modal-sm: 300px diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index 6a363b1710e..e8c7f8a8fc0 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -7,19 +7,6 @@ background: $black-transparent; } -.modal.popup-dialog { - display: block; - background-color: $black-transparent; - z-index: 2100; - - @media (min-width: $screen-md-min) { - .modal-dialog { - width: 600px; - margin: 30px auto; - } - } -} - .project-refs-form, .project-refs-target-form { display: inline-block; -- cgit v1.2.1 From a63c9e94dfb87013875a75a3c96ac5773c71f5d6 Mon Sep 17 00:00:00 2001 From: Simon Knox Date: Thu, 26 Oct 2017 17:15:24 +0300 Subject: match UX guide for submit/cancel buttons --- app/assets/javascripts/vue_shared/components/popup_dialog.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/vue_shared/components/popup_dialog.vue b/app/assets/javascripts/vue_shared/components/popup_dialog.vue index d326277d1d6..fae6eccb4f4 100644 --- a/app/assets/javascripts/vue_shared/components/popup_dialog.vue +++ b/app/assets/javascripts/vue_shared/components/popup_dialog.vue @@ -111,14 +111,14 @@ export default { - - - -
-<<<<<<< HEAD - -======= ->>>>>>> e8a46294c0... apply changes for JS and CSS from gitlab-ee!2912 diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss index b9bfae9356c..e5b9e1f2de6 100644 --- a/app/assets/stylesheets/pages/clusters.scss +++ b/app/assets/stylesheets/pages/clusters.scss @@ -6,5 +6,5 @@ .cluster-applications-table { // Wait for the Vue to kick-in and render the applications block - min-height: 179px; + min-height: 302px; } -- cgit v1.2.1 From c6c9b37b1d1c9304b0eef530adb4d32178adae16 Mon Sep 17 00:00:00 2001 From: Alessio Caiazza Date: Fri, 3 Nov 2017 19:20:29 +0100 Subject: Add Clusters::Applications::Helm tests --- app/models/clusters/applications/helm.rb | 8 +++++--- app/models/clusters/cluster.rb | 2 +- app/models/clusters/concerns/application_status.rb | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) (limited to 'app') diff --git a/app/models/clusters/applications/helm.rb b/app/models/clusters/applications/helm.rb index 54eca613ce3..42626a50175 100644 --- a/app/models/clusters/applications/helm.rb +++ b/app/models/clusters/applications/helm.rb @@ -3,8 +3,6 @@ module Clusters class Helm < ActiveRecord::Base self.table_name = 'clusters_applications_helm' - NAME = 'helm'.freeze - include ::Clusters::Concerns::ApplicationStatus belongs_to :cluster, class_name: 'Clusters::Cluster', foreign_key: :cluster_id @@ -13,8 +11,12 @@ module Clusters validates :cluster, presence: true + def self.application_name + self.to_s.demodulize.underscore + end + def name - NAME + self.class.application_name end end end diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 242bae4eb3e..7d0be3d3739 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -5,7 +5,7 @@ module Clusters self.table_name = 'clusters' APPLICATIONS = { - Clusters::Applications::Helm::NAME => Clusters::Applications::Helm + Applications::Helm.application_name => Applications::Helm }.freeze belongs_to :user diff --git a/app/models/clusters/concerns/application_status.rb b/app/models/clusters/concerns/application_status.rb index c5711fd0b58..7bb68d75224 100644 --- a/app/models/clusters/concerns/application_status.rb +++ b/app/models/clusters/concerns/application_status.rb @@ -24,7 +24,7 @@ module Clusters end event :make_scheduled do - transition %i(installable errored) => :scheduled + transition any - [:scheduled] => :scheduled end before_transition any => [:scheduled] do |app_status, _| -- cgit v1.2.1 From 609d49bcf88310507e39885add138121a6536001 Mon Sep 17 00:00:00 2001 From: tauriedavis Date: Fri, 3 Nov 2017 15:55:17 -0700 Subject: 34768 Fix problem with issuable header wrapping when content is too long --- app/assets/stylesheets/framework/avatar.scss | 3 +-- app/assets/stylesheets/pages/detail_page.scss | 1 + app/assets/stylesheets/pages/issuable.scss | 12 +++++++++--- app/assets/stylesheets/pages/issues.scss | 19 ++++++++++++++++--- 4 files changed, 27 insertions(+), 8 deletions(-) (limited to 'app') diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index f1aedc227f3..a1755e32142 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -42,8 +42,7 @@ &.avatar-inline { float: none; display: inline-block; - margin-left: 4px; - margin-bottom: 2px; + margin-left: 2px; flex-shrink: 0; -webkit-flex-shrink: 0; diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index 3d9eff35583..538e50ee306 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -3,6 +3,7 @@ border-bottom: 1px solid $border-color; color: $gl-text-color; line-height: 34px; + display: flex; a { color: $gl-text-color; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 7059a4cfe85..0df16da3c86 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -613,6 +613,8 @@ float: none; display: inline-block; margin-top: 0; + height: auto; + align-self: center; @media (max-width: $screen-xs-max) { position: absolute; @@ -626,6 +628,8 @@ padding-left: 45px; padding-right: 45px; line-height: 35px; + display: flex; + flex-grow: 1; @media (min-width: $screen-sm-min) { float: left; @@ -637,11 +641,12 @@ .issuable-actions { @include new-style-dropdown; - padding-top: 10px; + align-self: center; + flex-shrink: 0; + flex: 0 0 auto; @media (min-width: $screen-sm-min) { float: right; - padding-top: 0; } } @@ -655,8 +660,9 @@ .issuable-meta { display: inline-block; - line-height: 18px; font-size: 14px; + line-height: 24px; + align-self: center; } .js-issuable-selector-wrap { diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index e8ca5cedaee..8bb68ad2425 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -134,11 +134,24 @@ ul.related-merge-requests > li { } @media (max-width: $screen-xs-max) { - .issue-btn-group { - width: 100%; + .detail-page-header, + .issuable-header { + display: block; + + .issuable-meta { + line-height: 18px; + } + } - .btn { + .issuable-actions { + margin-top: 10px; + + .issue-btn-group { width: 100%; + + .btn { + width: 100%; + } } } } -- cgit v1.2.1 From 70b8f421ae6587d2a57db40e7487bf2dd788cbde Mon Sep 17 00:00:00 2001 From: Mayra Cabrera Date: Sat, 4 Nov 2017 14:51:26 -0400 Subject: Modifies commit branches section - Display the default branch (if the limit is not exceeded) - Requires '...' to be clicked before showing the rest of the branches and/or tags - Fixes haml lints --- app/views/projects/commit/_commit_box.html.haml | 2 +- .../projects/commit/_limit_exceeded_message.html.haml | 4 ++-- app/views/projects/commit/branches.html.haml | 18 ++++++++++++------ 3 files changed, 15 insertions(+), 9 deletions(-) (limited to 'app') diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index cd93886bb24..b1beb2ff368 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -61,7 +61,7 @@ %span.cgray= n_('parent', 'parents', @commit.parents.count) - @commit.parents.each do |parent| = link_to parent.short_id, project_commit_path(@project, parent), class: "commit-sha" - %div.commit-info.branches + .commit-info.branches %i.fa.fa-spinner.fa-spin - if @commit.last_pipeline diff --git a/app/views/projects/commit/_limit_exceeded_message.html.haml b/app/views/projects/commit/_limit_exceeded_message.html.haml index b3372108321..430ac6df2e4 100644 --- a/app/views/projects/commit/_limit_exceeded_message.html.haml +++ b/app/views/projects/commit/_limit_exceeded_message.html.haml @@ -1,8 +1,8 @@ -.has-tooltip{class: "limit-box limit-box-#{objects.singularize}", data: { title: "Project has too many #{objects} to search"} } +.has-tooltip{ class: "limit-box limit-box-#{objects.singularize}", data: { title: "Project has too many #{objects} to search"} } .limit-icon - if objects == "branches" = icon('code-fork') - else = icon('tag') .limit-message - %span= "#{objects.capitalize} unavailable" + %span #{objects.capitalize} unavailable diff --git a/app/views/projects/commit/branches.html.haml b/app/views/projects/commit/branches.html.haml index d282eb47914..1c4f16b5afa 100644 --- a/app/views/projects/commit/branches.html.haml +++ b/app/views/projects/commit/branches.html.haml @@ -1,9 +1,15 @@ - if @branches_limit_exceeded = render 'limit_exceeded_message', objects: 'branches' -- elsif @branches.any? - = commit_branches_links(@project, @branches) +- else + - branch = commit_default_branch(@project, @branches) + = commit_branch_link(project_ref_path(@project, branch), branch) -- if @tags_limit_exceeded - = render 'limit_exceeded_message', objects: 'tags' -- elsif @tags.any? - = commit_tags_links(@project, @tags) +- if @branches.any? || @tags.any? || @tags_limit_exceeded + %span + = link_to "…", "#", class: "js-details-expand label label-gray" + %span.js-details-content.hide + = commit_branches_links(@project, @branches) + - if @tags_limit_exceeded + = render 'limit_exceeded_message', objects: 'tags' + - else + = commit_tags_links(@project, @tags) -- cgit v1.2.1 From f6f97ff3bd72b926d21f4dc6b15cf3772588d373 Mon Sep 17 00:00:00 2001 From: Simon Knox Date: Mon, 6 Nov 2017 17:38:09 +1100 Subject: fix line lint --- app/assets/javascripts/smart_interval.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/assets/javascripts/smart_interval.js b/app/assets/javascripts/smart_interval.js index 03fac82ac6d..95dd00b580e 100644 --- a/app/assets/javascripts/smart_interval.js +++ b/app/assets/javascripts/smart_interval.js @@ -5,7 +5,8 @@ export default class SmartInterval { /** - * @param { function } opts.callback Function that returns a promise, called on each iteration unless still in progress (required) + * @param { function } opts.callback Function that returns a promise, called on each iteration + * unless still in progress (required) * @param { milliseconds } opts.startingInterval `currentInterval` is set to this initially * @param { milliseconds } opts.maxInterval `currentInterval` will be incremented to this * @param { milliseconds } opts.hiddenInterval `currentInterval` is set to this -- cgit v1.2.1 From fb63796ed9b5062431ee40faf9c7652964b48112 Mon Sep 17 00:00:00 2001 From: Takuya Noguchi Date: Fri, 3 Nov 2017 08:59:51 +0900 Subject: Enable BorderZero rule in scss-lint --- app/assets/stylesheets/framework/avatar.scss | 4 ++-- app/assets/stylesheets/framework/blocks.scss | 10 +++++----- app/assets/stylesheets/framework/buttons.scss | 2 +- app/assets/stylesheets/framework/common.scss | 8 ++++---- app/assets/stylesheets/framework/files.scss | 12 ++++++------ app/assets/stylesheets/framework/filters.scss | 4 ++-- app/assets/stylesheets/framework/header.scss | 4 ++-- app/assets/stylesheets/framework/highlight.scss | 4 ++-- app/assets/stylesheets/framework/lists.scss | 10 +++++----- app/assets/stylesheets/framework/mixins.scss | 2 +- app/assets/stylesheets/framework/responsive_tables.scss | 2 +- .../framework/secondary-navigation-elements.scss | 4 ++-- app/assets/stylesheets/framework/selects.scss | 2 +- app/assets/stylesheets/framework/sidebar.scss | 4 ++-- app/assets/stylesheets/framework/tables.scss | 2 +- app/assets/stylesheets/framework/timeline.scss | 4 ++-- app/assets/stylesheets/framework/typography.scss | 2 +- app/assets/stylesheets/framework/zen.scss | 2 +- app/assets/stylesheets/pages/builds.scss | 2 +- app/assets/stylesheets/pages/ci_projects.scss | 2 +- app/assets/stylesheets/pages/commits.scss | 4 ++-- app/assets/stylesheets/pages/cycle_analytics.scss | 8 ++++---- app/assets/stylesheets/pages/diff.scss | 12 ++++++------ app/assets/stylesheets/pages/editor.scss | 6 +++--- app/assets/stylesheets/pages/environments.scss | 8 ++++---- app/assets/stylesheets/pages/events.scss | 6 +++--- app/assets/stylesheets/pages/issuable.scss | 8 ++++---- app/assets/stylesheets/pages/login.scss | 4 ++-- app/assets/stylesheets/pages/merge_conflicts.scss | 2 +- app/assets/stylesheets/pages/note_form.scss | 4 ++-- app/assets/stylesheets/pages/notes.scss | 8 ++++---- app/assets/stylesheets/pages/pipelines.scss | 16 ++++++++-------- app/assets/stylesheets/pages/profile.scss | 2 +- app/assets/stylesheets/pages/projects.scss | 10 +++++----- app/assets/stylesheets/pages/repo.scss | 6 +++--- app/assets/stylesheets/pages/search.scss | 4 ++-- app/assets/stylesheets/pages/todos.scss | 2 +- app/assets/stylesheets/pages/tree.scss | 2 +- app/views/admin/background_jobs/show.html.haml | 2 +- 39 files changed, 100 insertions(+), 100 deletions(-) (limited to 'app') diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index f1aedc227f3..50c100d398f 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -59,7 +59,7 @@ &.avatar-tile { border-radius: 0; - border: none; + border: 0; } &:not([href]):hover { @@ -96,7 +96,7 @@ .avatar { border-radius: 0; - border: none; + border: 0; height: auto; width: 100%; margin: 0; diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index def986180fc..9c1439dfad5 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -39,7 +39,7 @@ } &.top-block { - border-top: none; + border-top: 0; .container-fluid { background-color: inherit; @@ -63,7 +63,7 @@ &.footer-block { margin-top: 0; - border-bottom: none; + border-bottom: 0; margin-bottom: -$gl-padding; } @@ -100,7 +100,7 @@ &.build-content { background-color: $white-light; - border-top: none; + border-top: 0; } } @@ -287,12 +287,12 @@ cursor: pointer; color: $blue-300; z-index: 1; - border: none; + border: 0; background-color: transparent; &:hover, &:focus { - border: none; + border: 0; color: $blue-400; } } diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 00a0e9cef67..c4a95afc4d2 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -304,7 +304,7 @@ } .btn-clipboard { - border: none; + border: 0; padding: 0 5px; } diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index ea3007f5e08..393a0052114 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -28,7 +28,7 @@ pre { &.clean { background: none; - border: none; + border: 0; margin: 0; padding: 0; } @@ -142,7 +142,7 @@ li.note { img { max-width: 100%; } .note-title { li { - border-bottom: none !important; + border-bottom: 0 !important; } } } @@ -187,7 +187,7 @@ li.note { pre { background: $white-light; - border: none; + border: 0; font-size: 12px; } } @@ -386,7 +386,7 @@ img.emoji { } .hide-bottom-border { - border-bottom: none !important; + border-bottom: 0 !important; } .gl-accessibility { diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 6382551fcc9..1247e5e4876 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -142,7 +142,7 @@ */ &.blame { table { - border: none; + border: 0; margin: 0; } @@ -150,20 +150,20 @@ border-bottom: 1px solid $blame-border; &:last-child { - border-bottom: none; + border-bottom: 0; } } td { - border-top: none; - border-bottom: none; + border-top: 0; + border-bottom: 0; &:first-child { - border-left: none; + border-left: 0; } &:last-child { - border-right: none; + border-right: 0; } &.blame-commit { diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index a7333925f80..74b6b31b07e 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -255,7 +255,7 @@ .clear-search { width: 35px; background-color: $white-light; - border: none; + border: 0; outline: none; z-index: 1; @@ -418,7 +418,7 @@ .droplab-dropdown .dropdown-menu .filter-dropdown-item { .btn { - border: none; + border: 0; width: 100%; text-align: left; padding: 8px 16px; diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 5d777f0d468..af51180eb18 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -10,7 +10,7 @@ z-index: 1000; margin-bottom: 0; min-height: $header-height; - border: none; + border: 0; border-bottom: 1px solid $border-color; position: fixed; top: 0; @@ -169,7 +169,7 @@ .navbar-collapse { flex: 0 0 auto; - border-top: none; + border-top: 0; padding: 0; @media (max-width: $screen-xs-max) { diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss index c63114f85b4..813a1711ea2 100644 --- a/app/assets/stylesheets/framework/highlight.scss +++ b/app/assets/stylesheets/framework/highlight.scss @@ -1,5 +1,5 @@ .file-content.code { - border: none; + border: 0; box-shadow: none; margin: 0; padding: 0; @@ -7,7 +7,7 @@ pre { padding: 10px 0; - border: none; + border: 0; border-radius: 0; font-family: $monospace_font; font-size: $code_font_size; diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 511608c618c..ad3bb0e35d1 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -42,7 +42,7 @@ } &:last-child { - border-bottom: none; + border-bottom: 0; &.bottom { background: $gray-light; @@ -92,7 +92,7 @@ ul.unstyled-list { } ul.unstyled-list > li { - border-bottom: none; + border-bottom: 0; } // Generic content list @@ -178,7 +178,7 @@ ul.content-list { // When dragging a list item &.ui-sortable-helper { - border-bottom: none; + border-bottom: 0; } &.list-placeholder { @@ -295,7 +295,7 @@ ul.indent-list { } > .group-list-tree > .group-row.has-children:first-child { - border-top: none; + border-top: 0; } } @@ -413,7 +413,7 @@ ul.indent-list { padding: 0; &.has-children { - border-top: none; + border-top: 0; } &:first-child { diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 2fee2164190..16d5edde61e 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -36,7 +36,7 @@ margin: 0; &:last-child { - border-bottom: none; + border-bottom: 0; } &.active { diff --git a/app/assets/stylesheets/framework/responsive_tables.scss b/app/assets/stylesheets/framework/responsive_tables.scss index 8b7afdbe1a5..7829d722560 100644 --- a/app/assets/stylesheets/framework/responsive_tables.scss +++ b/app/assets/stylesheets/framework/responsive_tables.scss @@ -24,7 +24,7 @@ @media (min-width: $screen-md-min) { margin: 0; padding: $gl-padding 0; - border: none; + border: 0; &:not(:last-child) { border-bottom: 1px solid $white-normal; diff --git a/app/assets/stylesheets/framework/secondary-navigation-elements.scss b/app/assets/stylesheets/framework/secondary-navigation-elements.scss index 9e1f77e5726..6c733f1c60b 100644 --- a/app/assets/stylesheets/framework/secondary-navigation-elements.scss +++ b/app/assets/stylesheets/framework/secondary-navigation-elements.scss @@ -63,7 +63,7 @@ .nav-links { margin-bottom: 0; - border-bottom: none; + border-bottom: 0; float: left; &.wide { @@ -335,7 +335,7 @@ border-bottom: 1px solid $border-color; .nav-links { - border-bottom: none; + border-bottom: 0; } } } diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index aa35cd9bea4..fc6ed0d779f 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -17,7 +17,7 @@ .select2-arrow { background-image: none; background-color: transparent; - border: none; + border: 0; padding-top: 12px; padding-right: 20px; font-size: 10px; diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index ef58382ba41..1a19b7320a0 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -9,7 +9,7 @@ &.container-blank { background: none; padding: 0; - border: none; + border: 0; } } } @@ -111,7 +111,7 @@ } .block:last-of-type { - border: none; + border: 0; } } diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss index 4dd31bf28cd..5bde96caf42 100644 --- a/app/assets/stylesheets/framework/tables.scss +++ b/app/assets/stylesheets/framework/tables.scss @@ -33,7 +33,7 @@ table { th { background-color: $gray-light; font-weight: $gl-font-weight-normal; - border-bottom: none; + border-bottom: 0; &.wide { width: 55%; diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index f718ec4bcad..373f35e71d8 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -21,7 +21,7 @@ } &.text-file .diff-file { - border-bottom: none; + border-bottom: 0; } } @@ -66,5 +66,5 @@ .discussion .timeline-entry { margin: 0; - border-right: none; + border-right: 0; } diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 3c0b4c82d19..0817cce114c 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -167,7 +167,7 @@ &.plain-readme { background: none; - border: none; + border: 0; padding: 0; margin: 0; font-size: 14px; diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss index 0c226ff7598..dbd3144b9b4 100644 --- a/app/assets/stylesheets/framework/zen.scss +++ b/app/assets/stylesheets/framework/zen.scss @@ -9,7 +9,7 @@ z-index: 1031; textarea { - border: none; + border: 0; box-shadow: none; border-radius: 0; color: $black; diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 46978be8ba0..27b10b536a2 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -48,7 +48,7 @@ overflow-x: auto; font-size: 12px; border-radius: 0; - border: none; + border: 0; .bash { display: block; diff --git a/app/assets/stylesheets/pages/ci_projects.scss b/app/assets/stylesheets/pages/ci_projects.scss index bf6a48889bf..fbe1f3081a0 100644 --- a/app/assets/stylesheets/pages/ci_projects.scss +++ b/app/assets/stylesheets/pages/ci_projects.scss @@ -36,7 +36,7 @@ pre.commit-message { background: none; padding: 0; - border: none; + border: 0; margin: 20px 0; border-radius: 0; } diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index ee3ca246374..b1850be8a5f 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -1,6 +1,6 @@ .commit-description { background: none; - border: none; + border: 0; padding: 0; margin-top: 10px; word-break: normal; @@ -247,7 +247,7 @@ word-break: normal; pre { - border: none; + border: 0; background: inherit; padding: 0; margin: 0; diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index 82d9be29201..292e0ad394b 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -80,7 +80,7 @@ .panel { .content-block { padding: 24px 0; - border-bottom: none; + border-bottom: 0; position: relative; @media (max-width: $screen-xs-max) { @@ -222,11 +222,11 @@ } &:first-child { - border-top: none; + border-top: 0; } &:last-child { - border-bottom: none; + border-bottom: 0; } .stage-nav-item-cell { @@ -290,7 +290,7 @@ border-bottom: 1px solid $gray-darker; &:last-child { - border-bottom: none; + border-bottom: 0; margin-bottom: 0; } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index faa3d1fb4d5..bce94e09367 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -47,7 +47,7 @@ table { width: 100%; font-family: $monospace_font; - border: none; + border: 0; border-collapse: separate; margin: 0; padding: 0; @@ -105,7 +105,7 @@ .new_line { @include user-select(none); margin: 0; - border: none; + border: 0; padding: 0 5px; border-right: 1px solid; text-align: right; @@ -133,7 +133,7 @@ display: block; margin: 0; padding: 0 1.5em; - border: none; + border: 0; position: relative; &.parallel { @@ -359,7 +359,7 @@ cursor: pointer; &:first-child { - border-left: none; + border-left: 0; } &:hover { @@ -388,7 +388,7 @@ .file-content .diff-file { margin: 0; - border: none; + border: 0; } .diff-wrap-lines .line_content { @@ -400,7 +400,7 @@ } .files-changed { - border-bottom: none; + border-bottom: 0; } .diff-stats-summary-toggler { diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index edfafa79c44..c586dab4cf2 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -3,13 +3,13 @@ border-top: 1px solid $border-color; border-right: 1px solid $border-color; border-left: 1px solid $border-color; - border-bottom: none; + border-bottom: 0; border-radius: $border-radius-small $border-radius-small 0 0; background: $gray-normal; } #editor { - border: none; + border: 0; border-radius: 0; height: 500px; margin: 0; @@ -171,7 +171,7 @@ width: 100%; margin: 5px 0; padding: 0; - border-left: none; + border-left: 0; } } diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index b5b0f3d9dfa..b0f57e26a40 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -117,7 +117,7 @@ } .no-btn { - border: none; + border: 0; background: none; outline: none; width: 100%; @@ -133,11 +133,11 @@ } .folder-row { - border-left: none; - border-right: none; + border-left: 0; + border-right: 0; @media (min-width: $screen-sm-max) { - border-top: none; + border-top: 0; } } diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index 1723d716805..eea8b7dd193 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -85,7 +85,7 @@ } pre { - border: none; + border: 0; background: $gray-light; border-radius: 0; color: $events-pre-color; @@ -128,14 +128,14 @@ } } - &:last-child { border: none; } + &:last-child { border: 0; } .event_commits { li { &.commit { background: transparent; padding: 0; - border: none; + border: 0; .commit-row-title { font-size: $gl-font-size; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 7059a4cfe85..70bf852509b 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -79,7 +79,7 @@ .title { padding: 0; margin-bottom: 16px; - border-bottom: none; + border-bottom: 0; } .btn-edit { @@ -164,7 +164,7 @@ } &:last-child { - border: none; + border: 0; } span { @@ -338,7 +338,7 @@ .block { width: $gutter_collapsed_width - 2px; padding: 15px 0 0; - border-bottom: none; + border-bottom: 0; overflow: hidden; } @@ -399,7 +399,7 @@ } .btn-clipboard { - border: none; + border: 0; color: $issuable-sidebar-color; &:hover { diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index 92d49bd864a..b7985c4dea5 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -139,7 +139,7 @@ border-left: 1px solid $border-color; &:first-of-type { - border-left: none; + border-left: 0; border-top-left-radius: $border-radius-default; } @@ -165,7 +165,7 @@ border-bottom: 1px solid $border-color; a { - border: none; + border: 0; border-bottom: 2px solid $link-underline-blue; margin-right: 0; color: $black; diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss index dbf3e2b763c..04bde64c752 100644 --- a/app/assets/stylesheets/pages/merge_conflicts.scss +++ b/app/assets/stylesheets/pages/merge_conflicts.scss @@ -262,7 +262,7 @@ $colors: ( .editor { pre { height: 350px; - border: none; + border: 0; border-radius: 0; margin-bottom: 0; } diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 5127307c5e7..66695462f65 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -16,7 +16,7 @@ .discussion { .new-note { margin: 0; - border: none; + border: 0; } } @@ -106,7 +106,7 @@ background-color: $orange-100; border-radius: $border-radius-default $border-radius-default 0 0; border: 1px solid $border-gray-normal; - border-bottom: none; + border-bottom: 0; padding: 3px 12px; margin: auto; align-items: center; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index ca363c6eac4..f89a392cb9d 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -331,7 +331,7 @@ ul.notes { td { border: 1px solid $white-normal; - border-left: none; + border-left: 0; &.notes_line { vertical-align: middle; @@ -666,7 +666,7 @@ ul.notes { .timeline-entry-inner { padding-left: $gl-padding; padding-right: $gl-padding; - border-bottom: none; + border-bottom: 0; } } } @@ -679,7 +679,7 @@ ul.notes { padding: 90px 0; &.discussion-locked { - border: none; + border: 0; background-color: $white-light; } @@ -759,7 +759,7 @@ ul.notes { top: 0; padding: 0; background-color: transparent; - border: none; + border: 0; outline: 0; color: $gray-darkest; transition: color $general-hover-transition-duration $general-hover-transition-curve; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 2a8cbc61af7..997cbc815c2 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -179,7 +179,7 @@ * Play button with icon in dropdowns */ .no-btn { - border: none; + border: 0; background: none; outline: none; width: 100%; @@ -288,7 +288,7 @@ .pipeline-actions { @include new-style-dropdown; - border-bottom: none; + border-bottom: 0; } .tab-pane { @@ -318,7 +318,7 @@ } .build-log { - border: none; + border: 0; line-height: initial; } } @@ -386,13 +386,13 @@ // Remove right connecting horizontal line from first build in last stage &:first-child { &::after { - border: none; + border: 0; } } // Remove right curved connectors from all builds in last stage &:not(:first-child) { &::after { - border: none; + border: 0; } } // Remove opposite curve @@ -409,7 +409,7 @@ // Remove left curved connectors from all builds in first stage &:not(:first-child) { &::before { - border: none; + border: 0; } } // Remove opposite curve @@ -518,7 +518,7 @@ .dropdown-menu-toggle { background-color: transparent; - border: none; + border: 0; padding: 0; &:focus { @@ -951,7 +951,7 @@ button.mini-pipeline-graph-dropdown-toggle { .terminal-container { .content-block { - border-bottom: none; + border-bottom: 0; } #terminal { diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index eab39f698c3..28dc71dc641 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -113,7 +113,7 @@ li { padding: 3px 0; - border: none; + border: 0; } } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index b0c3474e3d5..aaad6dbba8e 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -80,7 +80,7 @@ .project-feature-settings { background: $gray-lighter; - border-top: none; + border-top: 0; margin-bottom: 16px; } @@ -128,7 +128,7 @@ .project-feature-toggle { position: relative; - border: none; + border: 0; outline: 0; display: block; width: 100px; @@ -483,7 +483,7 @@ a.deploy-project-label { flex: 1; padding: 0; background: transparent; - border: none; + border: 0; line-height: 34px; margin: 0; @@ -1012,7 +1012,7 @@ pre.light-well { margin: 0; border-radius: 0 0 1px 1px; padding: 20px 0; - border: none; + border: 0; } .table-bordered { @@ -1165,7 +1165,7 @@ pre.light-well { table-layout: fixed; &.table-responsive { - border: none; + border: 0; } .variable-key { diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index 1bb4e3cc345..fee4638e20f 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -64,7 +64,7 @@ .monaco-editor.vs { .current-line { - border: none; + border: 0; background: $well-light-border; } @@ -139,7 +139,7 @@ &.active { background: $white-light; - border-bottom: none; + border-bottom: 0; } a { @@ -181,7 +181,7 @@ &.tabs-divider { width: 100%; background-color: $white-light; - border-right: none; + border-right: 0; border-top-right-radius: 2px; } } diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index eed711b1b66..7377be300e6 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -5,7 +5,7 @@ margin-bottom: $gl-padding; &:last-child { - border-bottom: none; + border-bottom: 0; } } @@ -57,7 +57,7 @@ input[type="checkbox"]:hover { } .search-input { - border: none; + border: 0; font-size: 14px; padding: 0 20px 0 0; margin-left: 5px; diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index 6c8d87185e9..2139a029fc7 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -141,7 +141,7 @@ } pre { - border: none; + border: 0; background: $gray-light; border-radius: 0; color: $todo-body-pre-color; diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index e2f6e511c86..50f0ef4414a 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -252,7 +252,7 @@ margin-top: 20px; padding: 0; border-top: 1px solid $white-dark; - border-bottom: none; + border-bottom: 0; } .commit-stats li { diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml index 3ef8f2a3acb..f0cc4d7ee62 100644 --- a/app/views/admin/background_jobs/show.html.haml +++ b/app/views/admin/background_jobs/show.html.haml @@ -42,4 +42,4 @@ .panel.panel-default - %iframe{ src: sidekiq_path, width: '100%', height: 970, style: "border: none" } + %iframe{ src: sidekiq_path, width: '100%', height: 970, style: "border: 0" } -- cgit v1.2.1 From 80b0834ae96480202678d8ca1e19c0ee4abf9001 Mon Sep 17 00:00:00 2001 From: Alessio Caiazza Date: Mon, 6 Nov 2017 10:23:15 +0100 Subject: Add Clusters::Appplications::CheckInstallationProgressService tests --- .../clusters/applications/check_installation_progress_service.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/services/clusters/applications/check_installation_progress_service.rb b/app/services/clusters/applications/check_installation_progress_service.rb index cf96c128c2e..81306a2ff5b 100644 --- a/app/services/clusters/applications/check_installation_progress_service.rb +++ b/app/services/clusters/applications/check_installation_progress_service.rb @@ -27,13 +27,13 @@ module Clusters end def on_failed - app.make_errored!(log || 'Installation silently failed') + app.make_errored!(installation_errors || 'Installation silently failed') finalize_installation end def check_timeout if Time.now.utc - app.updated_at.to_time.utc > ClusterWaitForAppInstallationWorker::TIMEOUT - app.make_errored!('App installation timeouted') + app.make_errored!('Installation timeouted') else ClusterWaitForAppInstallationWorker.perform_in( ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id) -- cgit v1.2.1 From d8223468ae2ae061020cc26336c51dc93cc75571 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 6 Nov 2017 10:41:27 +0100 Subject: Add ingress application --- app/models/clusters/applications/ingress.rb | 32 +++++++++++++++++++++++++++++ app/models/clusters/cluster.rb | 7 +++++-- 2 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 app/models/clusters/applications/ingress.rb (limited to 'app') diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb new file mode 100644 index 00000000000..0554cf84ed7 --- /dev/null +++ b/app/models/clusters/applications/ingress.rb @@ -0,0 +1,32 @@ +module Clusters + module Applications + class Ingress < ActiveRecord::Base + self.table_name = 'clusters_applications_ingress' + + include ::Clusters::Concerns::ApplicationStatus + + belongs_to :cluster, class_name: 'Clusters::Cluster', foreign_key: :cluster_id + + validates :cluster, presence: true + + default_value_for :ingress_type, :nginx + default_value_for :version, :nginx + + enum ingress_type: { + nginx: 1 + } + + def self.application_name + self.to_s.demodulize.underscore + end + + def name + self.class.application_name + end + + def chart + 'stable/nginx-ingress' + end + end + end +end diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 7d0be3d3739..cfed9c52860 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -5,7 +5,8 @@ module Clusters self.table_name = 'clusters' APPLICATIONS = { - Applications::Helm.application_name => Applications::Helm + Applications::Helm.application_name => Applications::Helm, + Applications::Ingress.application_name => Applications::Ingress }.freeze belongs_to :user @@ -20,6 +21,7 @@ module Clusters has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes', autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_one :application_helm, class_name: 'Clusters::Applications::Helm' + has_one :application_ingress, class_name: 'Clusters::Applications::Ingress' accepts_nested_attributes_for :provider_gcp, update_only: true accepts_nested_attributes_for :platform_kubernetes, update_only: true @@ -59,7 +61,8 @@ module Clusters def applications [ - application_helm || build_application_helm + application_helm || build_application_helm, + application_ingress || build_application_ingress ] end -- cgit v1.2.1 From 8ef73aaaf461ad7fe2b7e351c28c7dfc150bedf0 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 6 Nov 2017 10:48:38 +0100 Subject: Set installation paths --- app/views/projects/clusters/show.html.haml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'app') diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml index 799b07046e5..75cfeb08b34 100644 --- a/app/views/projects/clusters/show.html.haml +++ b/app/views/projects/clusters/show.html.haml @@ -6,9 +6,8 @@ - status_path = status_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster.id, format: :json) if can?(current_user, :admin_cluster, @cluster) .edit-cluster-form.js-edit-cluster-form{ data: { status_path: status_path, - install_helm_path: 'TODO', - install_ingress_path: 'TODO', - install_runner_path: 'TODO', + install_helm_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :helm), + install_ingress_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :ingress), toggle_status: @cluster.enabled? ? 'true': 'false', cluster_status: @cluster.status_name, cluster_status_reason: @cluster.status_reason, -- cgit v1.2.1 From fe0292cfa7266ad5f936e103cdf005a00da4c233 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 6 Nov 2017 10:50:10 +0100 Subject: Remove not used ingress and runner --- app/assets/javascripts/clusters/clusters_bundle.js | 4 ---- app/assets/javascripts/clusters/services/clusters_service.js | 2 -- app/views/projects/clusters/show.html.haml | 4 +--- 3 files changed, 1 insertion(+), 9 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index 053f11cc3c4..9a436b3e22d 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -28,8 +28,6 @@ export default class Clusters { const { statusPath, installHelmPath, - installIngressPath, - installRunnerPath, clusterStatus, clusterStatusReason, helpPath, @@ -42,8 +40,6 @@ export default class Clusters { this.service = new ClustersService({ endpoint: statusPath, installHelmEndpoint: installHelmPath, - installIngresEndpoint: installIngressPath, - installRunnerEndpoint: installRunnerPath, }); this.toggle = this.toggle.bind(this); diff --git a/app/assets/javascripts/clusters/services/clusters_service.js b/app/assets/javascripts/clusters/services/clusters_service.js index 0ac8e68187d..a7bb0961c7e 100644 --- a/app/assets/javascripts/clusters/services/clusters_service.js +++ b/app/assets/javascripts/clusters/services/clusters_service.js @@ -8,8 +8,6 @@ export default class ClusterService { this.options = options; this.appInstallEndpointMap = { helm: this.options.installHelmEndpoint, - ingress: this.options.installIngressEndpoint, - runner: this.options.installRunnerEndpoint, }; } diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml index 799b07046e5..f116c4f7dba 100644 --- a/app/views/projects/clusters/show.html.haml +++ b/app/views/projects/clusters/show.html.haml @@ -6,9 +6,7 @@ - status_path = status_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster.id, format: :json) if can?(current_user, :admin_cluster, @cluster) .edit-cluster-form.js-edit-cluster-form{ data: { status_path: status_path, - install_helm_path: 'TODO', - install_ingress_path: 'TODO', - install_runner_path: 'TODO', + install_helm_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :helm), toggle_status: @cluster.enabled? ? 'true': 'false', cluster_status: @cluster.status_name, cluster_status_reason: @cluster.status_reason, -- cgit v1.2.1 From 6902848a9c54f9eb1bfd82fe173ad0d5d62fe2d5 Mon Sep 17 00:00:00 2001 From: Markus Koller Date: Mon, 18 Sep 2017 15:03:24 +0200 Subject: Support custom attributes on projects --- app/finders/projects_finder.rb | 3 +++ app/models/project.rb | 1 + app/models/project_custom_attribute.rb | 6 ++++++ 3 files changed, 10 insertions(+) create mode 100644 app/models/project_custom_attribute.rb (limited to 'app') diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index eac6095d8dc..005612ededc 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -18,6 +18,8 @@ # non_archived: boolean # class ProjectsFinder < UnionFinder + include CustomAttributesFilter + attr_accessor :params attr_reader :current_user, :project_ids_relation @@ -44,6 +46,7 @@ class ProjectsFinder < UnionFinder collection = by_tags(collection) collection = by_search(collection) collection = by_archived(collection) + collection = by_custom_attributes(collection) sort(collection) end diff --git a/app/models/project.rb b/app/models/project.rb index b04aec550b1..38ac7b20b05 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -213,6 +213,7 @@ class Project < ActiveRecord::Base has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner' has_one :auto_devops, class_name: 'ProjectAutoDevops' + has_many :custom_attributes, class_name: 'ProjectCustomAttribute' accepts_nested_attributes_for :variables, allow_destroy: true accepts_nested_attributes_for :project_feature, update_only: true diff --git a/app/models/project_custom_attribute.rb b/app/models/project_custom_attribute.rb new file mode 100644 index 00000000000..3f1a7b86a82 --- /dev/null +++ b/app/models/project_custom_attribute.rb @@ -0,0 +1,6 @@ +class ProjectCustomAttribute < ActiveRecord::Base + belongs_to :project + + validates :project, :key, :value, presence: true + validates :key, uniqueness: { scope: [:project_id] } +end -- cgit v1.2.1 From 1f773a8ef5a1f76166d0455c6a5e473278885c17 Mon Sep 17 00:00:00 2001 From: Markus Koller Date: Mon, 18 Sep 2017 17:07:38 +0200 Subject: Support custom attributes on groups --- app/finders/groups_finder.rb | 8 +++++++- app/models/group.rb | 1 + app/models/group_custom_attribute.rb | 6 ++++++ 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 app/models/group_custom_attribute.rb (limited to 'app') diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb index 0c4c4b10fb6..0282b378d88 100644 --- a/app/finders/groups_finder.rb +++ b/app/finders/groups_finder.rb @@ -15,6 +15,8 @@ # Anonymous users will never return any `owned` groups. They will return all # public groups instead, even if `all_available` is set to false. class GroupsFinder < UnionFinder + include CustomAttributesFilter + def initialize(current_user = nil, params = {}) @current_user = current_user @params = params @@ -22,8 +24,12 @@ class GroupsFinder < UnionFinder def execute items = all_groups.map do |item| - by_parent(item) + item = by_parent(item) + item = by_custom_attributes(item) + + item end + find_union(items, Group).with_route.order_id_desc end diff --git a/app/models/group.rb b/app/models/group.rb index c660de7fcb6..8cf632fb566 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -26,6 +26,7 @@ class Group < Namespace has_many :notification_settings, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent has_many :labels, class_name: 'GroupLabel' has_many :variables, class_name: 'Ci::GroupVariable' + has_many :custom_attributes, class_name: 'GroupCustomAttribute' validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } validate :visibility_level_allowed_by_projects diff --git a/app/models/group_custom_attribute.rb b/app/models/group_custom_attribute.rb new file mode 100644 index 00000000000..8157d602d67 --- /dev/null +++ b/app/models/group_custom_attribute.rb @@ -0,0 +1,6 @@ +class GroupCustomAttribute < ActiveRecord::Base + belongs_to :group + + validates :group, :key, :value, presence: true + validates :key, uniqueness: { scope: [:group_id] } +end -- cgit v1.2.1 From a85292922d0721b4912e86396ea5a7ce511d0516 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 6 Nov 2017 11:03:25 +0100 Subject: Fix ingress endpoint --- app/assets/javascripts/clusters/clusters_bundle.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index ad376efc3c3..9f92d49f576 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -41,7 +41,7 @@ export default class Clusters { this.service = new ClustersService({ endpoint: statusPath, installHelmEndpoint: installHelmPath, - installIngresEndpoint: installIngressPath, + installIngressEndpoint: installIngressPath, }); this.toggle = this.toggle.bind(this); -- cgit v1.2.1 From 001de85e7c6f86423aca0d245fdc83c57b374630 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 6 Nov 2017 11:03:58 +0100 Subject: Return empty applications if not Kubernetes [ci skip] --- app/models/clusters/cluster.rb | 2 ++ 1 file changed, 2 insertions(+) (limited to 'app') diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 7d0be3d3739..5b9bd6e548b 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -58,6 +58,8 @@ module Clusters end def applications + return [] unless kubernetes? + [ application_helm || build_application_helm ] -- cgit v1.2.1 From 503f21367051c18412d6bdf3d4586eaddaf69087 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 6 Oct 2017 11:45:23 +0200 Subject: Make sure that every job has a stage assigned --- app/models/commit_status.rb | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index f3888528940..7f2295dd4da 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -14,7 +14,6 @@ class CommitStatus < ActiveRecord::Base delegate :sha, :short_sha, to: :pipeline validates :pipeline, presence: true, unless: :importing? - validates :name, presence: true, unless: :importing? alias_attribute :author, :user @@ -46,6 +45,21 @@ class CommitStatus < ActiveRecord::Base runner_system_failure: 4 } + ## + # We still create some CommitStatuses outside of CreatePipelineService. + # + # These are pages deployments and external statuses. + # + before_create do |status| + next if status.stage_id.present? || importing? + + ensure_pipeline_stage! do |stage| + status.run_after_commit do + StageUpdateWorker.perform_async(stage.id) + end + end + end + state_machine :status do event :process do transition [:skipped, :manual] => :created @@ -174,4 +188,16 @@ class CommitStatus < ActiveRecord::Base v =~ /\d+/ ? v.to_i : v end end + + private + + def ensure_pipeline_stage! + attributes = { name: stage, pipeline: pipeline, project: project } + + Ci::Stage.create!(attributes).tap do |stage| + self.stage_id = stage.id + + yield stage + end + end end -- cgit v1.2.1 From e2828a60679495d716ed3824959794f4d5fbf5bb Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 6 Oct 2017 12:07:11 +0200 Subject: Use existing pipeline stage if stage already exists --- app/models/commit_status.rb | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) (limited to 'app') diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 7f2295dd4da..12e187024dd 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -192,12 +192,20 @@ class CommitStatus < ActiveRecord::Base private def ensure_pipeline_stage! - attributes = { name: stage, pipeline: pipeline, project: project } - - Ci::Stage.create!(attributes).tap do |stage| + (find_stage || create_stage!).tap do |stage| self.stage_id = stage.id yield stage end end + + def find_stage + pipeline.stages.find_by(name: stage) + end + + def create_stage! + Ci::Stage.create!(name: stage, + pipeline: pipeline, + project: project) + end end -- cgit v1.2.1 From 164b1df59025e9685e243dd89d943ff5b1122b44 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 11 Oct 2017 15:32:19 +0200 Subject: Extract ensure stage service from commit status class --- app/models/commit_status.rb | 30 +++---------------------- app/services/ci/ensure_stage_service.rb | 39 +++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 27 deletions(-) create mode 100644 app/services/ci/ensure_stage_service.rb (limited to 'app') diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 12e187024dd..6b07dbdf3ea 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -50,13 +50,9 @@ class CommitStatus < ActiveRecord::Base # # These are pages deployments and external statuses. # - before_create do |status| - next if status.stage_id.present? || importing? - - ensure_pipeline_stage! do |stage| - status.run_after_commit do - StageUpdateWorker.perform_async(stage.id) - end + before_create unless: :importing? do + Ci::EnsureStageService.new(project, user).execute(self) do |stage| + self.run_after_commit { StageUpdateWorker.perform_async(stage.id) } end end @@ -188,24 +184,4 @@ class CommitStatus < ActiveRecord::Base v =~ /\d+/ ? v.to_i : v end end - - private - - def ensure_pipeline_stage! - (find_stage || create_stage!).tap do |stage| - self.stage_id = stage.id - - yield stage - end - end - - def find_stage - pipeline.stages.find_by(name: stage) - end - - def create_stage! - Ci::Stage.create!(name: stage, - pipeline: pipeline, - project: project) - end end diff --git a/app/services/ci/ensure_stage_service.rb b/app/services/ci/ensure_stage_service.rb new file mode 100644 index 00000000000..dc2f49e8db1 --- /dev/null +++ b/app/services/ci/ensure_stage_service.rb @@ -0,0 +1,39 @@ +module Ci + ## + # We call this service everytime we persist a CI/CD job. + # + # In most cases a job should already have a stage assigned, but in cases it + # doesn't have we need to either find existing one or create a brand new + # stage. + # + class EnsureStageService < BaseService + def execute(build) + @build = build + + return if build.stage_id.present? + return if build.invalid? + + ensure_stage.tap do |stage| + build.stage_id = stage.id + + yield stage if block_given? + end + end + + private + + def ensure_stage + find_stage || create_stage + end + + def find_stage + @build.pipeline.stages.find_by(name: @build.stage) + end + + def create_stage + Ci::Stage.create!(name: @build.stage, + pipeline: @build.pipeline, + project: @build.project) + end + end +end -- cgit v1.2.1 From 054200062f1151fd31138305721d04798e8880b7 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 1 Nov 2017 16:08:06 +0000 Subject: Multi-file editor fetch log data from a different endpoint Closes #38360 --- .../javascripts/repo/components/repo_file.vue | 35 ++++++++++++++++++++-- .../repo/components/repo_loading_file.vue | 2 +- app/assets/javascripts/repo/services/index.js | 7 +++++ app/assets/javascripts/repo/stores/actions/tree.js | 29 +++++++++++++++++- .../javascripts/repo/stores/mutation_types.js | 2 ++ app/assets/javascripts/repo/stores/mutations.js | 7 +++++ .../javascripts/repo/stores/mutations/tree.js | 11 +++++-- app/assets/javascripts/repo/stores/state.js | 1 + app/assets/javascripts/repo/stores/utils.js | 16 +++++----- app/controllers/projects/refs_controller.rb | 8 ++++- app/serializers/blob_entity.rb | 4 --- app/serializers/tree_entity.rb | 4 --- app/serializers/tree_root_entity.rb | 4 +++ 13 files changed, 104 insertions(+), 26 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/repo/components/repo_file.vue b/app/assets/javascripts/repo/components/repo_file.vue index 7a23154b340..3d2ab90187d 100644 --- a/app/assets/javascripts/repo/components/repo_file.vue +++ b/app/assets/javascripts/repo/components/repo_file.vue @@ -16,6 +16,9 @@ ...mapGetters([ 'isCollapsed', ]), + isSubmodule() { + return this.file.type === 'submodule'; + }, fileIcon() { return { 'fa-spinner fa-spin': this.file.loading, @@ -31,6 +34,9 @@ shortId() { return this.file.id.substr(0, 8); }, + submoduleColSpan() { + return !this.isCollapsed && this.isSubmodule ? 3 : undefined; + }, }, methods: { ...mapActions([ @@ -44,7 +50,7 @@ - + {{ file.name }} -