build.go 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  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. // SkipPush is used to skip pushing the image to the registry
  39. SkipPush bool
  40. }
  41. type buildOutput struct {
  42. Error error
  43. Logs string
  44. }
  45. // build will create an image repository if it does not exist, and then build and push the image
  46. func build(ctx context.Context, client api.Client, inp buildInput) buildOutput {
  47. output := buildOutput{}
  48. if inp.ProjectID == 0 {
  49. output.Error = errors.New("must specify a project id")
  50. return output
  51. }
  52. projectID := inp.ProjectID
  53. if inp.ImageTag == "" {
  54. output.Error = errors.New("must specify an image tag")
  55. return output
  56. }
  57. tag := inp.ImageTag
  58. if inp.RepositoryURL == "" {
  59. output.Error = errors.New("must specify a registry url")
  60. return output
  61. }
  62. repositoryURL := strings.TrimPrefix(inp.RepositoryURL, "https://")
  63. // this should catch the following v1 GCP repo format:
  64. // us-central1-docker.pkg.dev/GCP_PROJECT/porter-PORTER_PROJECT/APP_NAME-porter-stack-APP_NAME/APP_NAME-porter-stack-APP_NAME
  65. // and convert it to:
  66. // us-central1-docker.pkg.dev/GCP_PROJECT/porter-PORTER_PROJECT/APP_NAME
  67. if splits := strings.Split(repositoryURL, "porter-stack"); len(splits) == 3 {
  68. repositoryURL = strings.TrimSuffix(splits[0], "-")
  69. }
  70. err := createImageRepositoryIfNotExists(ctx, client, projectID, repositoryURL)
  71. if err != nil {
  72. output.Error = fmt.Errorf("error creating image repository: %w", err)
  73. return output
  74. }
  75. dockerAgent, err := docker.NewAgentWithAuthGetter(ctx, client, projectID)
  76. if err != nil {
  77. output.Error = fmt.Errorf("error getting docker agent: %w", err)
  78. return output
  79. }
  80. // create a temp file which build logs will be written to
  81. // temp file gets cleaned up when os exits (i.e. when the GHA completes), so no need to remove it manually
  82. logFile, _ := os.CreateTemp("", buildLogFilename)
  83. switch inp.BuildMethod {
  84. case buildMethodDocker:
  85. basePath, err := filepath.Abs(".")
  86. if err != nil {
  87. output.Error = fmt.Errorf("error getting absolute path: %w", err)
  88. return output
  89. }
  90. buildCtx, dockerfilePath, isDockerfileInCtx, err := resolveDockerPaths(
  91. basePath,
  92. inp.BuildContext,
  93. inp.Dockerfile,
  94. )
  95. if err != nil {
  96. output.Error = fmt.Errorf("error resolving docker paths: %w", err)
  97. return output
  98. }
  99. opts := &docker.BuildOpts{
  100. ImageRepo: repositoryURL,
  101. Tag: tag,
  102. CurrentTag: inp.CurrentImageTag,
  103. BuildContext: buildCtx,
  104. DockerfilePath: dockerfilePath,
  105. IsDockerfileInCtx: isDockerfileInCtx,
  106. Env: inp.Env,
  107. LogFile: logFile,
  108. UseCache: inp.PullImageBeforeBuild,
  109. }
  110. err = dockerAgent.BuildLocal(
  111. ctx,
  112. opts,
  113. )
  114. if err != nil {
  115. output.Error = fmt.Errorf("error building image with docker: %w", err)
  116. logString := "Error reading contents of build log file"
  117. if logFile != nil {
  118. content, err := os.ReadFile(logFile.Name())
  119. // only continue if we can read the file. if we cannot, logString will be the default
  120. if err == nil {
  121. logString = string(content)
  122. }
  123. }
  124. output.Logs = logString
  125. return output
  126. }
  127. case buildMethodPack:
  128. packAgent := &pack.Agent{}
  129. opts := &docker.BuildOpts{
  130. ImageRepo: repositoryURL,
  131. Tag: tag,
  132. BuildContext: inp.BuildContext,
  133. Env: inp.Env,
  134. LogFile: logFile,
  135. }
  136. buildConfig := &types.BuildConfig{
  137. Builder: inp.Builder,
  138. Buildpacks: inp.BuildPacks,
  139. }
  140. if buildConfig.Builder == "heroku/buildpacks:20" {
  141. if opts.Env == nil {
  142. opts.Env = map[string]string{}
  143. }
  144. opts.Env["ALLOW_EOL_SHIMMED_BUILDER"] = "1"
  145. }
  146. err := packAgent.Build(ctx, opts, buildConfig, "")
  147. if err != nil {
  148. output.Error = fmt.Errorf("error building image with pack: %w", err)
  149. logString := "Error reading contents of build log file"
  150. if logFile != nil {
  151. content, err := os.ReadFile(logFile.Name())
  152. // only continue if we can read the file. if we cannot, logString will be the default
  153. if err == nil {
  154. logString = string(content)
  155. }
  156. }
  157. output.Logs = logString
  158. return output
  159. }
  160. default:
  161. output.Error = fmt.Errorf("invalid build method: %s", inp.BuildMethod)
  162. return output
  163. }
  164. if !inp.SkipPush {
  165. err = dockerAgent.PushImage(ctx, fmt.Sprintf("%s:%s", repositoryURL, tag))
  166. if err != nil {
  167. output.Error = fmt.Errorf("error pushing image: %w", err)
  168. return output
  169. }
  170. }
  171. return output
  172. }
  173. type pushInput struct {
  174. ProjectID uint
  175. ImageTag string
  176. RepositoryURL string
  177. }
  178. func push(ctx context.Context, client api.Client, inp pushInput) error {
  179. if inp.ProjectID == 0 {
  180. return errors.New("must specify a project id")
  181. }
  182. projectID := inp.ProjectID
  183. if inp.ImageTag == "" {
  184. return errors.New("must specify an image tag")
  185. }
  186. tag := inp.ImageTag
  187. if inp.RepositoryURL == "" {
  188. return errors.New("must specify a registry url")
  189. }
  190. repositoryURL := strings.TrimPrefix(inp.RepositoryURL, "https://")
  191. dockerAgent, err := docker.NewAgentWithAuthGetter(ctx, client, projectID)
  192. if err != nil {
  193. return fmt.Errorf("error getting docker agent: %w", err)
  194. }
  195. err = dockerAgent.PushImage(ctx, fmt.Sprintf("%s:%s", repositoryURL, tag))
  196. if err != nil {
  197. return fmt.Errorf("error pushing image: %w", err)
  198. }
  199. return nil
  200. }
  201. func createImageRepositoryIfNotExists(ctx context.Context, client api.Client, projectID uint, imageURL string) error {
  202. if projectID == 0 {
  203. return errors.New("must specify a project id")
  204. }
  205. if imageURL == "" {
  206. return errors.New("must specify an image url")
  207. }
  208. regList, err := client.ListRegistries(ctx, projectID)
  209. if err != nil {
  210. return fmt.Errorf("error calling list registries: %w", err)
  211. }
  212. if regList == nil {
  213. return errors.New("registry list is nil")
  214. }
  215. if len(*regList) == 0 {
  216. return errors.New("no registries found for project")
  217. }
  218. var registryID uint
  219. for _, registry := range *regList {
  220. if strings.Contains(strings.TrimPrefix(imageURL, "https://"), strings.TrimPrefix(registry.URL, "https://")) {
  221. registryID = registry.ID
  222. break
  223. }
  224. }
  225. if registryID == 0 {
  226. return errors.New("no registries match url")
  227. }
  228. err = client.CreateRepository(
  229. ctx,
  230. projectID,
  231. registryID,
  232. &types.CreateRegistryRepositoryRequest{
  233. ImageRepoURI: imageURL,
  234. },
  235. )
  236. if err != nil {
  237. return fmt.Errorf("error creating repository: %w", err)
  238. }
  239. return nil
  240. }
  241. // resolveDockerPaths returns a path to the dockerfile that is either relative or absolute, and a path
  242. // to the build context that is absolute.
  243. //
  244. // The return value will be relative if the dockerfile exists within the build context, absolute
  245. // otherwise. The second return value is true if the dockerfile exists within the build context,
  246. // false otherwise.
  247. func resolveDockerPaths(basePath string, buildContextPath string, dockerfilePath string) (
  248. absoluteBuildContextPath string,
  249. outputDockerfilePath string,
  250. isDockerfileRelative bool,
  251. err error,
  252. ) {
  253. absoluteBuildContextPath, err = filepath.Abs(buildContextPath)
  254. if err != nil {
  255. return "", "", false, fmt.Errorf("error getting absolute path: %w", err)
  256. }
  257. outputDockerfilePath = dockerfilePath
  258. if !filepath.IsAbs(dockerfilePath) {
  259. outputDockerfilePath = filepath.Join(basePath, dockerfilePath)
  260. }
  261. pathComp, err := filepath.Rel(absoluteBuildContextPath, outputDockerfilePath)
  262. if err != nil {
  263. return "", "", false, fmt.Errorf("error getting relative path: %w", err)
  264. }
  265. if !strings.HasPrefix(pathComp, ".."+string(os.PathSeparator)) {
  266. isDockerfileRelative = true
  267. return absoluteBuildContextPath, pathComp, isDockerfileRelative, nil
  268. }
  269. isDockerfileRelative = false
  270. outputDockerfilePath, err = filepath.Abs(outputDockerfilePath)
  271. if err != nil {
  272. return "", "", false, err
  273. }
  274. return absoluteBuildContextPath, outputDockerfilePath, isDockerfileRelative, nil
  275. }