ci.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  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. yaml.MapItem{
  315. Key: "tags",
  316. Value: []string{
  317. "docker",
  318. },
  319. },
  320. )
  321. } else {
  322. res = append(res,
  323. yaml.MapItem{
  324. Key: "image",
  325. Value: map[string]interface{}{
  326. "name": "public.ecr.aws/o1j4x7p4/porter-cli:latest",
  327. "entrypoint": []string{
  328. "",
  329. },
  330. },
  331. },
  332. yaml.MapItem{
  333. Key: "script",
  334. Value: []string{
  335. fmt.Sprintf(
  336. "porter update --host \"%s\" --project %d --cluster %d "+
  337. "--token \"$%s\" --app \"%s\" "+
  338. "--tag \"$(echo $CI_COMMIT_SHA | cut -c1-7)\" --namespace \"%s\" --stream",
  339. g.ServerURL, g.ProjectID, g.ClusterID, g.getPorterTokenSecretName(),
  340. g.ReleaseName, g.ReleaseNamespace,
  341. ),
  342. },
  343. },
  344. yaml.MapItem{
  345. Key: "tags",
  346. Value: []string{
  347. "porter-runner",
  348. },
  349. },
  350. )
  351. }
  352. res = append(res,
  353. yaml.MapItem{
  354. Key: "stage",
  355. Value: jobName,
  356. },
  357. yaml.MapItem{
  358. Key: "timeout",
  359. Value: "20 minutes",
  360. },
  361. yaml.MapItem{
  362. Key: "variables",
  363. Value: map[string]string{
  364. "GIT_STRATEGY": "clone",
  365. },
  366. },
  367. )
  368. return res
  369. }
  370. func (g *GitlabCI) createGitlabSecret(client *gitlab.Client) error {
  371. _, resp, err := client.ProjectVariables.GetVariable(g.pID, g.getPorterTokenSecretName(),
  372. &gitlab.GetProjectVariableOptions{})
  373. if resp.StatusCode == http.StatusNotFound {
  374. _, _, err = client.ProjectVariables.CreateVariable(g.pID, &gitlab.CreateProjectVariableOptions{
  375. Key: gitlab.String(g.getPorterTokenSecretName()),
  376. Value: gitlab.String(g.PorterToken),
  377. Masked: gitlab.Bool(true),
  378. })
  379. if err != nil {
  380. return fmt.Errorf("error creating porter token variable: %w", err)
  381. }
  382. return nil
  383. } else if err != nil {
  384. return fmt.Errorf("error getting porter token variable: %w", err)
  385. }
  386. _, _, err = client.ProjectVariables.UpdateVariable(g.pID, g.getPorterTokenSecretName(),
  387. &gitlab.UpdateProjectVariableOptions{
  388. Value: gitlab.String(g.PorterToken),
  389. Masked: gitlab.Bool(true),
  390. },
  391. )
  392. if err != nil {
  393. return fmt.Errorf("error updating porter token variable: %w", err)
  394. }
  395. return nil
  396. }
  397. func (g *GitlabCI) deleteGitlabSecret(client *gitlab.Client) error {
  398. _, err := client.ProjectVariables.RemoveVariable(g.pID, g.getPorterTokenSecretName(),
  399. &gitlab.RemoveProjectVariableOptions{})
  400. if err != nil {
  401. return fmt.Errorf("error removing porter token variable: %w", err)
  402. }
  403. return nil
  404. }
  405. func (g *GitlabCI) getPorterTokenSecretName() string {
  406. return fmt.Sprintf("PORTER_TOKEN_%d_%s", g.ProjectID, strings.ToLower(strings.ReplaceAll(g.ReleaseName, "-", "_")))
  407. }
  408. func getGitlabStageJobName(releaseName string) string {
  409. return fmt.Sprintf("porter-%s", strings.ToLower(strings.ReplaceAll(releaseName, "_", "-")))
  410. }