summaryrefslogtreecommitdiff
path: root/app/models/integrations/teamcity.rb
blob: f0f83f118d7e4feffc557c0ee5a5c7fd8d8247cb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# frozen_string_literal: true

module Integrations
  class Teamcity < BaseCi
    include PushDataValidations
    include ReactivelyCached
    prepend EnableSslVerification

    TEAMCITY_SAAS_HOSTNAME = /\A[^\.]+\.teamcity\.com\z/i.freeze

    prop_accessor :teamcity_url, :build_type, :username, :password

    validates :teamcity_url, presence: true, public_url: true, if: :activated?
    validates :build_type, presence: true, if: :activated?
    validates :username,
      presence: true,
      if: ->(service) { service.activated? && service.password }
    validates :password,
      presence: true,
      if: ->(service) { service.activated? && service.username }

    attr_accessor :response

    before_validation :reset_password

    class << self
      def to_param
        'teamcity'
      end

      def supported_events
        %w(push merge_request)
      end
    end

    def reset_password
      if teamcity_url_changed? && !password_touched?
        self.password = nil
      end
    end

    def title
      'JetBrains TeamCity'
    end

    def description
      s_('ProjectService|Run CI/CD pipelines with JetBrains TeamCity.')
    end

    def help
      s_('To run CI/CD pipelines with JetBrains TeamCity, input the GitLab project details in the TeamCity project Version Control Settings.')
    end

    def fields
      [
        {
          type: 'text',
          name: 'teamcity_url',
          title: s_('ProjectService|TeamCity server URL'),
          placeholder: 'https://teamcity.example.com',
          required: true
        },
        {
          type: 'text',
          name: 'build_type',
          help: s_('ProjectService|The build configuration ID of the TeamCity project.'),
          required: true
        },
        {
          type: 'text',
          name: 'username',
          help: s_('ProjectService|Must have permission to trigger a manual build in TeamCity.')
        },
        {
          type: 'password',
          name: 'password',
          non_empty_password_title: s_('ProjectService|Enter new password'),
          non_empty_password_help: s_('ProjectService|Leave blank to use your current password')
        }
      ]
    end

    def build_page(sha, ref)
      with_reactive_cache(sha, ref) {|cached| cached[:build_page] }
    end

    def commit_status(sha, ref)
      with_reactive_cache(sha, ref) {|cached| cached[:commit_status] }
    end

    def calculate_reactive_cache(sha, ref)
      response = get_path("httpAuth/app/rest/builds/branch:unspecified:any,revision:#{sha}")

      if response
        { build_page: read_build_page(response), commit_status: read_commit_status(response) }
      else
        { build_page: teamcity_url, commit_status: :error }
      end
    end

    def execute(data)
      case data[:object_kind]
      when 'push'
        execute_push(data)
      when 'merge_request'
        execute_merge_request(data)
      end
    end

    def enable_ssl_verification
      original_value = Gitlab::Utils.to_boolean(properties['enable_ssl_verification'])
      original_value.nil? ? (new_record? || url_is_saas?) : original_value
    end

    private

    def url_is_saas?
      parsed_url = Addressable::URI.parse(teamcity_url)
      parsed_url&.scheme == 'https' && parsed_url.hostname.match?(TEAMCITY_SAAS_HOSTNAME)
    rescue Addressable::URI::InvalidURIError
      false
    end

    def execute_push(data)
      branch = Gitlab::Git.ref_name(data[:ref])
      post_to_build_queue(data, branch) if push_valid?(data)
    end

    def execute_merge_request(data)
      branch = data[:object_attributes][:source_branch]
      post_to_build_queue(data, branch) if merge_request_valid?(data)
    end

    def read_build_page(response)
      if response.code != 200
        # If actual build link can't be determined,
        # send user to build summary page.
        build_url("viewLog.html?buildTypeId=#{build_type}")
      else
        # If actual build link is available, go to build result page.
        built_id = response['build']['id']
        build_url("viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}")
      end
    end

    def read_commit_status(response)
      return :error unless response.code == 200 || response.code == 404

      status = if response.code == 404
                 'Pending'
               else
                 response['build']['status']
               end

      return :error unless status.present?

      if status.include?('SUCCESS')
        'success'
      elsif status.include?('FAILURE')
        'failed'
      elsif status.include?('Pending')
        'pending'
      else
        :error
      end
    end

    def build_url(path)
      Gitlab::Utils.append_path(teamcity_url, path)
    end

    def get_path(path)
      Gitlab::HTTP.try_get(build_url(path), verify: enable_ssl_verification, basic_auth: basic_auth, extra_log_info: { project_id: project_id }, use_read_total_timeout: true)
    end

    def post_to_build_queue(data, branch)
      Gitlab::HTTP.post(
        build_url('httpAuth/app/rest/buildQueue'),
        body: "<build branchName=#{branch.encode(xml: :attr)}>"\
              "<buildType id=#{build_type.encode(xml: :attr)}/>"\
              '</build>',
        headers: { 'Content-type' => 'application/xml' },
        verify: enable_ssl_verification,
        basic_auth: basic_auth,
        use_read_total_timeout: true
      )
    end

    def basic_auth
      { username: username, password: password }
    end
  end
end