forked from runatlantis/atlantis
/
gitlab_client.go
257 lines (227 loc) · 8.53 KB
/
gitlab_client.go
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
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
// Copyright 2017 HootSuite Media Inc.
//
// Licensed under the Apache License, Version 2.0 (the License);
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an AS IS BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Modified hereafter by contributors to runatlantis/atlantis.
package vcs
import (
"fmt"
"net"
"net/url"
"strings"
"github.com/runatlantis/atlantis/server/events/vcs/common"
"github.com/hashicorp/go-version"
"github.com/pkg/errors"
"github.com/runatlantis/atlantis/server/logging"
"github.com/lkysow/go-gitlab"
"github.com/runatlantis/atlantis/server/events/models"
)
type GitlabClient struct {
Client *gitlab.Client
// Version is set to the server version.
Version *version.Version
}
// commonMarkSupported is a version constraint that is true when this version of
// GitLab supports CommonMark, a markdown specification.
// See https://about.gitlab.com/2018/07/22/gitlab-11-1-released/
var commonMarkSupported = MustConstraint(">=11.1")
// gitlabClientUnderTest is true if we're running under go test.
var gitlabClientUnderTest = false
// NewGitlabClient returns a valid GitLab client.
func NewGitlabClient(hostname string, token string, logger *logging.SimpleLogger) (*GitlabClient, error) {
client := &GitlabClient{
Client: gitlab.NewClient(nil, token),
}
// If not using gitlab.com we need to set the URL to the API.
if hostname != "gitlab.com" {
// We assume the url will be over HTTPS if the user doesn't specify a scheme.
absoluteURL := hostname
if !strings.HasPrefix(hostname, "http://") && !strings.HasPrefix(hostname, "https://") {
absoluteURL = "https://" + absoluteURL
}
url, err := url.Parse(absoluteURL)
if err != nil {
return nil, errors.Wrapf(err, "parsing URL %q", absoluteURL)
}
// Warn if this hostname isn't resolvable. The GitLab client
// doesn't give good error messages in this case.
ips, err := net.LookupIP(url.Hostname())
if err != nil {
logger.Warn("unable to resolve %q: %s", url.Hostname(), err)
} else if len(ips) == 0 {
logger.Warn("found no IPs while resolving %q", url.Hostname())
}
// Now we're ready to construct the client.
absoluteURL = strings.TrimSuffix(absoluteURL, "/")
apiURL := fmt.Sprintf("%s/api/v4/", absoluteURL)
if err := client.Client.SetBaseURL(apiURL); err != nil {
return nil, errors.Wrapf(err, "setting GitLab API URL: %s", apiURL)
}
}
// Determine which version of GitLab is running.
if !gitlabClientUnderTest {
var err error
client.Version, err = client.GetVersion()
if err != nil {
return nil, err
}
logger.Info("determined GitLab is running version %s", client.Version.String())
}
return client, nil
}
// GetModifiedFiles returns the names of files that were modified in the merge request.
// The names include the path to the file from the repo root, ex. parent/child/file.txt.
func (g *GitlabClient) GetModifiedFiles(repo models.Repo, pull models.PullRequest) ([]string, error) {
const maxPerPage = 100
var files []string
nextPage := 1
// Constructing the api url by hand so we can do pagination.
apiURL := fmt.Sprintf("projects/%s/merge_requests/%d/changes", url.QueryEscape(repo.FullName), pull.Num)
for {
opts := gitlab.ListOptions{
Page: nextPage,
PerPage: maxPerPage,
}
req, err := g.Client.NewRequest("GET", apiURL, opts, nil)
if err != nil {
return nil, err
}
mr := new(gitlab.MergeRequest)
resp, err := g.Client.Do(req, mr)
if err != nil {
return nil, err
}
for _, f := range mr.Changes {
files = append(files, f.NewPath)
// If the file was renamed, we'll want to run plan in the directory
// it was moved from as well.
if f.RenamedFile {
files = append(files, f.OldPath)
}
}
if resp.NextPage == 0 {
break
}
nextPage = resp.NextPage
}
return files, nil
}
// CreateComment creates a comment on the merge request.
func (g *GitlabClient) CreateComment(repo models.Repo, pullNum int, comment string) error {
_, _, err := g.Client.Notes.CreateMergeRequestNote(repo.FullName, pullNum, &gitlab.CreateMergeRequestNoteOptions{Body: gitlab.String(comment)})
return err
}
// PullIsApproved returns true if the merge request was approved.
func (g *GitlabClient) PullIsApproved(repo models.Repo, pull models.PullRequest) (bool, error) {
approvals, _, err := g.Client.MergeRequests.GetMergeRequestApprovals(repo.FullName, pull.Num)
if err != nil {
return false, err
}
if approvals.ApprovalsLeft > 0 {
return false, nil
}
return true, nil
}
// PullIsMergeable returns true if the merge request can be merged.
// In GitLab, there isn't a single field that tells us if the pull request is
// mergeable so for now we check the merge_status and approvals_before_merge
// fields. We aren't checking if there are unresolved discussions or failing
// pipelines because those only block merges if the repo is set to require that.
// In order to check if the repo required these, we'd need to make another API
// call to get the repo settings. For now I'm going to leave this as is and if
// some users require checking this as well then we can revisit.
// It's also possible that GitLab implements their own "mergeable" field in
// their API in the future.
// See:
// - https://gitlab.com/gitlab-org/gitlab-ee/issues/3169
// - https://gitlab.com/gitlab-org/gitlab-ce/issues/42344
func (g *GitlabClient) PullIsMergeable(repo models.Repo, pull models.PullRequest) (bool, error) {
mr, _, err := g.Client.MergeRequests.GetMergeRequest(repo.FullName, pull.Num)
if err != nil {
return false, err
}
if mr.MergeStatus == "can_be_merged" && mr.ApprovalsBeforeMerge <= 0 {
return true, nil
}
return false, nil
}
// UpdateStatus updates the build status of a commit.
func (g *GitlabClient) UpdateStatus(repo models.Repo, pull models.PullRequest, state models.CommitStatus, description string) error {
const statusContext = "Atlantis"
gitlabState := gitlab.Failed
switch state {
case models.PendingCommitStatus:
gitlabState = gitlab.Pending
case models.FailedCommitStatus:
gitlabState = gitlab.Failed
case models.SuccessCommitStatus:
gitlabState = gitlab.Success
}
_, _, err := g.Client.Commits.SetCommitStatus(repo.FullName, pull.HeadCommit, &gitlab.SetCommitStatusOptions{
State: gitlabState,
Context: gitlab.String(statusContext),
Description: gitlab.String(description),
})
return err
}
func (g *GitlabClient) GetMergeRequest(repoFullName string, pullNum int) (*gitlab.MergeRequest, error) {
mr, _, err := g.Client.MergeRequests.GetMergeRequest(repoFullName, pullNum)
return mr, err
}
// MergePull merges the merge request.
func (g *GitlabClient) MergePull(pull models.PullRequest) error {
commitMsg := common.AutomergeCommitMsg
_, _, err := g.Client.MergeRequests.AcceptMergeRequest(
pull.BaseRepo.FullName,
pull.Num,
&gitlab.AcceptMergeRequestOptions{
MergeCommitMessage: &commitMsg,
})
return errors.Wrap(err, "unable to merge merge request, it may not be in a mergeable state")
}
// GetVersion returns the version of the Gitlab server this client is using.
func (g *GitlabClient) GetVersion() (*version.Version, error) {
req, err := g.Client.NewRequest("GET", "/version", nil, nil)
if err != nil {
return nil, err
}
versionResp := new(gitlab.Version)
_, err = g.Client.Do(req, versionResp)
if err != nil {
return nil, err
}
// We need to strip any "-ee" or similar from the resulting version because go-version
// uses that in its constraints and it breaks the comparison we're trying
// to do for Common Mark.
split := strings.Split(versionResp.Version, "-")
parsedVersion, err := version.NewVersion(split[0])
if err != nil {
return nil, errors.Wrapf(err, "parsing response to /version: %q", versionResp.Version)
}
return parsedVersion, nil
}
// SupportsCommonMark returns true if the version of Gitlab this client is
// using supports the CommonMark markdown format.
func (g *GitlabClient) SupportsCommonMark() bool {
// This function is called even if we didn't construct a gitlab client
// so we need to handle that case.
if g == nil {
return false
}
return commonMarkSupported.Check(g.Version)
}
// MustConstraint returns a constraint. It panics on error.
func MustConstraint(constraint string) version.Constraints {
c, err := version.NewConstraint(constraint)
if err != nil {
panic(err)
}
return c
}