ci.go 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  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. ciFileContentsMap := make(map[string]interface{})
  78. err = yaml.Unmarshal(ciFile, ciFileContentsMap)
  79. if err != nil {
  80. return fmt.Errorf("error unmarshalling existing .gitlab-ci.yml: %w", err)
  81. }
  82. stages, ok := ciFileContentsMap["stages"].([]string)
  83. if !ok {
  84. return fmt.Errorf("error converting stages to string slice")
  85. }
  86. stageExists := false
  87. for _, stage := range stages {
  88. if stage == jobName {
  89. stageExists = true
  90. break
  91. }
  92. }
  93. if !stageExists {
  94. stages = append(stages, jobName)
  95. ciFileContentsMap["stages"] = stages
  96. }
  97. ciFileContentsMap[jobName] = g.getCIJob(jobName)
  98. contentsYAML, _ := yaml.Marshal(ciFileContentsMap)
  99. _, _, err = client.RepositoryFiles.UpdateFile(g.pID, ".gitlab-ci.yml", &gitlab.UpdateFileOptions{
  100. Branch: gitlab.String(g.defaultGitBranch),
  101. AuthorName: gitlab.String("Porter Bot"),
  102. AuthorEmail: gitlab.String("contact@getporter.dev"),
  103. Content: gitlab.String(string(contentsYAML)),
  104. CommitMessage: gitlab.String("Update .gitlab-ci.yml file"),
  105. })
  106. if err != nil {
  107. return fmt.Errorf("error updating .gitlab-ci.yml file to add porter job: %w", err)
  108. }
  109. }
  110. return nil
  111. }
  112. func (g *GitlabCI) Cleanup() error {
  113. client, err := g.getClient()
  114. if err != nil {
  115. return err
  116. }
  117. g.pID = fmt.Sprintf("%s/%s", g.GitRepoOwner, g.GitRepoName)
  118. branches, _, err := client.Branches.ListBranches(g.pID, &gitlab.ListBranchesOptions{})
  119. if err != nil {
  120. return fmt.Errorf("error fetching list of branches: %w", err)
  121. }
  122. for _, branch := range branches {
  123. if branch.Default {
  124. g.defaultGitBranch = branch.Name
  125. break
  126. }
  127. }
  128. err = g.deleteGitlabSecret(client)
  129. if err != nil {
  130. return err
  131. }
  132. jobName := getGitlabStageJobName(g.ReleaseName)
  133. ciFile, resp, err := client.RepositoryFiles.GetRawFile(g.pID, ".gitlab-ci.yml", &gitlab.GetRawFileOptions{
  134. Ref: gitlab.String(g.defaultGitBranch),
  135. })
  136. if resp.StatusCode == http.StatusNotFound {
  137. return nil
  138. } else if err != nil {
  139. return fmt.Errorf("error getting .gitlab-ci.yml file: %w", err)
  140. }
  141. ciFileContentsMap := make(map[string]interface{})
  142. err = yaml.Unmarshal(ciFile, ciFileContentsMap)
  143. if err != nil {
  144. return fmt.Errorf("error unmarshalling existing .gitlab-ci.yml: %w", err)
  145. }
  146. stages, ok := ciFileContentsMap["stages"].([]interface{})
  147. if !ok {
  148. return fmt.Errorf("error converting stages to interface slice")
  149. }
  150. var newStages []string
  151. for _, stage := range stages {
  152. stageStr, ok := stage.(string)
  153. if !ok {
  154. return fmt.Errorf("error converting from interface to string")
  155. }
  156. if stageStr != jobName {
  157. newStages = append(newStages, stageStr)
  158. }
  159. }
  160. ciFileContentsMap["stage"] = newStages
  161. delete(ciFileContentsMap, jobName)
  162. contentsYAML, _ := yaml.Marshal(ciFileContentsMap)
  163. _, _, err = client.RepositoryFiles.UpdateFile(g.pID, ".gitlab-ci.yml", &gitlab.UpdateFileOptions{
  164. Branch: gitlab.String(g.defaultGitBranch),
  165. AuthorName: gitlab.String("Porter Bot"),
  166. AuthorEmail: gitlab.String("contact@getporter.dev"),
  167. Content: gitlab.String(string(contentsYAML)),
  168. CommitMessage: gitlab.String("Update .gitlab-ci.yml file"),
  169. })
  170. if err != nil {
  171. return fmt.Errorf("error updating .gitlab-ci.yml file to remove porter job: %w", err)
  172. }
  173. return nil
  174. }
  175. func (g *GitlabCI) getClient() (*gitlab.Client, error) {
  176. gi, err := g.Repo.GitlabIntegration().ReadGitlabIntegration(g.ProjectID, g.IntegrationID)
  177. if err != nil {
  178. return nil, err
  179. }
  180. oauthInt, err := g.Repo.GitlabAppOAuthIntegration().ReadGitlabAppOAuthIntegration(g.UserID, g.ProjectID, g.IntegrationID)
  181. if err != nil {
  182. return nil, err
  183. }
  184. accessToken, _, err := oauth.GetAccessToken(oauthInt.SharedOAuthModel, commonutils.GetGitlabOAuthConf(g.PorterConf, gi),
  185. oauth.MakeUpdateGitlabAppOAuthIntegrationFunction(oauthInt, g.Repo))
  186. if err != nil {
  187. return nil, err
  188. }
  189. client, err := gitlab.NewOAuthClient(accessToken, gitlab.WithBaseURL(gi.InstanceURL))
  190. if err != nil {
  191. return nil, err
  192. }
  193. return client, nil
  194. }
  195. func (g *GitlabCI) getCIJob(jobName string) map[string]interface{} {
  196. return map[string]interface{}{
  197. "image": "public.ecr.aws/o1j4x7p4/porter-cli:latest",
  198. "stage": jobName,
  199. "timeout": "20 minutes",
  200. "variables": map[string]string{
  201. "GIT_STRATEGY": "clone",
  202. },
  203. "rules": []map[string]string{
  204. {
  205. "if": fmt.Sprintf("$CI_COMMIT_BRANCH == \"%s\" && $CI_PIPELINE_SOURCE == \"push\"", g.GitBranch),
  206. },
  207. },
  208. "script": []string{
  209. fmt.Sprintf("export PORTER_HOST=\"%s\"", g.ServerURL),
  210. fmt.Sprintf("export PORTER_PROJECT=\"%d\"", g.ProjectID),
  211. fmt.Sprintf("export PORTER_CLUSTER=\"%d\"", g.ClusterID),
  212. fmt.Sprintf("export PORTER_TOKEN=\"$%s\"", g.getPorterTokenSecretName()),
  213. "export PORTER_TAG=\"$(echo $CI_COMMIT_SHA | cut -c1-7)\"",
  214. fmt.Sprintf("porter update --app \"%s\" --tag \"$PORTER_TAG\" --namespace \"%s\" --path \"%s\" --stream",
  215. g.ReleaseName, g.ReleaseNamespace, g.FolderPath),
  216. },
  217. }
  218. }
  219. func (g *GitlabCI) createGitlabSecret(client *gitlab.Client) error {
  220. _, _, err := client.ProjectVariables.CreateVariable(g.pID, &gitlab.CreateProjectVariableOptions{
  221. Key: gitlab.String(g.getPorterTokenSecretName()),
  222. Value: gitlab.String(g.PorterToken),
  223. Masked: gitlab.Bool(true),
  224. })
  225. if err != nil {
  226. return fmt.Errorf("error creating porter token variable: %w", err)
  227. }
  228. return nil
  229. }
  230. func (g *GitlabCI) deleteGitlabSecret(client *gitlab.Client) error {
  231. _, err := client.ProjectVariables.RemoveVariable(g.pID, g.getPorterTokenSecretName(), &gitlab.RemoveProjectVariableOptions{})
  232. if err != nil {
  233. return fmt.Errorf("error removing porter token variable: %w", err)
  234. }
  235. return nil
  236. }
  237. func (g *GitlabCI) getPorterTokenSecretName() string {
  238. return fmt.Sprintf("PORTER_TOKEN_%d", g.ProjectID)
  239. }
  240. func getGitlabStageJobName(releaseName string) string {
  241. return fmt.Sprintf("porter-%s", strings.ToLower(strings.ReplaceAll(releaseName, "_", "-")))
  242. }