job.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. package cmd
  2. import (
  3. "context"
  4. "fmt"
  5. "os"
  6. "strconv"
  7. "time"
  8. "github.com/fatih/color"
  9. api "github.com/porter-dev/porter/api/client"
  10. "github.com/porter-dev/porter/api/types"
  11. "github.com/spf13/cobra"
  12. v1 "k8s.io/api/batch/v1"
  13. )
  14. var jobCmd = &cobra.Command{
  15. Use: "job",
  16. }
  17. var batchImageUpdateCmd = &cobra.Command{
  18. Use: "update-images",
  19. Short: "Updates the image tag of all jobs in a namespace which use a specific image.",
  20. Long: fmt.Sprintf(`
  21. %s
  22. Updates the image tag of all jobs in a namespace which use a specific image. Note that for all
  23. jobs with version <= v0.4.0, this will trigger a new run of a manual job. However, for versions
  24. >= v0.5.0, this will not create a new run of the job.
  25. Example commands:
  26. %s
  27. This command is namespace-scoped and uses the default namespace. To specify a different namespace,
  28. use the --namespace flag:
  29. %s
  30. `,
  31. color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter job update-images\":"),
  32. color.New(color.FgGreen, color.Bold).Sprintf("porter job update-images --image-repo-uri my-image.registry.io --tag newtag"),
  33. color.New(color.FgGreen, color.Bold).Sprintf("porter job update-images --namespace custom-namespace --image-repo-uri my-image.registry.io --tag newtag"),
  34. ),
  35. Run: func(cmd *cobra.Command, args []string) {
  36. err := checkLoginAndRun(args, batchImageUpdate)
  37. if err != nil {
  38. os.Exit(1)
  39. }
  40. },
  41. }
  42. var waitCmd = &cobra.Command{
  43. Use: "wait",
  44. Short: "Waits for a job to complete.",
  45. Long: fmt.Sprintf(`
  46. %s
  47. Waits for a job with a given name and namespace to complete a run. If the job completes successfully,
  48. this command exits with exit code 0. Otherwise, this command exits with exit code 1.
  49. Example commands:
  50. %s
  51. This command is namespace-scoped and uses the default namespace. To specify a different namespace,
  52. use the --namespace flag:
  53. %s
  54. `,
  55. color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter job wait\":"),
  56. color.New(color.FgGreen, color.Bold).Sprintf("porter job wait --name job-example"),
  57. color.New(color.FgGreen, color.Bold).Sprintf("porter job wait --name job-example --namespace custom-namespace"),
  58. ),
  59. Run: func(cmd *cobra.Command, args []string) {
  60. err := checkLoginAndRun(args, waitForJob)
  61. if err != nil {
  62. os.Exit(1)
  63. }
  64. },
  65. }
  66. var imageRepoURI string
  67. func init() {
  68. rootCmd.AddCommand(jobCmd)
  69. jobCmd.AddCommand(batchImageUpdateCmd)
  70. jobCmd.AddCommand(waitCmd)
  71. batchImageUpdateCmd.PersistentFlags().StringVar(
  72. &tag,
  73. "tag",
  74. "",
  75. "The new image tag to use.",
  76. )
  77. batchImageUpdateCmd.PersistentFlags().StringVar(
  78. &namespace,
  79. "namespace",
  80. "",
  81. "The namespace of the jobs.",
  82. )
  83. batchImageUpdateCmd.PersistentFlags().StringVarP(
  84. &imageRepoURI,
  85. "image-repo-uri",
  86. "i",
  87. "",
  88. "Image repo uri",
  89. )
  90. batchImageUpdateCmd.MarkPersistentFlagRequired("image-repo-uri")
  91. batchImageUpdateCmd.MarkPersistentFlagRequired("tag")
  92. waitCmd.PersistentFlags().StringVar(
  93. &namespace,
  94. "namespace",
  95. "",
  96. "The namespace of the jobs.",
  97. )
  98. waitCmd.PersistentFlags().StringVar(
  99. &name,
  100. "name",
  101. "",
  102. "The name of the jobs.",
  103. )
  104. waitCmd.MarkPersistentFlagRequired("name")
  105. }
  106. func batchImageUpdate(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
  107. color.New(color.FgGreen).Println("Updating all jobs which use the image:", imageRepoURI)
  108. return client.UpdateBatchImage(
  109. context.TODO(),
  110. config.Project,
  111. config.Cluster,
  112. namespace,
  113. &types.UpdateImageBatchRequest{
  114. ImageRepoURI: imageRepoURI,
  115. Tag: tag,
  116. },
  117. )
  118. }
  119. // waits for a job with a given name/namespace
  120. func waitForJob(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
  121. // get the job release
  122. jobRelease, err := client.GetRelease(context.Background(), config.Project, config.Cluster, namespace, name)
  123. if err != nil {
  124. return err
  125. }
  126. // make sure the job chart has a manual job running
  127. pausedVal, ok := jobRelease.Release.Config["paused"]
  128. pausedErr := fmt.Errorf("this job template is not currently running a manual job")
  129. if !ok {
  130. return pausedErr
  131. }
  132. if pausedValBool, ok := pausedVal.(bool); ok && pausedValBool {
  133. return pausedErr
  134. }
  135. // attempt to parse out the timeout value for the job, given by `sidecar.timeout`
  136. // if it does not exist, we set the default to 30 minutes
  137. timeoutVal := getJobTimeoutValue(jobRelease.Release.Config)
  138. color.New(color.FgYellow).Printf("Waiting for timeout seconds %.1f\n", timeoutVal.Seconds())
  139. // if no job exists with the given revision, wait for the timeout value
  140. timeWait := time.Now().Add(timeoutVal)
  141. for time.Now().Before(timeWait) {
  142. // get the jobs for that job chart
  143. jobs, err := client.GetJobs(context.Background(), config.Project, config.Cluster, namespace, name)
  144. if err != nil {
  145. return err
  146. }
  147. job := getJobMatchingRevision(uint(jobRelease.Release.Version), jobs)
  148. if job == nil {
  149. time.Sleep(10 * time.Second)
  150. continue
  151. }
  152. // once job is running, wait for status to be completed, or failed
  153. // if failed, exit with non-zero exit code
  154. if job.Status.Failed > 0 {
  155. return fmt.Errorf("job failed")
  156. }
  157. if job.Status.Succeeded > 0 {
  158. return nil
  159. }
  160. // otherwise, return no error
  161. time.Sleep(10 * time.Second)
  162. }
  163. return fmt.Errorf("timed out waiting for job")
  164. }
  165. func getJobMatchingRevision(revision uint, jobs []v1.Job) *v1.Job {
  166. for _, job := range jobs {
  167. revisionLabel, revisionLabelExists := job.Labels["helm.sh/revision"]
  168. if !revisionLabelExists {
  169. continue
  170. }
  171. jobRevision, err := strconv.ParseUint(revisionLabel, 10, 64)
  172. if err != nil {
  173. continue
  174. }
  175. if uint(jobRevision) == revision {
  176. return &job
  177. }
  178. }
  179. return nil
  180. }
  181. func getJobTimeoutValue(values map[string]interface{}) time.Duration {
  182. defaultTimeout := time.Minute * 60
  183. sidecarInter, ok := values["sidecar"]
  184. if !ok {
  185. return defaultTimeout
  186. }
  187. sidecarVal, ok := sidecarInter.(map[string]interface{})
  188. if !ok {
  189. return defaultTimeout
  190. }
  191. timeoutInter, ok := sidecarVal["timeout"]
  192. if !ok {
  193. return defaultTimeout
  194. }
  195. timeoutVal, ok := timeoutInter.(int64)
  196. if !ok {
  197. return defaultTimeout
  198. }
  199. return time.Second * time.Duration(timeoutVal)
  200. }