ci.go 11 KB

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