update.go 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. package v2
  2. import (
  3. "context"
  4. "encoding/base64"
  5. "errors"
  6. "fmt"
  7. "os"
  8. "path/filepath"
  9. "strconv"
  10. "time"
  11. "github.com/porter-dev/porter/api/server/handlers/porter_app"
  12. "github.com/porter-dev/porter/api/types"
  13. "github.com/porter-dev/porter/internal/models"
  14. "github.com/fatih/color"
  15. api "github.com/porter-dev/porter/api/client"
  16. "github.com/porter-dev/porter/cli/cmd/config"
  17. )
  18. // UpdateInput is the input for the Update function
  19. type UpdateInput struct {
  20. // CLIConfig is the CLI configuration
  21. CLIConfig config.CLIConfig
  22. // Client is the Porter API client
  23. Client api.Client
  24. // PorterYamlPath is the path to the porter.yaml file
  25. PorterYamlPath string
  26. // AppName is the name of the app
  27. AppName string
  28. // PreviewApply is true when Update should create a new deployment target matching current git branch and apply to that target
  29. PreviewApply bool
  30. }
  31. // Update implements the functionality of the `porter apply` command for validate apply v2 projects
  32. func Update(ctx context.Context, inp UpdateInput) error {
  33. cliConf := inp.CLIConfig
  34. client := inp.Client
  35. deploymentTargetID, err := deploymentTargetFromConfig(ctx, client, cliConf.Project, cliConf.Cluster, inp.PreviewApply)
  36. if err != nil {
  37. return fmt.Errorf("error getting deployment target from config: %w", err)
  38. }
  39. var prNumber int
  40. prNumberEnv := os.Getenv("PORTER_PR_NUMBER")
  41. if prNumberEnv != "" {
  42. prNumber, err = strconv.Atoi(prNumberEnv)
  43. if err != nil {
  44. return fmt.Errorf("error parsing PORTER_PR_NUMBER to int: %w", err)
  45. }
  46. }
  47. porterYamlExists := len(inp.PorterYamlPath) != 0
  48. if porterYamlExists {
  49. _, err := os.Stat(filepath.Clean(inp.PorterYamlPath))
  50. if err != nil {
  51. if !os.IsNotExist(err) {
  52. return fmt.Errorf("error checking if porter yaml exists at path %s: %w", inp.PorterYamlPath, err)
  53. }
  54. // If a path was specified but the file does not exist, we will not immediately error out.
  55. // This supports users migrated from v1 who use a workflow file that always specifies a porter yaml path
  56. // in the apply command.
  57. porterYamlExists = false
  58. }
  59. }
  60. var b64YAML string
  61. if porterYamlExists {
  62. porterYaml, err := os.ReadFile(filepath.Clean(inp.PorterYamlPath))
  63. if err != nil {
  64. return fmt.Errorf("could not read porter yaml file: %w", err)
  65. }
  66. b64YAML = base64.StdEncoding.EncodeToString(porterYaml)
  67. color.New(color.FgGreen).Printf("Using Porter YAML at path: %s\n", inp.PorterYamlPath) // nolint:errcheck,gosec
  68. }
  69. commitSHA := commitSHAFromEnv()
  70. gitSource, err := gitSourceFromEnv()
  71. if err != nil {
  72. return fmt.Errorf("error getting git source from env: %w", err)
  73. }
  74. updateInput := api.UpdateAppInput{
  75. ProjectID: cliConf.Project,
  76. ClusterID: cliConf.Cluster,
  77. Name: inp.AppName,
  78. GitSource: gitSource,
  79. DeploymentTargetId: deploymentTargetID,
  80. Base64PorterYAML: b64YAML,
  81. CommitSHA: commitSHA,
  82. }
  83. updateResp, err := client.UpdateApp(ctx, updateInput)
  84. if err != nil {
  85. return fmt.Errorf("error calling update app endpoint: %w", err)
  86. }
  87. if updateResp.AppRevisionId == "" {
  88. return errors.New("app revision id is empty")
  89. }
  90. appName := updateResp.AppName
  91. buildSettings, err := client.GetBuildFromRevision(ctx, cliConf.Project, cliConf.Cluster, appName, updateResp.AppRevisionId)
  92. if err != nil {
  93. return fmt.Errorf("error getting build from revision: %w", err)
  94. }
  95. if buildSettings != nil && buildSettings.Build.Method != "" {
  96. eventID, _ := createBuildEvent(ctx, client, appName, cliConf.Project, cliConf.Cluster, deploymentTargetID, commitSHA)
  97. reportBuildFailureInput := reportBuildFailureInput{
  98. client: client,
  99. appName: appName,
  100. cliConf: cliConf,
  101. deploymentTargetID: deploymentTargetID,
  102. appRevisionID: updateResp.AppRevisionId,
  103. eventID: eventID,
  104. commitSHA: commitSHA,
  105. prNumber: prNumber,
  106. }
  107. if commitSHA == "" {
  108. return errors.New("build is required but commit SHA cannot be identified. Please set the PORTER_COMMIT_SHA environment variable or run apply in git repository with access to the git CLI")
  109. }
  110. color.New(color.FgGreen).Printf("Building new image with tag %s...\n", commitSHA) // nolint:errcheck,gosec
  111. buildInput, err := buildInputFromBuildSettings(cliConf.Project, appName, commitSHA, buildSettings.Image, buildSettings.Build)
  112. if err != nil {
  113. err := fmt.Errorf("error creating build input from build settings: %w", err)
  114. reportBuildFailureInput.buildError = err
  115. _ = reportBuildFailure(ctx, reportBuildFailureInput)
  116. return err
  117. }
  118. buildOutput := build(ctx, client, buildInput)
  119. if buildOutput.Error != nil {
  120. err := fmt.Errorf("error building app: %w", buildOutput.Error)
  121. reportBuildFailureInput.buildLogs = buildOutput.Logs
  122. reportBuildFailureInput.buildError = buildOutput.Error
  123. _ = reportBuildFailure(ctx, reportBuildFailureInput)
  124. return err
  125. }
  126. _, err = client.UpdateRevisionStatus(ctx, cliConf.Project, cliConf.Cluster, appName, updateResp.AppRevisionId, models.AppRevisionStatus_BuildSuccessful)
  127. if err != nil {
  128. err := fmt.Errorf("error updating revision status post build: %w", err)
  129. reportBuildFailureInput.buildError = err
  130. _ = reportBuildFailure(ctx, reportBuildFailureInput)
  131. return err
  132. }
  133. color.New(color.FgGreen).Printf("Successfully built image (tag: %s)\n", buildSettings.Image.Tag) // nolint:errcheck,gosec
  134. buildMetadata := make(map[string]interface{})
  135. buildMetadata["end_time"] = time.Now().UTC()
  136. _ = updateExistingEvent(ctx, client, appName, cliConf.Project, cliConf.Cluster, deploymentTargetID, types.PorterAppEventType_Build, eventID, types.PorterAppEventStatus_Success, buildMetadata)
  137. }
  138. color.New(color.FgGreen).Printf("Deploying new revision %s for app %s...\n", updateResp.AppRevisionId, appName) // nolint:errcheck,gosec
  139. now := time.Now().UTC()
  140. var status models.AppRevisionStatus
  141. for {
  142. if time.Since(now) > checkDeployTimeout {
  143. return errors.New("timed out waiting for app to deploy")
  144. }
  145. revision, err := client.GetRevision(ctx, cliConf.Project, cliConf.Cluster, appName, updateResp.AppRevisionId)
  146. if err != nil {
  147. return fmt.Errorf("error getting app revision status: %w", err)
  148. }
  149. status = revision.AppRevision.Status
  150. if status == models.AppRevisionStatus_DeployFailed || status == models.AppRevisionStatus_PredeployFailed || status == models.AppRevisionStatus_Deployed {
  151. break
  152. }
  153. if status == models.AppRevisionStatus_AwaitingPredeploy {
  154. color.New(color.FgGreen).Printf("Waiting for predeploy to complete..\n") // nolint:errcheck,gosec
  155. }
  156. time.Sleep(checkDeployFrequency)
  157. }
  158. _, _ = client.ReportRevisionStatus(ctx, api.ReportRevisionStatusInput{
  159. ProjectID: cliConf.Project,
  160. ClusterID: cliConf.Cluster,
  161. AppName: appName,
  162. AppRevisionID: updateResp.AppRevisionId,
  163. PRNumber: prNumber,
  164. CommitSHA: commitSHA,
  165. })
  166. if status == models.AppRevisionStatus_DeployFailed {
  167. return errors.New("app failed to deploy")
  168. }
  169. if status == models.AppRevisionStatus_PredeployFailed {
  170. return errors.New("predeploy failed for new revision")
  171. }
  172. color.New(color.FgGreen).Printf("Successfully applied new revision %s\n", updateResp.AppRevisionId) // nolint:errcheck,gosec
  173. return nil
  174. }
  175. // checkDeployTimeout is the timeout for checking if an app has been deployed
  176. const checkDeployTimeout = 15 * time.Minute
  177. // checkDeployFrequency is the frequency for checking if an app has been deployed
  178. const checkDeployFrequency = 10 * time.Second
  179. func gitSourceFromEnv() (porter_app.GitSource, error) {
  180. var source porter_app.GitSource
  181. var repoID uint
  182. if os.Getenv("GITHUB_REPOSITORY_ID") != "" {
  183. id, err := strconv.Atoi(os.Getenv("GITHUB_REPOSITORY_ID"))
  184. if err != nil {
  185. return source, fmt.Errorf("unable to parse GITHUB_REPOSITORY_ID to int: %w", err)
  186. }
  187. repoID = uint(id)
  188. }
  189. return porter_app.GitSource{
  190. GitBranch: os.Getenv("GITHUB_REF_NAME"),
  191. GitRepoID: repoID,
  192. GitRepoName: os.Getenv("GITHUB_REPOSITORY"),
  193. }, nil
  194. }
  195. func buildInputFromBuildSettings(projectID uint, appName string, commitSHA string, image porter_app.Image, build porter_app.BuildSettings) (buildInput, error) {
  196. var buildSettings buildInput
  197. if appName == "" {
  198. return buildSettings, errors.New("app name is empty")
  199. }
  200. if image.Repository == "" {
  201. return buildSettings, errors.New("image repository is empty")
  202. }
  203. if build.Method == "" {
  204. return buildSettings, errors.New("build method is empty")
  205. }
  206. if commitSHA == "" {
  207. return buildSettings, errors.New("commit SHA is empty")
  208. }
  209. return buildInput{
  210. ProjectID: projectID,
  211. AppName: appName,
  212. BuildContext: build.Context,
  213. Dockerfile: build.Dockerfile,
  214. BuildMethod: build.Method,
  215. Builder: build.Builder,
  216. BuildPacks: build.Buildpacks,
  217. ImageTag: commitSHA,
  218. RepositoryURL: image.Repository,
  219. }, nil
  220. }