preview.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  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. deleteWorkflowYAML, err := getPreviewDeleteActionYAML(opts)
  63. if err != nil {
  64. return err
  65. }
  66. githubBranch, _, err := opts.Client.Repositories.GetBranch(
  67. context.Background(), opts.GitRepoOwner, opts.GitRepoName, defaultBranch, true,
  68. )
  69. if err != nil {
  70. return err
  71. }
  72. if githubBranch.GetProtected() {
  73. err = createNewBranch(opts.Client, opts.GitRepoOwner, opts.GitRepoName, defaultBranch, "porter-preview")
  74. if err != nil {
  75. return fmt.Errorf(
  76. "Unable to create PR to merge workflow files into protected branch: %s.\n"+
  77. "To enable Porter Preview Environment deployments, please create Github workflow "+
  78. "files in this branch with the following contents:\n"+
  79. "--------\n%s--------\n--------\n%s--------\nERROR: %w",
  80. defaultBranch, string(applyWorkflowYAML), string(deleteWorkflowYAML), ErrCreatePRForProtectedBranch,
  81. )
  82. }
  83. _, err = commitWorkflowFile(
  84. opts.Client,
  85. fmt.Sprintf("porter_%s_env.yml", strings.ToLower(opts.EnvironmentName)),
  86. applyWorkflowYAML, opts.GitRepoOwner,
  87. opts.GitRepoName, "porter-preview", false,
  88. )
  89. if err != nil {
  90. return fmt.Errorf(
  91. "Unable to create PR to merge workflow files into protected branch: %s.\n"+
  92. "To enable Porter Preview Environment deployments, please create Github workflow "+
  93. "files in this branch with the following contents:\n"+
  94. "--------\n%s--------\n--------\n%s--------\nERROR: %w",
  95. defaultBranch, string(applyWorkflowYAML), string(deleteWorkflowYAML), ErrCreatePRForProtectedBranch,
  96. )
  97. }
  98. _, err = commitWorkflowFile(
  99. opts.Client,
  100. fmt.Sprintf("porter_%s_delete_env.yml", strings.ToLower(opts.EnvironmentName)),
  101. deleteWorkflowYAML, opts.GitRepoOwner,
  102. opts.GitRepoName, "porter-preview", false,
  103. )
  104. if err != nil {
  105. return fmt.Errorf(
  106. "Unable to create PR to merge workflow files into protected branch: %s.\n"+
  107. "To enable Porter Preview Environment deployments, please create a Github workflow "+
  108. "file in this branch with the following contents:\n"+
  109. "--------\n%s--------\nERROR: %w",
  110. defaultBranch, string(deleteWorkflowYAML), ErrCreatePRForProtectedBranch,
  111. )
  112. }
  113. pr, _, err := opts.Client.PullRequests.Create(
  114. context.Background(), opts.GitRepoOwner, opts.GitRepoName, &github.NewPullRequest{
  115. Title: github.String("Enable Porter Preview Environment deployments"),
  116. Base: github.String(defaultBranch),
  117. Head: github.String("porter-preview"),
  118. },
  119. )
  120. if err != nil {
  121. return err
  122. }
  123. return fmt.Errorf("Please merge %s to enable Porter Preview Environment deployments.\nERROR: %w",
  124. pr.GetHTMLURL(), ErrProtectedBranch)
  125. }
  126. _, err = commitWorkflowFile(
  127. opts.Client,
  128. fmt.Sprintf("porter_%s_env.yml", strings.ToLower(opts.EnvironmentName)),
  129. applyWorkflowYAML,
  130. opts.GitRepoOwner,
  131. opts.GitRepoName,
  132. defaultBranch,
  133. false,
  134. )
  135. if err != nil {
  136. if strings.Contains(err.Error(), "409 Could not create file") {
  137. // possibly a write-protected branch
  138. err = createNewBranch(opts.Client, opts.GitRepoOwner, opts.GitRepoName, defaultBranch, "porter-preview")
  139. if err != nil {
  140. return fmt.Errorf("write-protected branch %s. Error creating porter-preview branch: %w", defaultBranch, err)
  141. }
  142. _, err = commitWorkflowFile(
  143. opts.Client,
  144. fmt.Sprintf("porter_%s_env.yml", strings.ToLower(opts.EnvironmentName)),
  145. applyWorkflowYAML,
  146. opts.GitRepoOwner,
  147. opts.GitRepoName,
  148. "porter-preview",
  149. false,
  150. )
  151. if err != nil {
  152. return fmt.Errorf("write-protected branch %s. Error committing to porter-preview branch: %w", defaultBranch, err)
  153. }
  154. _, err = commitWorkflowFile(
  155. opts.Client,
  156. fmt.Sprintf("porter_%s_delete_env.yml", strings.ToLower(opts.EnvironmentName)),
  157. deleteWorkflowYAML,
  158. opts.GitRepoOwner,
  159. opts.GitRepoName,
  160. "porter-preview",
  161. false,
  162. )
  163. if err != nil {
  164. return fmt.Errorf("write-protected branch %s. Error committing to porter-preview branch: %w", defaultBranch, err)
  165. }
  166. pr, _, err := opts.Client.PullRequests.Create(
  167. context.Background(), opts.GitRepoOwner, opts.GitRepoName, &github.NewPullRequest{
  168. Title: github.String("Merge Porter preview environment Github Actions workflow files"),
  169. Base: github.String(defaultBranch),
  170. Head: github.String("porter-preview"),
  171. },
  172. )
  173. if err != nil {
  174. return err
  175. }
  176. return fmt.Errorf("write-protected branch %s. Please merge %s to enable preview environment for your repository", defaultBranch, pr.GetURL())
  177. }
  178. return err
  179. }
  180. _, err = commitWorkflowFile(
  181. opts.Client,
  182. fmt.Sprintf("porter_%s_delete_env.yml", strings.ToLower(opts.EnvironmentName)),
  183. deleteWorkflowYAML,
  184. opts.GitRepoOwner,
  185. opts.GitRepoName,
  186. defaultBranch,
  187. false,
  188. )
  189. if err != nil {
  190. return err
  191. }
  192. return err
  193. }
  194. func DeleteEnv(opts *EnvOpts) error {
  195. // get the repository to find the default branch
  196. repo, _, err := opts.Client.Repositories.Get(
  197. context.TODO(),
  198. opts.GitRepoOwner,
  199. opts.GitRepoName,
  200. )
  201. if err != nil {
  202. return err
  203. }
  204. defaultBranch := repo.GetDefaultBranch()
  205. // delete GitHub Environment: check that environment exists before deletion
  206. _, resp, err := opts.Client.Repositories.GetEnvironment(
  207. context.Background(),
  208. opts.GitRepoOwner,
  209. opts.GitRepoName,
  210. opts.EnvironmentName,
  211. )
  212. if err == nil && resp != nil && resp.StatusCode == http.StatusOK {
  213. _, err = opts.Client.Repositories.DeleteEnvironment(
  214. context.Background(),
  215. opts.GitRepoOwner,
  216. opts.GitRepoName,
  217. opts.EnvironmentName,
  218. )
  219. if err != nil {
  220. return err
  221. }
  222. }
  223. githubBranch, _, err := opts.Client.Repositories.GetBranch(
  224. context.Background(), opts.GitRepoOwner, opts.GitRepoName, defaultBranch, true,
  225. )
  226. if err != nil {
  227. return err
  228. }
  229. if githubBranch.GetProtected() {
  230. return ErrProtectedBranch
  231. }
  232. err = deleteGithubFile(
  233. opts.Client,
  234. fmt.Sprintf("porter_%s_env.yml", strings.ToLower(opts.EnvironmentName)),
  235. opts.GitRepoOwner,
  236. opts.GitRepoName,
  237. defaultBranch,
  238. false,
  239. )
  240. if err != nil {
  241. return err
  242. }
  243. return deleteGithubFile(
  244. opts.Client,
  245. fmt.Sprintf("porter_%s_delete_env.yml", strings.ToLower(opts.EnvironmentName)),
  246. opts.GitRepoOwner,
  247. opts.GitRepoName,
  248. defaultBranch,
  249. false,
  250. )
  251. }
  252. func getPreviewApplyActionYAML(opts *EnvOpts) ([]byte, error) {
  253. gaSteps := []GithubActionYAMLStep{
  254. getCheckoutCodeStep(),
  255. getCreatePreviewEnvStep(
  256. opts.ServerURL,
  257. getPorterTokenSecretName(opts.ProjectID),
  258. opts.ProjectID,
  259. opts.ClusterID,
  260. opts.GitInstallationID,
  261. opts.GitRepoOwner,
  262. opts.GitRepoName,
  263. "v0.2.0",
  264. ),
  265. }
  266. actionYAML := GithubActionYAML{
  267. On: map[string]interface{}{
  268. "workflow_dispatch": map[string]interface{}{
  269. "inputs": map[string]interface{}{
  270. "pr_number": map[string]interface{}{
  271. "description": "Pull request number",
  272. "type": "number",
  273. "required": true,
  274. },
  275. "pr_title": map[string]interface{}{
  276. "description": "Pull request title",
  277. "type": "string",
  278. "required": true,
  279. },
  280. "pr_branch_from": map[string]interface{}{
  281. "description": "Pull request head branch",
  282. "type": "string",
  283. "required": true,
  284. },
  285. "pr_branch_into": map[string]interface{}{
  286. "description": "Pull request base branch",
  287. "type": "string",
  288. "required": true,
  289. },
  290. },
  291. },
  292. },
  293. Name: "Porter Preview Environment",
  294. Jobs: map[string]GithubActionYAMLJob{
  295. "porter-preview": {
  296. RunsOn: "ubuntu-latest",
  297. Steps: gaSteps,
  298. },
  299. },
  300. }
  301. return yaml.Marshal(actionYAML)
  302. }
  303. func getPreviewDeleteActionYAML(opts *EnvOpts) ([]byte, error) {
  304. gaSteps := []GithubActionYAMLStep{
  305. getDeletePreviewEnvStep(
  306. opts.ServerURL,
  307. getPorterTokenSecretName(opts.ProjectID),
  308. opts.ProjectID,
  309. opts.ClusterID,
  310. opts.GitRepoName,
  311. "v0.2.0",
  312. ),
  313. }
  314. actionYAML := GithubActionYAML{
  315. On: map[string]interface{}{
  316. "workflow_dispatch": map[string]interface{}{
  317. "inputs": map[string]interface{}{
  318. "deployment_id": map[string]interface{}{
  319. "description": "Deployment ID",
  320. "type": "number",
  321. "required": true,
  322. },
  323. },
  324. },
  325. },
  326. Name: "Porter Preview Environment",
  327. Jobs: map[string]GithubActionYAMLJob{
  328. "porter-delete-preview": {
  329. RunsOn: "ubuntu-latest",
  330. Steps: gaSteps,
  331. },
  332. },
  333. }
  334. return yaml.Marshal(actionYAML)
  335. }
  336. func createNewBranch(
  337. client *github.Client,
  338. gitRepoOwner, gitRepoName, baseBranch, headBranch string,
  339. ) error {
  340. _, resp, err := client.Repositories.GetBranch(
  341. context.Background(), gitRepoOwner, gitRepoName, headBranch, true,
  342. )
  343. headBranchRef := fmt.Sprintf("refs/heads/%s", headBranch)
  344. if err == nil {
  345. // delete the stale branch
  346. _, err := client.Git.DeleteRef(
  347. context.Background(), gitRepoOwner, gitRepoName, headBranchRef,
  348. )
  349. if err != nil {
  350. return err
  351. }
  352. } else if resp.StatusCode != http.StatusNotFound {
  353. return err
  354. }
  355. base, _, err := client.Repositories.GetBranch(
  356. context.Background(), gitRepoOwner, gitRepoName, baseBranch, true,
  357. )
  358. if err != nil {
  359. return err
  360. }
  361. _, _, err = client.Git.CreateRef(
  362. context.Background(), gitRepoOwner, gitRepoName, &github.Reference{
  363. Ref: github.String(headBranchRef),
  364. Object: &github.GitObject{
  365. SHA: base.Commit.SHA,
  366. },
  367. },
  368. )
  369. if err != nil {
  370. return err
  371. }
  372. return nil
  373. }