ci.go 8.2 KB

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