preview.go 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. package actions
  2. import (
  3. "context"
  4. "fmt"
  5. "net/http"
  6. "strings"
  7. "github.com/google/go-github/v41/github"
  8. "gopkg.in/yaml.v2"
  9. )
  10. type EnvOpts struct {
  11. Client *github.Client
  12. ServerURL string
  13. PorterToken string
  14. GitRepoOwner, GitRepoName string
  15. EnvironmentName string
  16. ProjectID, ClusterID, GitInstallationID uint
  17. }
  18. func SetupEnv(opts *EnvOpts) error {
  19. // make a best-effort to create a Github environment. this is a non-fatal operation,
  20. // as the environments API is not enabled for private repositories that don't have
  21. // github enterprise.
  22. _, resp, err := opts.Client.Repositories.GetEnvironment(
  23. context.Background(),
  24. opts.GitRepoOwner,
  25. opts.GitRepoName,
  26. opts.EnvironmentName,
  27. )
  28. if resp != nil && resp.StatusCode == http.StatusNotFound {
  29. opts.Client.Repositories.CreateUpdateEnvironment(
  30. context.Background(),
  31. opts.GitRepoOwner,
  32. opts.GitRepoName,
  33. opts.EnvironmentName,
  34. nil,
  35. )
  36. }
  37. // create porter token secret
  38. err = createGithubSecret(
  39. opts.Client,
  40. getPorterTokenSecretName(opts.ProjectID),
  41. opts.PorterToken,
  42. opts.GitRepoOwner,
  43. opts.GitRepoName,
  44. )
  45. if err != nil {
  46. return err
  47. }
  48. // get the repository to find the default branch
  49. repo, _, err := opts.Client.Repositories.Get(
  50. context.TODO(),
  51. opts.GitRepoOwner,
  52. opts.GitRepoName,
  53. )
  54. if err != nil {
  55. return err
  56. }
  57. defaultBranch := repo.GetDefaultBranch()
  58. applyWorkflowYAML, err := getPreviewApplyActionYAML(opts)
  59. if err != nil {
  60. return err
  61. }
  62. githubBranch, _, err := opts.Client.Repositories.GetBranch(
  63. context.Background(), opts.GitRepoOwner, opts.GitRepoName, defaultBranch, true,
  64. )
  65. if err != nil {
  66. return err
  67. }
  68. if githubBranch.GetProtected() {
  69. err = createNewBranch(opts.Client, opts.GitRepoOwner, opts.GitRepoName, defaultBranch, "porter-preview")
  70. if err != nil {
  71. return fmt.Errorf(
  72. "Unable to create PR to merge workflow files into protected branch: %s.\n"+
  73. "To enable Porter Preview Environment deployments, please create Github workflow "+
  74. "files in this branch with the following contents:\n"+
  75. "--------\n%s--------\nERROR: %w",
  76. defaultBranch, string(applyWorkflowYAML), ErrCreatePRForProtectedBranch,
  77. )
  78. }
  79. _, err = commitWorkflowFile(
  80. opts.Client,
  81. fmt.Sprintf("porter_%s_env.yml", strings.ToLower(opts.EnvironmentName)),
  82. applyWorkflowYAML, opts.GitRepoOwner,
  83. opts.GitRepoName, "porter-preview", false,
  84. )
  85. if err != nil {
  86. return fmt.Errorf(
  87. "Unable to create PR to merge workflow files into protected branch: %s.\n"+
  88. "To enable Porter Preview Environment deployments, please create Github workflow "+
  89. "files in this branch with the following contents:\n"+
  90. "--------\n%s--------\nERROR: %w",
  91. defaultBranch, string(applyWorkflowYAML), ErrCreatePRForProtectedBranch,
  92. )
  93. }
  94. pr, _, err := opts.Client.PullRequests.Create(
  95. context.Background(), opts.GitRepoOwner, opts.GitRepoName, &github.NewPullRequest{
  96. Title: github.String("Enable Porter Preview Environment deployments"),
  97. Base: github.String(defaultBranch),
  98. Head: github.String("porter-preview"),
  99. },
  100. )
  101. if err != nil {
  102. return err
  103. }
  104. return fmt.Errorf("Please merge %s to enable Porter Preview Environment deployments.\nERROR: %w",
  105. pr.GetHTMLURL(), ErrProtectedBranch)
  106. }
  107. _, err = commitWorkflowFile(
  108. opts.Client,
  109. fmt.Sprintf("porter_%s_env.yml", strings.ToLower(opts.EnvironmentName)),
  110. applyWorkflowYAML,
  111. opts.GitRepoOwner,
  112. opts.GitRepoName,
  113. defaultBranch,
  114. false,
  115. )
  116. return err
  117. }
  118. func DeleteEnv(opts *EnvOpts) error {
  119. // get the repository to find the default branch
  120. repo, _, err := opts.Client.Repositories.Get(
  121. context.TODO(),
  122. opts.GitRepoOwner,
  123. opts.GitRepoName,
  124. )
  125. if err != nil {
  126. return err
  127. }
  128. defaultBranch := repo.GetDefaultBranch()
  129. // delete GitHub Environment: check that environment exists before deletion
  130. _, resp, err := opts.Client.Repositories.GetEnvironment(
  131. context.Background(),
  132. opts.GitRepoOwner,
  133. opts.GitRepoName,
  134. opts.EnvironmentName,
  135. )
  136. if err == nil && resp != nil && resp.StatusCode == http.StatusOK {
  137. _, err = opts.Client.Repositories.DeleteEnvironment(
  138. context.Background(),
  139. opts.GitRepoOwner,
  140. opts.GitRepoName,
  141. opts.EnvironmentName,
  142. )
  143. if err != nil {
  144. return err
  145. }
  146. }
  147. githubBranch, _, err := opts.Client.Repositories.GetBranch(
  148. context.Background(), opts.GitRepoOwner, opts.GitRepoName, defaultBranch, true,
  149. )
  150. if err != nil {
  151. return err
  152. }
  153. if githubBranch.GetProtected() {
  154. return ErrProtectedBranch
  155. }
  156. err = deleteGithubFile(
  157. opts.Client,
  158. fmt.Sprintf("porter_%s_env.yml", strings.ToLower(opts.EnvironmentName)),
  159. opts.GitRepoOwner,
  160. opts.GitRepoName,
  161. defaultBranch,
  162. false,
  163. )
  164. if err != nil {
  165. return err
  166. }
  167. return deleteGithubFile(
  168. opts.Client,
  169. fmt.Sprintf("porter_%s_delete_env.yml", strings.ToLower(opts.EnvironmentName)),
  170. opts.GitRepoOwner,
  171. opts.GitRepoName,
  172. defaultBranch,
  173. false,
  174. )
  175. }
  176. func getPreviewApplyActionYAML(opts *EnvOpts) ([]byte, error) {
  177. gaSteps := []GithubActionYAMLStep{
  178. getCheckoutCodeStep(),
  179. getCreatePreviewEnvStep(
  180. opts.ServerURL,
  181. getPorterTokenSecretName(opts.ProjectID),
  182. opts.ProjectID,
  183. opts.ClusterID,
  184. opts.GitInstallationID,
  185. opts.GitRepoOwner,
  186. opts.GitRepoName,
  187. "v0.2.0",
  188. ),
  189. }
  190. actionYAML := GithubActionYAML{
  191. On: map[string]interface{}{
  192. "workflow_dispatch": map[string]interface{}{
  193. "inputs": map[string]interface{}{
  194. "pr_number": map[string]interface{}{
  195. "description": "Pull request number",
  196. "type": "number",
  197. "required": true,
  198. },
  199. "pr_title": map[string]interface{}{
  200. "description": "Pull request title",
  201. "type": "string",
  202. "required": true,
  203. },
  204. "pr_branch_from": map[string]interface{}{
  205. "description": "Pull request head branch",
  206. "type": "string",
  207. "required": true,
  208. },
  209. "pr_branch_into": map[string]interface{}{
  210. "description": "Pull request base branch",
  211. "type": "string",
  212. "required": true,
  213. },
  214. },
  215. },
  216. },
  217. Name: "Porter Preview Environment",
  218. Jobs: map[string]GithubActionYAMLJob{
  219. "porter-preview": {
  220. RunsOn: "ubuntu-latest",
  221. Concurrency: map[string]string{
  222. "group": "${{ github.workflow }}-${{ github.event.inputs.pr_number }}",
  223. },
  224. Steps: gaSteps,
  225. },
  226. },
  227. }
  228. return yaml.Marshal(actionYAML)
  229. }
  230. func createNewBranch(
  231. client *github.Client,
  232. gitRepoOwner, gitRepoName, baseBranch, headBranch string,
  233. ) error {
  234. _, resp, err := client.Repositories.GetBranch(
  235. context.Background(), gitRepoOwner, gitRepoName, headBranch, true,
  236. )
  237. headBranchRef := fmt.Sprintf("refs/heads/%s", headBranch)
  238. if err == nil {
  239. // delete the stale branch
  240. _, err := client.Git.DeleteRef(
  241. context.Background(), gitRepoOwner, gitRepoName, headBranchRef,
  242. )
  243. if err != nil {
  244. return err
  245. }
  246. } else if resp.StatusCode != http.StatusNotFound {
  247. return err
  248. }
  249. base, _, err := client.Repositories.GetBranch(
  250. context.Background(), gitRepoOwner, gitRepoName, baseBranch, true,
  251. )
  252. if err != nil {
  253. return err
  254. }
  255. _, _, err = client.Git.CreateRef(
  256. context.Background(), gitRepoOwner, gitRepoName, &github.Reference{
  257. Ref: github.String(headBranchRef),
  258. Object: &github.GitObject{
  259. SHA: base.Commit.SHA,
  260. },
  261. },
  262. )
  263. if err != nil {
  264. return err
  265. }
  266. return nil
  267. }