actions.go 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. package actions
  2. import (
  3. "context"
  4. "encoding/base64"
  5. "fmt"
  6. "github.com/google/go-github/v33/github"
  7. "github.com/porter-dev/porter/internal/models"
  8. "github.com/porter-dev/porter/internal/repository"
  9. "golang.org/x/crypto/nacl/box"
  10. "golang.org/x/oauth2"
  11. "strings"
  12. "gopkg.in/yaml.v2"
  13. )
  14. type GithubActions struct {
  15. GitIntegration *models.GitRepo
  16. GitRepoName string
  17. GitRepoOwner string
  18. Repo repository.Repository
  19. GithubConf *oauth2.Config
  20. WebhookToken string
  21. PorterToken string
  22. BuildEnv map[string]string
  23. ProjectID uint
  24. ReleaseName string
  25. GitBranch string
  26. DockerFilePath string
  27. FolderPath string
  28. ImageRepoURL string
  29. defaultBranch string
  30. }
  31. func (g *GithubActions) Setup() (string, error) {
  32. client, err := g.getClient()
  33. if err != nil {
  34. return "", err
  35. }
  36. // get the repository to find the default branch
  37. repo, _, err := client.Repositories.Get(
  38. context.TODO(),
  39. g.GitRepoOwner,
  40. g.GitRepoName,
  41. )
  42. if err != nil {
  43. return "", err
  44. }
  45. g.defaultBranch = repo.GetDefaultBranch()
  46. // create a new secret with a webhook token
  47. err = g.createGithubSecret(client, g.getWebhookSecretName(), g.WebhookToken)
  48. if err != nil {
  49. return "", err
  50. }
  51. // create a new secret with a porter token
  52. err = g.createGithubSecret(client, g.getPorterTokenSecretName(), g.PorterToken)
  53. if err != nil {
  54. return "", err
  55. }
  56. // create a new secret with the build variables
  57. err = g.createEnvSecret(client)
  58. if err != nil {
  59. return "", err
  60. }
  61. fileBytes, err := g.GetGithubActionYAML()
  62. if err != nil {
  63. return "", err
  64. }
  65. return g.commitGithubFile(client, g.getPorterYMLFileName(), fileBytes)
  66. }
  67. func (g *GithubActions) Cleanup() error {
  68. client, err := g.getClient()
  69. if err != nil {
  70. return err
  71. }
  72. // get the repository to find the default branch
  73. repo, _, err := client.Repositories.Get(
  74. context.TODO(),
  75. g.GitRepoOwner,
  76. g.GitRepoName,
  77. )
  78. if err != nil {
  79. return err
  80. }
  81. g.defaultBranch = repo.GetDefaultBranch()
  82. // delete the webhook token secret
  83. err = g.deleteGithubSecret(client, g.getWebhookSecretName())
  84. if err != nil {
  85. return err
  86. }
  87. // delete the env secret
  88. err = g.deleteGithubSecret(client, g.getBuildEnvSecretName())
  89. if err != nil {
  90. return err
  91. }
  92. return g.deleteGithubFile(client, g.getPorterYMLFileName())
  93. }
  94. type GithubActionYAMLStep struct {
  95. Name string `yaml:"name,omitempty"`
  96. ID string `yaml:"id,omitempty"`
  97. Uses string `yaml:"uses,omitempty"`
  98. Run string `yaml:"run,omitempty"`
  99. }
  100. type GithubActionYAMLOnPushBranches struct {
  101. Branches []string `yaml:"branches,omitempty"`
  102. }
  103. type GithubActionYAMLOnPush struct {
  104. Push GithubActionYAMLOnPushBranches `yaml:"push,omitempty"`
  105. }
  106. type GithubActionYAMLJob struct {
  107. RunsOn string `yaml:"runs-on,omitempty"`
  108. Steps []GithubActionYAMLStep `yaml:"steps,omitempty"`
  109. }
  110. type GithubActionYAML struct {
  111. On GithubActionYAMLOnPush `yaml:"on,omitempty"`
  112. Name string `yaml:"name,omitempty"`
  113. Jobs map[string]GithubActionYAMLJob `yaml:"jobs,omitempty"`
  114. }
  115. func (g *GithubActions) GetGithubActionYAML() ([]byte, error) {
  116. gaSteps := []GithubActionYAMLStep{
  117. getCheckoutCodeStep(),
  118. getDownloadPorterStep(),
  119. getConfigurePorterStep(g.getPorterTokenSecretName()),
  120. }
  121. if g.DockerFilePath == "" {
  122. gaSteps = append(gaSteps, getBuildPackPushStep(g.getBuildEnvSecretName(), g.FolderPath, g.ImageRepoURL))
  123. } else {
  124. gaSteps = append(gaSteps, getDockerBuildPushStep(g.getBuildEnvSecretName(), g.DockerFilePath, g.ImageRepoURL))
  125. }
  126. gaSteps = append(gaSteps, deployPorterWebhookStep(g.getWebhookSecretName(), g.ImageRepoURL))
  127. branch := g.GitBranch
  128. if branch == "" {
  129. branch = g.defaultBranch
  130. }
  131. actionYAML := &GithubActionYAML{
  132. On: GithubActionYAMLOnPush{
  133. Push: GithubActionYAMLOnPushBranches{
  134. Branches: []string{
  135. branch,
  136. },
  137. },
  138. },
  139. Name: "Deploy to Porter",
  140. Jobs: map[string]GithubActionYAMLJob{
  141. "porter-deploy": {
  142. RunsOn: "ubuntu-latest",
  143. Steps: gaSteps,
  144. },
  145. },
  146. }
  147. return yaml.Marshal(actionYAML)
  148. }
  149. func (g *GithubActions) getClient() (*github.Client, error) {
  150. // get the oauth integration
  151. oauthInt, err := g.Repo.OAuthIntegration.ReadOAuthIntegration(g.GitIntegration.OAuthIntegrationID)
  152. if err != nil {
  153. return nil, err
  154. }
  155. tok := &oauth2.Token{
  156. AccessToken: string(oauthInt.AccessToken),
  157. RefreshToken: string(oauthInt.RefreshToken),
  158. TokenType: "Bearer",
  159. }
  160. client := github.NewClient(g.GithubConf.Client(oauth2.NoContext, tok))
  161. return client, nil
  162. }
  163. func (g *GithubActions) createGithubSecret(
  164. client *github.Client,
  165. secretName,
  166. secretValue string,
  167. ) error {
  168. // get the public key for the repo
  169. key, _, err := client.Actions.GetRepoPublicKey(context.TODO(), g.GitRepoOwner, g.GitRepoName)
  170. if err != nil {
  171. return err
  172. }
  173. // encrypt the secret with the public key
  174. keyBytes := [32]byte{}
  175. keyDecoded, err := base64.StdEncoding.DecodeString(*key.Key)
  176. if err != nil {
  177. return err
  178. }
  179. copy(keyBytes[:], keyDecoded[:])
  180. secretEncoded, err := box.SealAnonymous(nil, []byte(secretValue), &keyBytes, nil)
  181. if err != nil {
  182. return err
  183. }
  184. encrypted := base64.StdEncoding.EncodeToString(secretEncoded)
  185. encryptedSecret := &github.EncryptedSecret{
  186. Name: secretName,
  187. KeyID: *key.KeyID,
  188. EncryptedValue: encrypted,
  189. }
  190. // write the secret to the repo
  191. _, err = client.Actions.CreateOrUpdateRepoSecret(context.TODO(), g.GitRepoOwner, g.GitRepoName, encryptedSecret)
  192. return err
  193. }
  194. func (g *GithubActions) deleteGithubSecret(
  195. client *github.Client,
  196. secretName string,
  197. ) error {
  198. // delete the secret from the repo
  199. _, err := client.Actions.DeleteRepoSecret(
  200. context.TODO(),
  201. g.GitRepoOwner,
  202. g.GitRepoName,
  203. secretName,
  204. )
  205. return err
  206. }
  207. func (g *GithubActions) CreateEnvSecret() error {
  208. client, err := g.getClient()
  209. if err != nil {
  210. return err
  211. }
  212. return g.createEnvSecret(client)
  213. }
  214. func (g *GithubActions) createEnvSecret(client *github.Client) error {
  215. // convert the env object to a string
  216. lines := make([]string, 0)
  217. for key, val := range g.BuildEnv {
  218. lines = append(lines, fmt.Sprintf(`%s=%s`, key, val))
  219. }
  220. secretName := g.getBuildEnvSecretName()
  221. return g.createGithubSecret(client, secretName, strings.Join(lines, "\n"))
  222. }
  223. func (g *GithubActions) getWebhookSecretName() string {
  224. return fmt.Sprintf("WEBHOOK_%s", strings.Replace(
  225. strings.ToUpper(g.ReleaseName), "-", "_", -1),
  226. )
  227. }
  228. func (g *GithubActions) getBuildEnvSecretName() string {
  229. return fmt.Sprintf("ENV_%s", strings.Replace(
  230. strings.ToUpper(g.ReleaseName), "-", "_", -1),
  231. )
  232. }
  233. func (g *GithubActions) getPorterYMLFileName() string {
  234. return fmt.Sprintf("porter_%s.yml", strings.Replace(
  235. strings.ToLower(g.ReleaseName), "-", "_", -1),
  236. )
  237. }
  238. func (g *GithubActions) getPorterTokenSecretName() string {
  239. return fmt.Sprintf("PORTER_TOKEN_%d", g.ProjectID)
  240. }
  241. func (g *GithubActions) commitGithubFile(
  242. client *github.Client,
  243. filename string,
  244. contents []byte,
  245. ) (string, error) {
  246. filepath := ".github/workflows/" + filename
  247. sha := ""
  248. // get contents of a file if it exists
  249. fileData, _, _, _ := client.Repositories.GetContents(
  250. context.TODO(),
  251. g.GitRepoOwner,
  252. g.GitRepoName,
  253. filepath,
  254. &github.RepositoryContentGetOptions{},
  255. )
  256. if fileData != nil {
  257. sha = *fileData.SHA
  258. }
  259. opts := &github.RepositoryContentFileOptions{
  260. Message: github.String(fmt.Sprintf("Create %s file", filename)),
  261. Content: contents,
  262. Branch: github.String(g.defaultBranch),
  263. SHA: &sha,
  264. Committer: &github.CommitAuthor{
  265. Name: github.String("Porter Bot"),
  266. Email: github.String("contact@getporter.dev"),
  267. },
  268. }
  269. resp, _, err := client.Repositories.UpdateFile(
  270. context.TODO(),
  271. g.GitRepoOwner,
  272. g.GitRepoName,
  273. filepath,
  274. opts,
  275. )
  276. if err != nil {
  277. return "", err
  278. }
  279. return *resp.Commit.SHA, nil
  280. }
  281. func (g *GithubActions) deleteGithubFile(
  282. client *github.Client,
  283. filename string,
  284. ) error {
  285. filepath := ".github/workflows/" + filename
  286. sha := ""
  287. // get contents of a file if it exists
  288. fileData, _, _, _ := client.Repositories.GetContents(
  289. context.TODO(),
  290. g.GitRepoOwner,
  291. g.GitRepoName,
  292. filepath,
  293. &github.RepositoryContentGetOptions{},
  294. )
  295. if fileData != nil {
  296. sha = *fileData.SHA
  297. }
  298. opts := &github.RepositoryContentFileOptions{
  299. Message: github.String(fmt.Sprintf("Delete %s file", filename)),
  300. Branch: github.String(g.defaultBranch),
  301. SHA: &sha,
  302. Committer: &github.CommitAuthor{
  303. Name: github.String("Porter Bot"),
  304. Email: github.String("contact@getporter.dev"),
  305. },
  306. }
  307. _, _, err := client.Repositories.DeleteFile(
  308. context.TODO(),
  309. g.GitRepoOwner,
  310. g.GitRepoName,
  311. filepath,
  312. opts,
  313. )
  314. if err != nil {
  315. return err
  316. }
  317. return nil
  318. }