ci.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. package gitlab
  2. import (
  3. "fmt"
  4. "net/http"
  5. "net/url"
  6. "strings"
  7. "github.com/porter-dev/porter/api/server/shared/commonutils"
  8. "github.com/porter-dev/porter/api/server/shared/config"
  9. "github.com/porter-dev/porter/internal/oauth"
  10. "github.com/porter-dev/porter/internal/repository"
  11. "github.com/xanzy/go-gitlab"
  12. "gopkg.in/yaml.v2"
  13. )
  14. type GitlabCI struct {
  15. ServerURL string
  16. GitRepoName string
  17. GitRepoOwner string
  18. GitBranch string
  19. Repo repository.Repository
  20. ProjectID uint
  21. ClusterID uint
  22. UserID uint
  23. IntegrationID uint
  24. PorterConf *config.Config
  25. ReleaseName string
  26. ReleaseNamespace string
  27. FolderPath string
  28. PorterToken string
  29. defaultGitBranch string
  30. pID string
  31. gitlabInstanceURL string
  32. }
  33. func (g *GitlabCI) Setup() error {
  34. client, err := g.getClient()
  35. if err != nil {
  36. return err
  37. }
  38. g.pID = fmt.Sprintf("%s/%s", g.GitRepoOwner, g.GitRepoName)
  39. branches, _, err := client.Branches.ListBranches(g.pID, &gitlab.ListBranchesOptions{})
  40. if err != nil {
  41. return fmt.Errorf("error fetching list of branches: %w", err)
  42. }
  43. for _, branch := range branches {
  44. if branch.Default {
  45. g.defaultGitBranch = branch.Name
  46. break
  47. }
  48. }
  49. err = g.createGitlabSecret(client)
  50. if err != nil {
  51. return err
  52. }
  53. jobName := getGitlabStageJobName(g.ReleaseName)
  54. ciFile, resp, err := client.RepositoryFiles.GetRawFile(g.pID, ".gitlab-ci.yml", &gitlab.GetRawFileOptions{
  55. Ref: gitlab.String(g.defaultGitBranch),
  56. })
  57. if resp.StatusCode == http.StatusNotFound {
  58. // create .gitlab-ci.yml
  59. contentsMap := make(map[string]interface{})
  60. contentsMap["stages"] = []string{
  61. jobName,
  62. }
  63. contentsMap[jobName] = g.getCIJob(jobName)
  64. contentsYAML, _ := yaml.Marshal(contentsMap)
  65. _, _, err = client.RepositoryFiles.CreateFile(g.pID, ".gitlab-ci.yml", &gitlab.CreateFileOptions{
  66. Branch: gitlab.String(g.defaultGitBranch),
  67. AuthorName: gitlab.String("Porter Bot"),
  68. AuthorEmail: gitlab.String("contact@getporter.dev"),
  69. Content: gitlab.String(string(contentsYAML)),
  70. CommitMessage: gitlab.String("Create .gitlab-ci.yml file"),
  71. })
  72. if err != nil {
  73. return fmt.Errorf("error creating .gitlab-ci.yml file: %w", err)
  74. }
  75. } else if err != nil {
  76. return fmt.Errorf("error getting .gitlab-ci.yml file: %w", err)
  77. } else {
  78. // update .gitlab-ci.yml if needed
  79. // to preserve the order of the YAML, we use a MapSlice
  80. ciFileContentsMap := yaml.MapSlice{}
  81. err = yaml.Unmarshal(ciFile, &ciFileContentsMap)
  82. if err != nil {
  83. return fmt.Errorf("error unmarshalling existing .gitlab-ci.yml: %w", err)
  84. }
  85. var stagesInt []interface{}
  86. stagesIdx := -1
  87. for idx, elem := range ciFileContentsMap {
  88. if key, ok := elem.Key.(string); ok {
  89. if key == "stages" {
  90. stages, ok := elem.Value.([]interface{})
  91. if !ok {
  92. return fmt.Errorf("error converting stages to interface slice")
  93. }
  94. stagesInt = stages
  95. stagesIdx = idx
  96. break
  97. }
  98. } else {
  99. return fmt.Errorf("invalid key '%v' in .gitlab-ci.yml", elem.Key)
  100. }
  101. }
  102. // two cases can happen here:
  103. // 1: "stages" exists
  104. // 2: "stages" does not exist
  105. if stagesIdx >= 0 { // 1: "stages" exists
  106. stageExists := false
  107. for _, stage := range stagesInt {
  108. stageStr, ok := stage.(string)
  109. if !ok {
  110. return fmt.Errorf("error converting from interface to string")
  111. }
  112. if stageStr == jobName {
  113. stageExists = true
  114. break
  115. }
  116. }
  117. if !stageExists {
  118. stagesInt = append(stagesInt, jobName)
  119. ciFileContentsMap[stagesIdx] = yaml.MapItem{
  120. Key: "stages",
  121. Value: stagesInt,
  122. }
  123. }
  124. } else { // 2: "stages" does not exist
  125. stagesInt = append(stagesInt, jobName)
  126. ciFileContentsMap = append(ciFileContentsMap, yaml.MapItem{
  127. Key: "stages",
  128. Value: stagesInt,
  129. })
  130. }
  131. ciFileContentsMap = append(ciFileContentsMap, yaml.MapItem{
  132. Key: jobName,
  133. Value: g.getCIJob(jobName),
  134. })
  135. contentsYAML, err := yaml.Marshal(ciFileContentsMap)
  136. if err != nil {
  137. return fmt.Errorf("error marshalling contents of .gitlab-ci.yml while updating to add porter job")
  138. }
  139. _, _, err = client.RepositoryFiles.UpdateFile(g.pID, ".gitlab-ci.yml", &gitlab.UpdateFileOptions{
  140. Branch: gitlab.String(g.defaultGitBranch),
  141. AuthorName: gitlab.String("Porter Bot"),
  142. AuthorEmail: gitlab.String("contact@getporter.dev"),
  143. Content: gitlab.String(string(contentsYAML)),
  144. CommitMessage: gitlab.String("Update .gitlab-ci.yml file"),
  145. })
  146. if err != nil {
  147. return fmt.Errorf("error updating .gitlab-ci.yml file to add porter job: %w", err)
  148. }
  149. }
  150. return nil
  151. }
  152. func (g *GitlabCI) Cleanup() error {
  153. client, err := g.getClient()
  154. if err != nil {
  155. return err
  156. }
  157. g.pID = fmt.Sprintf("%s/%s", g.GitRepoOwner, g.GitRepoName)
  158. branches, _, err := client.Branches.ListBranches(g.pID, &gitlab.ListBranchesOptions{})
  159. if err != nil {
  160. return fmt.Errorf("error fetching list of branches: %w", err)
  161. }
  162. for _, branch := range branches {
  163. if branch.Default {
  164. g.defaultGitBranch = branch.Name
  165. break
  166. }
  167. }
  168. err = g.deleteGitlabSecret(client)
  169. if err != nil {
  170. return err
  171. }
  172. jobName := getGitlabStageJobName(g.ReleaseName)
  173. ciFile, resp, err := client.RepositoryFiles.GetRawFile(g.pID, ".gitlab-ci.yml", &gitlab.GetRawFileOptions{
  174. Ref: gitlab.String(g.defaultGitBranch),
  175. })
  176. if resp.StatusCode == http.StatusNotFound {
  177. return nil
  178. } else if err != nil {
  179. return fmt.Errorf("error getting .gitlab-ci.yml file: %w", err)
  180. }
  181. ciFileContentsMap := yaml.MapSlice{}
  182. err = yaml.Unmarshal(ciFile, &ciFileContentsMap)
  183. if err != nil {
  184. return fmt.Errorf("error unmarshalling existing .gitlab-ci.yml: %w", err)
  185. }
  186. var stagesInt []interface{}
  187. stagesIdx := -1
  188. for idx, elem := range ciFileContentsMap {
  189. if key, ok := elem.Key.(string); ok {
  190. if key == "stages" {
  191. stages, ok := elem.Value.([]interface{})
  192. if !ok {
  193. return fmt.Errorf("error converting stages to interface slice")
  194. }
  195. stagesInt = stages
  196. stagesIdx = idx
  197. break
  198. }
  199. } else {
  200. return fmt.Errorf("invalid key '%v' in .gitlab-ci.yml", elem.Key)
  201. }
  202. }
  203. if stagesIdx >= 0 { // "stages" exists
  204. var newStages []string
  205. for _, stage := range stagesInt {
  206. stageStr, ok := stage.(string)
  207. if !ok {
  208. return fmt.Errorf("error converting from interface to string")
  209. }
  210. if stageStr != jobName {
  211. newStages = append(newStages, stageStr)
  212. }
  213. }
  214. ciFileContentsMap[stagesIdx] = yaml.MapItem{
  215. Key: "stages",
  216. Value: newStages,
  217. }
  218. }
  219. newCIFileContentsMap := yaml.MapSlice{}
  220. for _, elem := range ciFileContentsMap {
  221. if key, ok := elem.Key.(string); ok {
  222. if key != jobName {
  223. newCIFileContentsMap = append(newCIFileContentsMap, elem)
  224. }
  225. } else {
  226. return fmt.Errorf("invalid key '%v' in .gitlab-ci.yml", elem.Key)
  227. }
  228. }
  229. contentsYAML, err := yaml.Marshal(newCIFileContentsMap)
  230. if err != nil {
  231. return fmt.Errorf("error unmarshalling contents of .gitlab-ci.yml while updating to remove porter job")
  232. }
  233. _, _, err = client.RepositoryFiles.UpdateFile(g.pID, ".gitlab-ci.yml", &gitlab.UpdateFileOptions{
  234. Branch: gitlab.String(g.defaultGitBranch),
  235. AuthorName: gitlab.String("Porter Bot"),
  236. AuthorEmail: gitlab.String("contact@getporter.dev"),
  237. Content: gitlab.String(string(contentsYAML)),
  238. CommitMessage: gitlab.String("Update .gitlab-ci.yml file"),
  239. })
  240. if err != nil {
  241. return fmt.Errorf("error updating .gitlab-ci.yml file to remove porter job: %w", err)
  242. }
  243. return nil
  244. }
  245. func (g *GitlabCI) getClient() (*gitlab.Client, error) {
  246. gi, err := g.Repo.GitlabIntegration().ReadGitlabIntegration(g.ProjectID, g.IntegrationID)
  247. if err != nil {
  248. return nil, err
  249. }
  250. giOAuthInt, err := g.Repo.GitlabAppOAuthIntegration().ReadGitlabAppOAuthIntegration(g.UserID, g.ProjectID, g.IntegrationID)
  251. if err != nil {
  252. return nil, err
  253. }
  254. oauthInt, err := g.Repo.OAuthIntegration().ReadOAuthIntegration(g.ProjectID, giOAuthInt.OAuthIntegrationID)
  255. if err != nil {
  256. return nil, err
  257. }
  258. accessToken, _, err := oauth.GetAccessToken(
  259. oauthInt.SharedOAuthModel,
  260. commonutils.GetGitlabOAuthConf(g.PorterConf, gi),
  261. oauth.MakeUpdateGitlabAppOAuthIntegrationFunction(g.ProjectID, giOAuthInt, g.Repo),
  262. )
  263. if err != nil {
  264. return nil, err
  265. }
  266. client, err := gitlab.NewOAuthClient(accessToken, gitlab.WithBaseURL(gi.InstanceURL))
  267. if err != nil {
  268. return nil, err
  269. }
  270. g.gitlabInstanceURL = gi.InstanceURL
  271. return client, nil
  272. }
  273. func (g *GitlabCI) getCIJob(jobName string) yaml.MapSlice {
  274. res := yaml.MapSlice{}
  275. url, _ := url.Parse(g.gitlabInstanceURL)
  276. res = append(res,
  277. yaml.MapItem{
  278. Key: "rules",
  279. Value: []map[string]string{
  280. {
  281. "if": fmt.Sprintf("$CI_COMMIT_BRANCH == \"%s\" && $CI_PIPELINE_SOURCE == \"push\"", g.GitBranch),
  282. },
  283. },
  284. },
  285. )
  286. if url.Hostname() == "gitlab.com" || url.Hostname() == "www.gitlab.com" {
  287. res = append(res,
  288. yaml.MapItem{
  289. Key: "image",
  290. Value: "docker:latest",
  291. },
  292. yaml.MapItem{
  293. Key: "services",
  294. Value: []string{
  295. "docker:dind",
  296. },
  297. },
  298. yaml.MapItem{
  299. Key: "script",
  300. Value: []string{
  301. fmt.Sprintf(
  302. "docker run --rm --workdir=\"/app\" "+
  303. "-v /var/run/docker.sock:/var/run/docker.sock "+
  304. "-v $(pwd):/app "+
  305. "public.ecr.aws/o1j4x7p4/porter-cli:latest "+
  306. "update --host \"%s\" --project %d --cluster %d "+
  307. "--token \"$%s\" --app \"%s\" "+
  308. "--tag \"$(echo $CI_COMMIT_SHA | cut -c1-7)\" --namespace \"%s\" --stream",
  309. g.ServerURL, g.ProjectID, g.ClusterID, g.getPorterTokenSecretName(),
  310. g.ReleaseName, g.ReleaseNamespace,
  311. ),
  312. },
  313. },
  314. )
  315. } else {
  316. res = append(res,
  317. yaml.MapItem{
  318. Key: "image",
  319. Value: "public.ecr.aws/o1j4x7p4/porter-cli:latest",
  320. },
  321. yaml.MapItem{
  322. Key: "script",
  323. Value: []string{
  324. fmt.Sprintf(
  325. "update --host \"%s\" --project %d --cluster %d "+
  326. "--token \"$%s\" --app \"%s\" "+
  327. "--tag \"$(echo $CI_COMMIT_SHA | cut -c1-7)\" --namespace \"%s\" --stream",
  328. g.ServerURL, g.ProjectID, g.ClusterID, g.getPorterTokenSecretName(),
  329. g.ReleaseName, g.ReleaseNamespace,
  330. ),
  331. },
  332. },
  333. )
  334. }
  335. res = append(res,
  336. yaml.MapItem{
  337. Key: "stage",
  338. Value: jobName,
  339. },
  340. yaml.MapItem{
  341. Key: "tags",
  342. Value: []string{
  343. "porter-runner",
  344. },
  345. },
  346. yaml.MapItem{
  347. Key: "timeout",
  348. Value: "20 minutes",
  349. },
  350. yaml.MapItem{
  351. Key: "variables",
  352. Value: map[string]string{
  353. "GIT_STRATEGY": "clone",
  354. },
  355. },
  356. )
  357. return res
  358. }
  359. func (g *GitlabCI) createGitlabSecret(client *gitlab.Client) error {
  360. _, resp, err := client.ProjectVariables.GetVariable(g.pID, g.getPorterTokenSecretName(),
  361. &gitlab.GetProjectVariableOptions{})
  362. if resp.StatusCode == http.StatusNotFound {
  363. _, _, err = client.ProjectVariables.CreateVariable(g.pID, &gitlab.CreateProjectVariableOptions{
  364. Key: gitlab.String(g.getPorterTokenSecretName()),
  365. Value: gitlab.String(g.PorterToken),
  366. Masked: gitlab.Bool(true),
  367. })
  368. if err != nil {
  369. return fmt.Errorf("error creating porter token variable: %w", err)
  370. }
  371. return nil
  372. } else if err != nil {
  373. return fmt.Errorf("error getting porter token variable: %w", err)
  374. }
  375. _, _, err = client.ProjectVariables.UpdateVariable(g.pID, g.getPorterTokenSecretName(),
  376. &gitlab.UpdateProjectVariableOptions{
  377. Value: gitlab.String(g.PorterToken),
  378. Masked: gitlab.Bool(true),
  379. },
  380. )
  381. if err != nil {
  382. return fmt.Errorf("error updating porter token variable: %w", err)
  383. }
  384. return nil
  385. }
  386. func (g *GitlabCI) deleteGitlabSecret(client *gitlab.Client) error {
  387. _, err := client.ProjectVariables.RemoveVariable(g.pID, g.getPorterTokenSecretName(),
  388. &gitlab.RemoveProjectVariableOptions{})
  389. if err != nil {
  390. return fmt.Errorf("error removing porter token variable: %w", err)
  391. }
  392. return nil
  393. }
  394. func (g *GitlabCI) getPorterTokenSecretName() string {
  395. return fmt.Sprintf("PORTER_TOKEN_%d_%s", g.ProjectID, strings.ToLower(strings.ReplaceAll(g.ReleaseName, "-", "_")))
  396. }
  397. func getGitlabStageJobName(releaseName string) string {
  398. return fmt.Sprintf("porter-%s", strings.ToLower(strings.ReplaceAll(releaseName, "_", "-")))
  399. }