preview.go 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  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. _, err = commitGithubFile(
  67. opts.Client,
  68. fmt.Sprintf("porter_%s_env.yml", strings.ToLower(opts.EnvironmentName)),
  69. applyWorkflowYAML,
  70. opts.GitRepoOwner,
  71. opts.GitRepoName,
  72. defaultBranch,
  73. false,
  74. )
  75. if err != nil {
  76. if strings.Contains(err.Error(), "409 Could not create file") {
  77. // possibly a write-protected branch
  78. err = createPorterPreviewBranch(opts, defaultBranch)
  79. if err != nil {
  80. return fmt.Errorf("write-protected branch %s. Error creating porter-preview branch: %w", defaultBranch, err)
  81. }
  82. _, err = commitGithubFile(
  83. opts.Client,
  84. fmt.Sprintf("porter_%s_env.yml", strings.ToLower(opts.EnvironmentName)),
  85. applyWorkflowYAML,
  86. opts.GitRepoOwner,
  87. opts.GitRepoName,
  88. "porter-preview",
  89. false,
  90. )
  91. if err != nil {
  92. return fmt.Errorf("write-protected branch %s. Error committing to porter-preview branch: %w", defaultBranch, err)
  93. }
  94. _, err = commitGithubFile(
  95. opts.Client,
  96. fmt.Sprintf("porter_%s_delete_env.yml", strings.ToLower(opts.EnvironmentName)),
  97. deleteWorkflowYAML,
  98. opts.GitRepoOwner,
  99. opts.GitRepoName,
  100. "porter-preview",
  101. false,
  102. )
  103. if err != nil {
  104. return fmt.Errorf("write-protected branch %s. Error committing to porter-preview branch: %w", defaultBranch, err)
  105. }
  106. pr, _, err := opts.Client.PullRequests.Create(
  107. context.Background(), opts.GitRepoOwner, opts.GitRepoName, &github.NewPullRequest{
  108. Title: github.String("Merge Porter preview environment Github Actions workflow files"),
  109. Base: github.String(defaultBranch),
  110. Head: github.String("porter-preview"),
  111. },
  112. )
  113. if err != nil {
  114. return err
  115. }
  116. return fmt.Errorf("write-protected branch %s. Please merge %s to enable preview environment for your repository", defaultBranch, pr.GetURL())
  117. }
  118. return err
  119. }
  120. _, err = commitGithubFile(
  121. opts.Client,
  122. fmt.Sprintf("porter_%s_delete_env.yml", strings.ToLower(opts.EnvironmentName)),
  123. deleteWorkflowYAML,
  124. opts.GitRepoOwner,
  125. opts.GitRepoName,
  126. defaultBranch,
  127. false,
  128. )
  129. if err != nil {
  130. return err
  131. }
  132. return err
  133. }
  134. func DeleteEnv(opts *EnvOpts) error {
  135. // get the repository to find the default branch
  136. repo, _, err := opts.Client.Repositories.Get(
  137. context.TODO(),
  138. opts.GitRepoOwner,
  139. opts.GitRepoName,
  140. )
  141. if err != nil {
  142. return err
  143. }
  144. defaultBranch := repo.GetDefaultBranch()
  145. // delete GitHub Environment: check that environment exists before deletion
  146. _, resp, err := opts.Client.Repositories.GetEnvironment(
  147. context.Background(),
  148. opts.GitRepoOwner,
  149. opts.GitRepoName,
  150. opts.EnvironmentName,
  151. )
  152. if err == nil && resp != nil && resp.StatusCode == http.StatusOK {
  153. _, err = opts.Client.Repositories.DeleteEnvironment(
  154. context.Background(),
  155. opts.GitRepoOwner,
  156. opts.GitRepoName,
  157. opts.EnvironmentName,
  158. )
  159. if err != nil {
  160. return err
  161. }
  162. }
  163. err = deleteGithubFile(
  164. opts.Client,
  165. fmt.Sprintf("porter_%s_env.yml", strings.ToLower(opts.EnvironmentName)),
  166. opts.GitRepoOwner,
  167. opts.GitRepoName,
  168. defaultBranch,
  169. false,
  170. )
  171. if err != nil {
  172. return err
  173. }
  174. return deleteGithubFile(
  175. opts.Client,
  176. fmt.Sprintf("porter_%s_delete_env.yml", strings.ToLower(opts.EnvironmentName)),
  177. opts.GitRepoOwner,
  178. opts.GitRepoName,
  179. defaultBranch,
  180. false,
  181. )
  182. }
  183. func getPreviewApplyActionYAML(opts *EnvOpts) ([]byte, error) {
  184. gaSteps := []GithubActionYAMLStep{
  185. getCheckoutCodeStep(),
  186. getCreatePreviewEnvStep(
  187. opts.ServerURL,
  188. getPorterTokenSecretName(opts.ProjectID),
  189. opts.ProjectID,
  190. opts.ClusterID,
  191. opts.GitInstallationID,
  192. opts.GitRepoOwner,
  193. opts.GitRepoName,
  194. "v0.2.0",
  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": "number",
  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. Steps: gaSteps,
  229. },
  230. },
  231. }
  232. return yaml.Marshal(actionYAML)
  233. }
  234. func getPreviewDeleteActionYAML(opts *EnvOpts) ([]byte, error) {
  235. gaSteps := []GithubActionYAMLStep{
  236. getDeletePreviewEnvStep(
  237. opts.ServerURL,
  238. getPorterTokenSecretName(opts.ProjectID),
  239. opts.ProjectID,
  240. opts.ClusterID,
  241. opts.GitRepoName,
  242. "v0.2.0",
  243. ),
  244. }
  245. actionYAML := GithubActionYAML{
  246. On: map[string]interface{}{
  247. "workflow_dispatch": map[string]interface{}{
  248. "inputs": map[string]interface{}{
  249. "environment_id": map[string]interface{}{
  250. "description": "Environment ID",
  251. "type": "number",
  252. "required": true,
  253. },
  254. "repo_owner": map[string]interface{}{
  255. "description": "Repository owner",
  256. "type": "string",
  257. "required": true,
  258. },
  259. "repo_name": map[string]interface{}{
  260. "description": "Repository name",
  261. "type": "string",
  262. "required": true,
  263. },
  264. "pr_number": map[string]interface{}{
  265. "description": "Pull request number",
  266. "type": "number",
  267. "required": true,
  268. },
  269. },
  270. },
  271. },
  272. Name: "Porter Preview Environment",
  273. Jobs: map[string]GithubActionYAMLJob{
  274. "porter-delete-preview": {
  275. RunsOn: "ubuntu-latest",
  276. Steps: gaSteps,
  277. },
  278. },
  279. }
  280. return yaml.Marshal(actionYAML)
  281. }
  282. func createPorterPreviewBranch(opts *EnvOpts, defaultBranch string) error {
  283. _, resp, err := opts.Client.Repositories.GetBranch(
  284. context.Background(), opts.GitRepoOwner, opts.GitRepoName, "porter-preview", false,
  285. )
  286. if resp.StatusCode == http.StatusNotFound {
  287. branch, _, err := opts.Client.Repositories.GetBranch(
  288. context.Background(), opts.GitRepoOwner, opts.GitRepoName, defaultBranch, false,
  289. )
  290. if err != nil {
  291. return err
  292. }
  293. _, _, err = opts.Client.Git.CreateRef(
  294. context.Background(), opts.GitRepoOwner, opts.GitRepoName, &github.Reference{
  295. Ref: github.String("refs/heads/porter-preview"),
  296. Object: &github.GitObject{
  297. SHA: branch.Commit.SHA,
  298. },
  299. },
  300. )
  301. if err != nil {
  302. return err
  303. }
  304. return nil
  305. }
  306. if err != nil {
  307. return err
  308. }
  309. return nil
  310. }