actions.go 8.6 KB

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