preview.go 7.7 KB

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