actions.go 10 KB

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