ci.go 12 KB

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