actions.go 9.1 KB

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