pack.go 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. package pack
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "net/url"
  7. "os"
  8. "path/filepath"
  9. "regexp"
  10. "strings"
  11. packclient "github.com/buildpacks/pack/pkg/client"
  12. githubApi "github.com/google/go-github/v41/github"
  13. "github.com/porter-dev/porter/api/types"
  14. "github.com/porter-dev/porter/cli/cmd/docker"
  15. "github.com/porter-dev/porter/cli/cmd/github"
  16. "k8s.io/client-go/util/homedir"
  17. )
  18. var sharedPackClient *packclient.Client
  19. func init() {
  20. var err error
  21. // initialize a pack client
  22. logger := newPackLogger()
  23. sharedPackClient, err = packclient.NewClient(packclient.WithLogger(logger))
  24. if err != nil {
  25. panic(err)
  26. }
  27. }
  28. // Agent is a buildpack agent
  29. type Agent struct{}
  30. // Build manages buildpack builds
  31. func (a *Agent) Build(ctx context.Context, opts *docker.BuildOpts, buildConfig *types.BuildConfig, cacheImage string) error {
  32. absPath, err := filepath.Abs(opts.BuildContext)
  33. if err != nil {
  34. return err
  35. }
  36. mode := os.FileMode(0o600)
  37. procfilePath := filepath.Clean(filepath.Join(absPath, "Procfile"))
  38. file, err := os.OpenFile(procfilePath, os.O_RDONLY|os.O_CREATE, mode)
  39. if err != nil {
  40. return err
  41. }
  42. if err := file.Close(); err != nil {
  43. return err
  44. }
  45. buildOpts := packclient.BuildOptions{
  46. RelativeBaseDir: filepath.Dir(absPath),
  47. Image: fmt.Sprintf("%s:%s", opts.ImageRepo, opts.Tag),
  48. Builder: "paketobuildpacks/builder:full",
  49. AppPath: opts.BuildContext,
  50. Env: opts.Env,
  51. GroupID: 0,
  52. }
  53. if opts.UseCache {
  54. buildOpts.CacheImage = cacheImage
  55. buildOpts.Publish = true
  56. }
  57. if buildConfig != nil {
  58. buildOpts.Builder = buildConfig.Builder
  59. for i := range buildConfig.Buildpacks {
  60. bp := buildConfig.Buildpacks[i]
  61. if bp == "" {
  62. continue
  63. }
  64. bpRealName, err := getBuildpackName(ctx, bp)
  65. if err != nil {
  66. return err
  67. }
  68. buildOpts.Buildpacks = append(buildOpts.Buildpacks, bpRealName)
  69. }
  70. // FIXME: use all the config vars
  71. }
  72. if len(buildOpts.Buildpacks) > 0 && strings.HasPrefix(buildOpts.Builder, "heroku") {
  73. buildOpts.Buildpacks = append(buildOpts.Buildpacks, "heroku/procfile@2.0.1")
  74. }
  75. return sharedPackClient.Build(ctx, buildOpts)
  76. }
  77. func getBuildpackName(ctx context.Context, bp string) (string, error) {
  78. if bp == "" {
  79. return "", errors.New("please specify a buildpack name")
  80. }
  81. u, err := url.Parse(bp)
  82. if err != nil {
  83. return bp, nil
  84. }
  85. // if there is no scheme, it's likely something like `heroku/nodejs`
  86. // if the scheme is `urn`, it's like something like `urn:cnb:registry:heroku/nodejs`
  87. if u.Scheme == "" || u.Scheme == "urn" {
  88. return bp, nil
  89. }
  90. // pass cnb-shimmed buildpacks as is
  91. if u.Host == "cnb-shim.herokuapp.com" {
  92. return bp, nil
  93. }
  94. var bpRealName string
  95. // could be a git repository containing the buildpack
  96. if !strings.HasSuffix(u.Path, ".zip") && u.Host != "github.com" && u.Host != "www.github.com" {
  97. return bpRealName, errors.New("please provide either a github.com URL or a ZIP file URL")
  98. }
  99. urlPaths := strings.Split(u.Path[1:], "/")
  100. dstDir := filepath.Join(homedir.HomeDir(), ".porter")
  101. bpCustomName := regexp.MustCompile("/|-").ReplaceAllString(u.Path[1:], "_")
  102. var zipFileName string
  103. if strings.HasSuffix(bpCustomName, ".zip") {
  104. zipFileName = bpCustomName
  105. } else {
  106. zipFileName = fmt.Sprintf("%s.zip", bpCustomName)
  107. }
  108. downloader := &github.ZIPDownloader{
  109. ZipFolderDest: dstDir,
  110. AssetFolderDest: dstDir,
  111. ZipName: zipFileName,
  112. RemoveAfterDownload: true,
  113. }
  114. if zipFileName != bpCustomName {
  115. // try to download the repo ZIP from github
  116. githubClient := githubApi.NewClient(nil)
  117. rel, _, err := githubClient.Repositories.GetLatestRelease(
  118. ctx,
  119. urlPaths[0],
  120. urlPaths[1],
  121. )
  122. if err == nil {
  123. bp = rel.GetZipballURL()
  124. } else {
  125. // default to the current default branch
  126. repo, _, err := githubClient.Repositories.Get(
  127. ctx,
  128. urlPaths[0],
  129. urlPaths[1],
  130. )
  131. if err != nil {
  132. return bpRealName, errors.New("could not fetch git repo details")
  133. }
  134. bp = fmt.Sprintf("%s/archive/refs/heads/%s.zip", bp, repo.GetDefaultBranch())
  135. }
  136. }
  137. if err := downloader.DownloadToFile(bp); err != nil {
  138. return bpRealName, fmt.Errorf("failed to download buildpack: %w", err)
  139. }
  140. if err := downloader.UnzipToDir(); err != nil {
  141. return bpRealName, fmt.Errorf("failed to extract buildpack: %w", err)
  142. }
  143. dstFiles, err := os.ReadDir(dstDir)
  144. if err != nil {
  145. return bpRealName, fmt.Errorf("failed to list files in extracted buildpack: %w", err)
  146. }
  147. for _, info := range dstFiles {
  148. if info.Type().IsDir() && strings.Contains(info.Name(), urlPaths[1]) {
  149. bpRealName = filepath.Join(dstDir, info.Name())
  150. }
  151. }
  152. return bpRealName, nil
  153. }