actions.go 9.8 KB

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