build.go 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. package v2
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "os"
  7. "path/filepath"
  8. "strings"
  9. "github.com/porter-dev/porter/api/types"
  10. "github.com/porter-dev/porter/cli/cmd/pack"
  11. "github.com/porter-dev/porter/cli/cmd/docker"
  12. api "github.com/porter-dev/porter/api/client"
  13. )
  14. const (
  15. buildMethodPack = "pack"
  16. buildMethodDocker = "docker"
  17. buildLogFilename = "PORTER_BUILD_LOGS"
  18. )
  19. // buildInput is the input struct for the build method
  20. type buildInput struct {
  21. ProjectID uint
  22. // AppName is the name of the application being built and is used to name the repository
  23. AppName string
  24. BuildContext string
  25. Dockerfile string
  26. BuildMethod string
  27. // Builder is the image containing the components necessary to build the application in a pack build
  28. Builder string
  29. BuildPacks []string
  30. // ImageTag is the tag to apply to the new image
  31. ImageTag string
  32. // CurrentImageTag is used in docker build to cache from
  33. CurrentImageTag string
  34. RepositoryURL string
  35. // PullImageBeforeBuild is used to pull the docker image before building
  36. PullImageBeforeBuild bool
  37. Env map[string]string
  38. }
  39. type buildOutput struct {
  40. Error error
  41. Logs string
  42. }
  43. // build will create an image repository if it does not exist, and then build and push the image
  44. func build(ctx context.Context, client api.Client, inp buildInput) buildOutput {
  45. output := buildOutput{}
  46. if inp.ProjectID == 0 {
  47. output.Error = errors.New("must specify a project id")
  48. return output
  49. }
  50. projectID := inp.ProjectID
  51. if inp.ImageTag == "" {
  52. output.Error = errors.New("must specify an image tag")
  53. return output
  54. }
  55. tag := inp.ImageTag
  56. if inp.RepositoryURL == "" {
  57. output.Error = errors.New("must specify a registry url")
  58. return output
  59. }
  60. repositoryURL := strings.TrimPrefix(inp.RepositoryURL, "https://")
  61. err := createImageRepositoryIfNotExists(ctx, client, projectID, repositoryURL)
  62. if err != nil {
  63. output.Error = fmt.Errorf("error creating image repository: %w", err)
  64. return output
  65. }
  66. dockerAgent, err := docker.NewAgentWithAuthGetter(ctx, client, projectID)
  67. if err != nil {
  68. output.Error = fmt.Errorf("error getting docker agent: %w", err)
  69. return output
  70. }
  71. // create a temp file which build logs will be written to
  72. // temp file gets cleaned up when os exits (i.e. when the GHA completes), so no need to remove it manually
  73. logFile, _ := os.CreateTemp("", buildLogFilename)
  74. switch inp.BuildMethod {
  75. case buildMethodDocker:
  76. basePath, err := filepath.Abs(".")
  77. if err != nil {
  78. output.Error = fmt.Errorf("error getting absolute path: %w", err)
  79. return output
  80. }
  81. buildCtx, dockerfilePath, isDockerfileInCtx, err := resolveDockerPaths(
  82. basePath,
  83. inp.BuildContext,
  84. inp.Dockerfile,
  85. )
  86. if err != nil {
  87. output.Error = fmt.Errorf("error resolving docker paths: %w", err)
  88. return output
  89. }
  90. opts := &docker.BuildOpts{
  91. ImageRepo: repositoryURL,
  92. Tag: tag,
  93. CurrentTag: inp.CurrentImageTag,
  94. BuildContext: buildCtx,
  95. DockerfilePath: dockerfilePath,
  96. IsDockerfileInCtx: isDockerfileInCtx,
  97. Env: inp.Env,
  98. LogFile: logFile,
  99. UseCache: inp.PullImageBeforeBuild,
  100. }
  101. err = dockerAgent.BuildLocal(
  102. ctx,
  103. opts,
  104. )
  105. if err != nil {
  106. output.Error = fmt.Errorf("error building image with docker: %w", err)
  107. logString := "Error reading contents of build log file"
  108. if logFile != nil {
  109. content, err := os.ReadFile(logFile.Name())
  110. // only continue if we can read the file. if we cannot, logString will be the default
  111. if err == nil {
  112. logString = string(content)
  113. }
  114. }
  115. output.Logs = logString
  116. return output
  117. }
  118. case buildMethodPack:
  119. packAgent := &pack.Agent{}
  120. opts := &docker.BuildOpts{
  121. ImageRepo: repositoryURL,
  122. Tag: tag,
  123. BuildContext: inp.BuildContext,
  124. Env: inp.Env,
  125. LogFile: logFile,
  126. }
  127. buildConfig := &types.BuildConfig{
  128. Builder: inp.Builder,
  129. Buildpacks: inp.BuildPacks,
  130. }
  131. err := packAgent.Build(ctx, opts, buildConfig, "")
  132. if err != nil {
  133. output.Error = fmt.Errorf("error building image with pack: %w", err)
  134. logString := "Error reading contents of build log file"
  135. if logFile != nil {
  136. content, err := os.ReadFile(logFile.Name())
  137. // only continue if we can read the file. if we cannot, logString will be the default
  138. if err == nil {
  139. logString = string(content)
  140. }
  141. }
  142. output.Logs = logString
  143. return output
  144. }
  145. default:
  146. output.Error = fmt.Errorf("invalid build method: %s", inp.BuildMethod)
  147. return output
  148. }
  149. err = dockerAgent.PushImage(ctx, fmt.Sprintf("%s:%s", repositoryURL, tag))
  150. if err != nil {
  151. output.Error = fmt.Errorf("error pushing image: %w", err)
  152. return output
  153. }
  154. return output
  155. }
  156. func createImageRepositoryIfNotExists(ctx context.Context, client api.Client, projectID uint, imageURL string) error {
  157. if projectID == 0 {
  158. return errors.New("must specify a project id")
  159. }
  160. if imageURL == "" {
  161. return errors.New("must specify an image url")
  162. }
  163. regList, err := client.ListRegistries(ctx, projectID)
  164. if err != nil {
  165. return fmt.Errorf("error calling list registries: %w", err)
  166. }
  167. if regList == nil {
  168. return errors.New("registry list is nil")
  169. }
  170. if len(*regList) == 0 {
  171. return errors.New("no registries found for project")
  172. }
  173. var registryID uint
  174. for _, registry := range *regList {
  175. if strings.Contains(strings.TrimPrefix(imageURL, "https://"), strings.TrimPrefix(registry.URL, "https://")) {
  176. registryID = registry.ID
  177. break
  178. }
  179. }
  180. if registryID == 0 {
  181. return errors.New("no registries match url")
  182. }
  183. err = client.CreateRepository(
  184. ctx,
  185. projectID,
  186. registryID,
  187. &types.CreateRegistryRepositoryRequest{
  188. ImageRepoURI: imageURL,
  189. },
  190. )
  191. if err != nil {
  192. return fmt.Errorf("error creating repository: %w", err)
  193. }
  194. return nil
  195. }
  196. // resolveDockerPaths returns a path to the dockerfile that is either relative or absolute, and a path
  197. // to the build context that is absolute.
  198. //
  199. // The return value will be relative if the dockerfile exists within the build context, absolute
  200. // otherwise. The second return value is true if the dockerfile exists within the build context,
  201. // false otherwise.
  202. func resolveDockerPaths(basePath string, buildContextPath string, dockerfilePath string) (
  203. absoluteBuildContextPath string,
  204. outputDockerfilePath string,
  205. isDockerfileRelative bool,
  206. err error,
  207. ) {
  208. absoluteBuildContextPath, err = filepath.Abs(buildContextPath)
  209. if err != nil {
  210. return "", "", false, fmt.Errorf("error getting absolute path: %w", err)
  211. }
  212. outputDockerfilePath = dockerfilePath
  213. if !filepath.IsAbs(dockerfilePath) {
  214. outputDockerfilePath = filepath.Join(basePath, dockerfilePath)
  215. }
  216. pathComp, err := filepath.Rel(absoluteBuildContextPath, outputDockerfilePath)
  217. if err != nil {
  218. return "", "", false, fmt.Errorf("error getting relative path: %w", err)
  219. }
  220. if !strings.HasPrefix(pathComp, ".."+string(os.PathSeparator)) {
  221. isDockerfileRelative = true
  222. return absoluteBuildContextPath, pathComp, isDockerfileRelative, nil
  223. }
  224. isDockerfileRelative = false
  225. outputDockerfilePath, err = filepath.Abs(outputDockerfilePath)
  226. if err != nil {
  227. return "", "", false, err
  228. }
  229. return absoluteBuildContextPath, outputDockerfilePath, isDockerfileRelative, nil
  230. }