actions.go 9.8 KB

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