diff options
author | Robert Speicher <rspeicher@gmail.com> | 2021-01-20 22:34:23 +0300 |
---|---|---|
committer | Robert Speicher <rspeicher@gmail.com> | 2021-01-20 22:34:23 +0300 |
commit | 6438df3a1e0fb944485cebf07976160184697d72 (patch) | |
tree | 00b09bfd170e77ae9391b1a2f5a93ef6839f2597 /lib/atlassian | |
parent | 42bcd54d971da7ef2854b896a7b34f4ef8601067 (diff) |
Add latest changes from gitlab-org/gitlab@13-8-stable-eev13.8.0-rc42
Diffstat (limited to 'lib/atlassian')
6 files changed, 318 insertions, 4 deletions
diff --git a/lib/atlassian/jira_connect/client.rb b/lib/atlassian/jira_connect/client.rb index da24d0e20ee..c67fe24d456 100644 --- a/lib/atlassian/jira_connect/client.rb +++ b/lib/atlassian/jira_connect/client.rb @@ -16,11 +16,15 @@ module Atlassian common = { project: project, update_sequence_id: update_sequence_id } dev_info = args.slice(:commits, :branches, :merge_requests) build_info = args.slice(:pipelines) + deploy_info = args.slice(:deployments) + ff_info = args.slice(:feature_flags) responses = [] responses << store_dev_info(**common, **dev_info) if dev_info.present? responses << store_build_info(**common, **build_info) if build_info.present? + responses << store_deploy_info(**common, **deploy_info) if deploy_info.present? + responses << store_ff_info(**common, **ff_info) if ff_info.present? raise ArgumentError, 'Invalid arguments' if responses.empty? responses.compact @@ -28,11 +32,47 @@ module Atlassian private + def store_ff_info(project:, feature_flags:, **opts) + return unless Feature.enabled?(:jira_sync_feature_flags, project) + + items = feature_flags.map { |flag| ::Atlassian::JiraConnect::Serializers::FeatureFlagEntity.represent(flag, opts) } + items.reject! { |item| item.issue_keys.empty? } + + return if items.empty? + + r = post('/rest/featureflags/0.1/bulk', { + flags: items, + properties: { projectId: "project-#{project.id}" } + }) + + handle_response(r, 'feature flags') do |data| + failed = data['failedFeatureFlags'] + if failed.present? + errors = failed.flat_map do |k, errs| + errs.map { |e| "#{k}: #{e['message']}" } + end + { 'errorMessages' => errors } + end + end + end + + def store_deploy_info(project:, deployments:, **opts) + return unless Feature.enabled?(:jira_sync_deployments, project) + + items = deployments.map { |d| ::Atlassian::JiraConnect::Serializers::DeploymentEntity.represent(d, opts) } + items.reject! { |d| d.issue_keys.empty? } + + return if items.empty? + + r = post('/rest/deployments/0.1/bulk', { deployments: items }) + handle_response(r, 'deployments') { |data| errors(data, 'rejectedDeployments') } + end + def store_build_info(project:, pipelines:, update_sequence_id: nil) return unless Feature.enabled?(:jira_sync_builds, project) builds = pipelines.map do |pipeline| - build = Serializers::BuildEntity.represent( + build = ::Atlassian::JiraConnect::Serializers::BuildEntity.represent( pipeline, update_sequence_id: update_sequence_id ) @@ -42,7 +82,8 @@ module Atlassian end.compact return if builds.empty? - post('/rest/builds/0.1/bulk', { builds: builds }) + r = post('/rest/builds/0.1/bulk', { builds: builds }) + handle_response(r, 'builds') { |data| errors(data, 'rejectedBuilds') } end def store_dev_info(project:, commits: nil, branches: nil, merge_requests: nil, update_sequence_id: nil) @@ -75,6 +116,34 @@ module Atlassian { providerMetadata: { product: "GitLab #{Gitlab::VERSION}" } } end + def handle_response(response, name, &block) + data = response.parsed_response + + case response.code + when 200 then yield data + when 400 then { 'errorMessages' => data.map { |e| e['message'] } } + when 401 then { 'errorMessages' => ['Invalid JWT'] } + when 403 then { 'errorMessages' => ["App does not support #{name}"] } + when 413 then { 'errorMessages' => ['Data too large'] + data.map { |e| e['message'] } } + when 429 then { 'errorMessages' => ['Rate limit exceeded'] } + when 503 then { 'errorMessages' => ['Service unavailable'] } + else + { 'errorMessages' => ['Unknown error'], 'response' => data } + end + end + + def errors(data, key) + messages = if data[key].present? + data[key].flat_map do |rejection| + rejection['errors'].map { |e| e['message'] } + end + else + [] + end + + { 'errorMessages' => messages } + end + def user_notes_count(merge_requests) return unless merge_requests diff --git a/lib/atlassian/jira_connect/serializers/build_entity.rb b/lib/atlassian/jira_connect/serializers/build_entity.rb index 3eb8b1f1978..8372d2a62da 100644 --- a/lib/atlassian/jira_connect/serializers/build_entity.rb +++ b/lib/atlassian/jira_connect/serializers/build_entity.rb @@ -25,8 +25,10 @@ module Atlassian # extract Jira issue keys from either the source branch/ref or the # merge request title. @issue_keys ||= begin - src = "#{pipeline.source_ref} #{pipeline.merge_request&.title}" - JiraIssueKeyExtractor.new(src).issue_keys + pipeline.all_merge_requests.flat_map do |mr| + src = "#{mr.source_branch} #{mr.title}" + JiraIssueKeyExtractor.new(src).issue_keys + end.uniq end end diff --git a/lib/atlassian/jira_connect/serializers/deployment_entity.rb b/lib/atlassian/jira_connect/serializers/deployment_entity.rb new file mode 100644 index 00000000000..9ef1666b61c --- /dev/null +++ b/lib/atlassian/jira_connect/serializers/deployment_entity.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +module Atlassian + module JiraConnect + module Serializers + class DeploymentEntity < Grape::Entity + include Gitlab::Routing + + format_with(:iso8601, &:iso8601) + + expose :schema_version, as: :schemaVersion + expose :iid, as: :deploymentSequenceNumber + expose :update_sequence_id, as: :updateSequenceNumber + expose :display_name, as: :displayName + expose :description + expose :associations + expose :url + expose :label + expose :state + expose :updated_at, as: :lastUpdated, format_with: :iso8601 + expose :pipeline_entity, as: :pipeline + expose :environment_entity, as: :environment + + def issue_keys + return [] unless build&.pipeline.present? + + @issue_keys ||= BuildEntity.new(build.pipeline).issue_keys + end + + private + + delegate :project, :deployable, :environment, :iid, :ref, :short_sha, to: :object + alias_method :deployment, :object + alias_method :build, :deployable + + def associations + keys = issue_keys + + [{ associationType: :issueKeys, values: keys }] if keys.present? + end + + def display_name + "Deployment #{iid} (#{ref}@#{short_sha}) to #{environment.name}" + end + + def label + "#{project.full_path}-#{environment.name}-#{iid}-#{short_sha}" + end + + def description + "Deployment #{deployment.iid} of #{project.name} at #{short_sha} (#{build&.name}) to #{environment.name}" + end + + def url + # There is no controller action to show a single deployment, so we + # link to the build instead + project_job_url(project, build) if build + end + + def state + case deployment.status + when 'created' then 'pending' + when 'running' then 'in_progress' + when 'success' then 'successful' + when 'failed' then 'failed' + when 'canceled', 'skipped' then 'cancelled' + else + 'unknown' + end + end + + def schema_version + '1.0' + end + + def pipeline_entity + PipelineEntity.new(build.pipeline) if build&.pipeline.present? + end + + def environment_entity + EnvironmentEntity.new(environment) + end + + def update_sequence_id + options[:update_sequence_id] || Client.generate_update_sequence_id + end + end + end + end +end diff --git a/lib/atlassian/jira_connect/serializers/environment_entity.rb b/lib/atlassian/jira_connect/serializers/environment_entity.rb new file mode 100644 index 00000000000..f3699e4d0ee --- /dev/null +++ b/lib/atlassian/jira_connect/serializers/environment_entity.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Atlassian + module JiraConnect + module Serializers + class EnvironmentEntity < Grape::Entity + format_with(:string, &:to_s) + + expose :id, format_with: :string + expose :display_name, as: :displayName + expose :type + + private + + alias_method :environment, :object + delegate :project, to: :object + + def display_name + "#{project.name}/#{environment.name}" + end + + def type + case environment.name + when /prod/i + 'production' + when /test/i + 'testing' + when /staging/i + 'staging' + when /(dev|review)/i + 'development' + else + 'unmapped' + end + end + end + end + end +end diff --git a/lib/atlassian/jira_connect/serializers/feature_flag_entity.rb b/lib/atlassian/jira_connect/serializers/feature_flag_entity.rb new file mode 100644 index 00000000000..e17c150aacb --- /dev/null +++ b/lib/atlassian/jira_connect/serializers/feature_flag_entity.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +module Atlassian + module JiraConnect + module Serializers + class FeatureFlagEntity < Grape::Entity + include Gitlab::Routing + + alias_method :flag, :object + + format_with(:string, &:to_s) + + expose :schema_version, as: :schemaVersion + expose :id, format_with: :string + expose :name, as: :key + expose :update_sequence_id, as: :updateSequenceId + expose :name, as: :displayName + expose :summary + expose :details + expose :issue_keys, as: :issueKeys + + def issue_keys + @issue_keys ||= JiraIssueKeyExtractor.new(flag.description).issue_keys + end + + def schema_version + '1.0' + end + + def update_sequence_id + options[:update_sequence_id] || Client.generate_update_sequence_id + end + + STRATEGY_NAMES = { + ::Operations::FeatureFlags::Strategy::STRATEGY_DEFAULT => 'All users', + ::Operations::FeatureFlags::Strategy::STRATEGY_GITLABUSERLIST => 'User List', + ::Operations::FeatureFlags::Strategy::STRATEGY_GRADUALROLLOUTUSERID => 'Percent of users', + ::Operations::FeatureFlags::Strategy::STRATEGY_FLEXIBLEROLLOUT => 'Percent rollout', + ::Operations::FeatureFlags::Strategy::STRATEGY_USERWITHID => 'User IDs' + }.freeze + + private + + # The summary does not map very well to our FeatureFlag model. + # + # We allow feature flags to have multiple strategies, depending + # on the environment. Jira expects a single rollout strategy. + # + # Also, we don't actually support showing a single flag, so we use the + # edit path as an interim solution. + def summary(strategies = flag.strategies) + { + url: project_url(flag.project) + "/-/feature_flags/#{flag.id}/edit", + lastUpdated: flag.updated_at.iso8601, + status: { + enabled: flag.active, + defaultValue: '', + rollout: { + percentage: strategies.map do |s| + s.parameters['rollout'] || s.parameters['percentage'] + end.compact.first&.to_f, + text: strategies.map { |s| STRATEGY_NAMES[s.name] }.compact.join(', ') + }.compact + } + } + end + + def details + envs = flag.strategies.flat_map do |s| + s.scopes.map do |es| + env_type = es.environment_scope.scan(/development|testing|staging|production/).first + [es.environment_scope, env_type, s] + end + end + + envs.map do |env_name, env_type, strat| + summary([strat]).merge(environment: { name: env_name, type: env_type }.compact) + end + end + end + end + end +end diff --git a/lib/atlassian/jira_connect/serializers/pipeline_entity.rb b/lib/atlassian/jira_connect/serializers/pipeline_entity.rb new file mode 100644 index 00000000000..e67cf1a7229 --- /dev/null +++ b/lib/atlassian/jira_connect/serializers/pipeline_entity.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Atlassian + module JiraConnect + module Serializers + # Both this an BuildEntity represent a Ci::Pipeline + class PipelineEntity < Grape::Entity + include Gitlab::Routing + + format_with(:string, &:to_s) + + expose :id, format_with: :string + expose :display_name, as: :displayName + expose :url + + private + + alias_method :pipeline, :object + delegate :project, to: :object + + def display_name + "#{project.name} pipeline #{pipeline.iid}" + end + + def url + project_pipeline_url(project, pipeline) + end + end + end + end +end |