actions.go 10 KB

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