Skip to content

Commit ff26ba4

Browse files
author
Juan José Vázquez
committed
Implements Gitlab integration (fixes #43)
1 parent 1bf8a4d commit ff26ba4

27 files changed

+642
-124
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
--: ----------------------------------------------------------------------------
2+
--: Copyright (C) 2017 Verizon. All Rights Reserved.
3+
--:
4+
--: Licensed under the Apache License, Version 2.0 (the "License");
5+
--: you may not use this file except in compliance with the License.
6+
--: You may obtain a copy of the License at
7+
--:
8+
--: http://www.apache.org/licenses/LICENSE-2.0
9+
--:
10+
--: Unless required by applicable law or agreed to in writing, software
11+
--: distributed under the License is distributed on an "AS IS" BASIS,
12+
--: WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
--: See the License for the specific language governing permissions and
14+
--: limitations under the License.
15+
--:
16+
--: ----------------------------------------------------------------------------
17+
ALTER TABLE PUBLIC."audit_log" ALTER COLUMN "release_id" VARCHAR(25);

core/src/main/scala/AccessToken.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@
1717
package nelson
1818

1919
final case class AccessToken(
20-
value: String
20+
value: String,
21+
isPrivate: Boolean = false
2122
)

core/src/main/scala/Config.scala

Lines changed: 148 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,17 @@ import java.nio.file.{Path, Paths}
2222
import java.security.SecureRandom
2323
import java.security.cert.{CertificateFactory, X509Certificate}
2424
import java.util.concurrent.{ExecutorService, Executors, ScheduledExecutorService, ThreadFactory}
25-
2625
import journal.Logger
2726
import nelson.BannedClientsConfig.HttpUserAgent
2827
import nelson.cleanup.ExpirationPolicy
28+
import nelson.Github.GithubOp
2929
import org.http4s.Uri
3030
import org.http4s.client.Client
3131
import org.http4s.client.blaze._
3232
import storage.StoreOp
33-
import logging.{WorkflowLogger,LoggingOp}
34-
import audit.{Auditor,AuditEvent}
35-
import notifications.{SlackHttp,SlackOp,EmailOp,EmailServer}
36-
33+
import logging.{LoggingOp, WorkflowLogger}
34+
import audit.{AuditEvent, Auditor}
35+
import notifications.{EmailOp, EmailServer, SlackHttp, SlackOp}
3736
import scala.concurrent.ExecutionContext
3837
import scala.concurrent.duration._
3938
import scala.util.control.NonFatal
@@ -45,57 +44,139 @@ import scheduler.SchedulerOp
4544
import vault._
4645
import vault.http4s._
4746

48-
/**
49-
*
50-
*/
51-
final case class GithubConfig(
52-
domain: Option[String],
53-
clientId: String,
54-
clientSecret: String,
55-
redirectUri: String,
56-
scope: String,
57-
systemAccessToken: AccessToken,
58-
systemUsername: String,
59-
organizationBlacklist: List[String],
60-
organizationAdminList: List[String]
61-
){
47+
sealed abstract class ScmConfig extends Product with Serializable {
48+
def domain: Option[String]
49+
def clientId: String
50+
def clientSecret: String
51+
def redirectUri: String
52+
def scope: String
53+
def systemAccessToken: AccessToken
54+
def systemUsername: String
55+
def organizationBlacklist: List[String]
56+
def organizationAdminList: List[String]
6257
def isEnterprise: Boolean = domain.nonEmpty
58+
def base: String
59+
def oauth: String
60+
def api: String
61+
def tokenEndpoint: String
62+
def loginEndpoint: String
63+
def userEndpoint: String
64+
def userOrgsEndpoint: String
65+
def orgEndpoint(login: String): String
66+
def repoEndpoint(page: Int = 1): String
67+
def webhookEndpoint(slug: Slug): String
68+
def contentsEndpoint(slug: Slug, path: String): String
69+
def releaseEndpoint(slug: Slug, releaseId: String): String
70+
71+
def withOrganizationAdminList(l: List[String]): ScmConfig
72+
73+
private [nelson] def encodeURI(uri: String): String =
74+
java.net.URLEncoder.encode(uri, "UTF-8")
75+
}
76+
object ScmConfig {
77+
final case class GithubConfig(
78+
domain: Option[String],
79+
clientId: String,
80+
clientSecret: String,
81+
redirectUri: String,
82+
scope: String,
83+
systemAccessToken: AccessToken,
84+
systemUsername: String,
85+
organizationBlacklist: List[String],
86+
organizationAdminList: List[String]) extends ScmConfig {
6387

64-
val oauth =
65-
"https://"+ domain.fold("github.com")(identity)
88+
val base: String =
89+
"https://"+ domain.fold("github.com")(identity)
6690

67-
val api =
68-
"https://"+ domain.fold("api.github.com")(_+"/api/v3")
91+
val oauth: String =
92+
base
6993

70-
val tokenEndpoint =
71-
s"${oauth}/login/oauth/access_token"
94+
val api: String =
95+
"https://"+ domain.fold("api.github.com")(_+"/api/v3")
7296

73-
val loginEndpoint =
74-
s"${oauth}/login/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURI(redirectUri)}&scope=${scope}"
97+
val tokenEndpoint: String =
98+
s"$oauth/login/oauth/access_token"
7599

76-
val userEndpoint =
77-
s"${api}/user"
100+
val loginEndpoint: String =
101+
s"$oauth/login/oauth/authorize?client_id=$clientId&redirect_uri=${encodeURI(redirectUri)}&scope=$scope"
78102

79-
val userOrgsEndpoint =
80-
s"${userEndpoint}/orgs"
103+
val userEndpoint: String =
104+
s"$api/user"
81105

82-
def orgEndpoint(login: String) =
83-
s"${api}/orgs/${login}"
106+
val userOrgsEndpoint: String =
107+
s"$userEndpoint/orgs"
84108

85-
def repoEndpoint(page: Int = 1) =
86-
s"${api}/user/repos?affiliation=owner,organization_member&visibility=all&direction=asc&page=${page}"
109+
def orgEndpoint(login: String): String =
110+
s"$api/orgs/$login"
87111

88-
def webhookEndpoint(slug: Slug) =
89-
s"${api}/repos/${slug}/hooks"
112+
def repoEndpoint(page: Int = 1): String =
113+
s"$api/user/repos?affiliation=owner,organization_member&visibility=all&direction=asc&page=$page"
90114

91-
def contentsEndpoint(slug: Slug, path: String) =
92-
s"${api}/repos/${slug}/contents/${path}"
115+
def webhookEndpoint(slug: Slug): String =
116+
s"$api/repos/$slug/hooks"
93117

94-
def releaseEndpoint(slug: Slug, releaseId: Long) =
95-
s"${api}/repos/${slug}/releases/${releaseId}"
118+
def contentsEndpoint(slug: Slug, path: String): String =
119+
s"$api/repos/$slug/contents/$path"
96120

97-
private [nelson] def encodeURI(uri: String): String =
98-
java.net.URLEncoder.encode(uri, "UTF-8")
121+
def releaseEndpoint(slug: Slug, releaseId: String): String =
122+
s"$api/repos/$slug/releases/$releaseId"
123+
124+
def withOrganizationAdminList(l: List[String]): ScmConfig =
125+
this.copy(organizationAdminList = l)
126+
}
127+
128+
final case class GitlabConfig(
129+
domain: Option[String],
130+
clientId: String,
131+
clientSecret: String,
132+
redirectUri: String,
133+
scope: String,
134+
systemAccessToken: AccessToken,
135+
systemUsername: String,
136+
organizationBlacklist: List[String],
137+
organizationAdminList: List[String]) extends ScmConfig {
138+
139+
val base: String =
140+
"https://" + domain.fold("gitlab.com")(identity)
141+
142+
val oauth: String =
143+
base + "/oauth"
144+
145+
val api: String =
146+
base + "/api/v4"
147+
148+
val tokenEndpoint: String =
149+
s"$oauth/token"
150+
151+
val loginEndpoint: String =
152+
s"$oauth/authorize?client_id=$clientId&redirect_uri=${encodeURI(redirectUri)}&response_type=code"
153+
154+
val userEndpoint: String =
155+
s"$api/user"
156+
157+
val userOrgsEndpoint: String =
158+
s"$api/groups"
159+
160+
def orgEndpoint(login: String): String =
161+
s"$api/groups/$login"
162+
163+
def repoEndpoint(page: Int = 1): String =
164+
s"$api/projects?page=$page"
165+
166+
def webhookEndpoint(slug: Slug): String =
167+
s"$api/projects/${ urlEncode(slug.toString) }/hooks"
168+
169+
def contentsEndpoint(slug: Slug, path: String): String =
170+
s"$api/projects/${ urlEncode(slug.toString) }/repository/files/$path"
171+
172+
def releaseEndpoint(slug: Slug, releaseId: String): String =
173+
s"$api/projects/${ urlEncode(slug.toString) }/repository/tags/$releaseId"
174+
175+
def withOrganizationAdminList(l: List[String]): ScmConfig =
176+
this.copy(organizationAdminList = l)
177+
178+
private[this] def urlEncode(s: String) = java.net.URLEncoder.encode(s, "UTF-8")
179+
}
99180
}
100181

101182
/**
@@ -316,7 +397,7 @@ final case class PolicyConfig(
316397
* actually cares about.
317398
*/
318399
final case class NelsonConfig(
319-
git: GithubConfig,
400+
git: ScmConfig,
320401
network: NetworkConfig,
321402
security: SecurityConfig,
322403
database: DatabaseConfig,
@@ -400,7 +481,12 @@ object Config {
400481
val nomadcfg = readNomad(cfg.subconfig("nelson.nomad"))
401482

402483
val gitcfg = readGithub(cfg.subconfig("nelson.github"))
403-
val git = new Github.GithubHttp(gitcfg, http0)
484+
val git: GithubOp ~> Task = gitcfg match {
485+
case _: ScmConfig.GithubConfig =>
486+
new Github.GithubHttp(gitcfg, http0)
487+
case _ =>
488+
new Gitlab.GitlabHttp(gitcfg, http4sClient(timeout))
489+
}
404490

405491
val workflowConf = readWorkflowLogger(cfg.subconfig("nelson.workflow-logger"))
406492
val workflowlogger = new WorkflowLogger(
@@ -754,18 +840,24 @@ object Config {
754840
monitoringPort = cfg.require[Int]("monitoring-port")
755841
)
756842

757-
private def readGithub(cfg: KConfig): GithubConfig =
758-
GithubConfig(
759-
domain = cfg.lookup[String]("domain"),
760-
clientId = cfg.require[String]("client-id"),
761-
clientSecret = cfg.require[String]("client-secret"),
762-
redirectUri = cfg.require[String]("redirect-uri"),
763-
scope = cfg.require[String]("scope"),
764-
systemAccessToken = AccessToken(cfg.require[String]("access-token")),
765-
systemUsername = cfg.require[String]("system-username"),
766-
organizationBlacklist = cfg.lookup[List[String]]("organization-blacklist").getOrElse(Nil),
767-
organizationAdminList = cfg.lookup[List[String]]("organization-admins").getOrElse(Nil)
843+
private def readGithub(cfg: KConfig): ScmConfig = {
844+
val service = cfg.lookup[String]("service").getOrElse("github")
845+
val attrs = (
846+
cfg.lookup[String]("domain"),
847+
cfg.require[String]("client-id"),
848+
cfg.require[String]("client-secret"),
849+
cfg.require[String]("redirect-uri"),
850+
cfg.require[String]("scope"),
851+
AccessToken(cfg.require[String]("access-token"), isPrivate = true),
852+
cfg.require[String]("system-username"),
853+
cfg.lookup[List[String]]("organization-blacklist").getOrElse(Nil),
854+
cfg.lookup[List[String]]("organization-admins").getOrElse(Nil)
768855
)
856+
import ScmConfig._
857+
val confBuilder =
858+
if (service == "github") GithubConfig else GitlabConfig
859+
confBuilder.tupled(attrs)
860+
}
769861

770862
private def readSlack(cfg: KConfig): Option[SlackConfig] = {
771863
for {

core/src/main/scala/Github.scala

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,13 @@ object Github {
7676
) extends Event
7777

7878
final case class ReleaseEvent(
79-
id: Long,
79+
id: String,
8080
slug: Slug,
8181
repositoryId: Long
8282
) extends Event
8383

8484
final case class Release(
85-
id: Long,
85+
id: String,
8686
url: String,
8787
htmlUrl: String,
8888
assets: Seq[Asset],
@@ -129,7 +129,7 @@ object Github {
129129
final case class GetReleaseAssetContent(asset: Github.Asset, t: AccessToken)
130130
extends GithubOp[Github.Asset]
131131

132-
final case class GetRelease(slug: Slug, releaseId: ID, t: AccessToken)
132+
final case class GetRelease(slug: Slug, releaseId: String, t: AccessToken)
133133
extends GithubOp[Github.Release]
134134

135135
final case class GetUserRepositories(token: AccessToken)
@@ -178,7 +178,7 @@ object Github {
178178
* nelson gets notified of a release, as the payload we get does not contain
179179
* the assets that we need.
180180
*/
181-
def fetchRelease(slug: Slug, id: ID)(t: AccessToken): GithubOpF[Github.Release] =
181+
def fetchRelease(slug: Slug, id: String)(t: AccessToken): GithubOpF[Github.Release] =
182182
for {
183183
r <- Free.liftFC(GetRelease(slug, id, t))
184184
a <- fetchReleaseAssets(r)(t)
@@ -226,7 +226,7 @@ object Github {
226226
}
227227
}
228228

229-
final class GithubHttp(cfg: GithubConfig, http: dispatch.Http) extends (GithubOp ~> Task) {
229+
final class GithubHttp(cfg: ScmConfig, http: dispatch.Http) extends (GithubOp ~> Task) {
230230
import java.net.URI
231231
import dispatch._, Defaults._
232232
import nelson.Json._
@@ -272,7 +272,7 @@ object Github {
272272
b <- http(url(a) OK as.String).toTask
273273
} yield asset.copy(content = Option(b))
274274

275-
case GetRelease(slug: Slug, releaseId: ID, t: AccessToken) =>
275+
case GetRelease(slug: Slug, releaseId: String, t: AccessToken) =>
276276
for {
277277
resp <- fetch(cfg.releaseEndpoint(slug, releaseId), t)
278278
rel <- fromJson[Github.Release](resp)

0 commit comments

Comments
 (0)