GitOps for k8s
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

release_cmd.go 8.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. package main
  2. import (
  3. "context"
  4. "fmt"
  5. "io"
  6. "time"
  7. "github.com/spf13/cobra"
  8. "github.com/fluxcd/flux/api/v11"
  9. "github.com/fluxcd/flux/api/v6"
  10. "github.com/fluxcd/flux/cluster"
  11. "github.com/fluxcd/flux/job"
  12. "github.com/fluxcd/flux/resource"
  13. "github.com/fluxcd/flux/update"
  14. )
  15. type workloadReleaseOpts struct {
  16. *rootOpts
  17. namespace string
  18. workloads []string
  19. allWorkloads bool
  20. image string
  21. allImages bool
  22. exclude []string
  23. dryRun bool
  24. interactive bool
  25. force bool
  26. watch bool
  27. outputOpts
  28. cause update.Cause
  29. // Deprecated
  30. controllers []string
  31. }
  32. func newWorkloadRelease(parent *rootOpts) *workloadReleaseOpts {
  33. return &workloadReleaseOpts{rootOpts: parent}
  34. }
  35. func (opts *workloadReleaseOpts) Command() *cobra.Command {
  36. cmd := &cobra.Command{
  37. Use: "release",
  38. Short: "Release a new version of a workload.",
  39. Example: makeExample(
  40. "fluxctl release -n default --workload=deployment/foo --update-image=library/hello:v2",
  41. "fluxctl release --all --update-image=library/hello:v2",
  42. "fluxctl release --workload=default:deployment/foo --update-all-images",
  43. ),
  44. RunE: opts.RunE,
  45. }
  46. AddOutputFlags(cmd, &opts.outputOpts)
  47. AddCauseFlags(cmd, &opts.cause)
  48. cmd.Flags().StringVarP(&opts.namespace, "namespace", "n", getKubeConfigContextNamespace("default"), "Workload namespace")
  49. // Note: we cannot define a shorthand for --workload since it clashes with the shorthand of --watch
  50. cmd.Flags().StringSliceVarP(&opts.workloads, "workload", "", []string{}, "List of workloads to release <namespace>:<kind>/<name>")
  51. cmd.Flags().BoolVar(&opts.allWorkloads, "all", false, "Release all workloads")
  52. cmd.Flags().StringVarP(&opts.image, "update-image", "i", "", "Update a specific image")
  53. cmd.Flags().BoolVar(&opts.allImages, "update-all-images", false, "Update all images to latest versions")
  54. cmd.Flags().StringSliceVar(&opts.exclude, "exclude", []string{}, "List of workloads to exclude")
  55. cmd.Flags().BoolVar(&opts.dryRun, "dry-run", false, "Do not release anything; just report back what would have been done")
  56. cmd.Flags().BoolVar(&opts.interactive, "interactive", false, "Select interactively which containers to update")
  57. cmd.Flags().BoolVarP(&opts.force, "force", "f", false, "Disregard locks and container image filters (has no effect when used with --all or --update-all-images)")
  58. cmd.Flags().BoolVarP(&opts.watch, "watch", "w", false, "Watch rollout progress during release")
  59. // Deprecated
  60. cmd.Flags().StringSliceVarP(&opts.controllers, "controller", "c", []string{}, "List of controllers to release <namespace>:<kind>/<name>")
  61. cmd.Flags().MarkDeprecated("controller", "changed to --workload, use that instead")
  62. return cmd
  63. }
  64. func (opts *workloadReleaseOpts) RunE(cmd *cobra.Command, args []string) error {
  65. if len(args) != 0 {
  66. return errorWantedNoArgs
  67. }
  68. if err := checkExactlyOne("--update-image=<image> or --update-all-images", opts.image != "", opts.allImages); err != nil {
  69. return err
  70. }
  71. // Backwards compatibility with --controller until we remove it
  72. opts.workloads = append(opts.workloads, opts.controllers...)
  73. switch {
  74. case len(opts.workloads) <= 0 && !opts.allWorkloads:
  75. return newUsageError("please supply either --all, or at least one --workload=<workload>")
  76. case opts.watch && opts.dryRun:
  77. return newUsageError("cannot use --watch with --dry-run")
  78. case opts.force && opts.allWorkloads && opts.allImages:
  79. return newUsageError("--force has no effect when used with --all and --update-all-images")
  80. case opts.force && opts.allWorkloads:
  81. fmt.Fprintf(cmd.OutOrStderr(), "Warning: --force will not ignore locked workloads when used with --all\n")
  82. case opts.force && opts.allImages:
  83. fmt.Fprintf(cmd.OutOrStderr(), "Warning: --force will not ignore container image tags when used with --update-all-images\n")
  84. }
  85. var workloads []update.ResourceSpec
  86. if opts.allWorkloads {
  87. workloads = []update.ResourceSpec{update.ResourceSpecAll}
  88. } else {
  89. for _, workload := range opts.workloads {
  90. id, err := resource.ParseIDOptionalNamespace(opts.namespace, workload)
  91. if err != nil {
  92. return err
  93. }
  94. workloads = append(workloads, update.MakeResourceSpec(id))
  95. }
  96. }
  97. var (
  98. image update.ImageSpec
  99. err error
  100. )
  101. switch {
  102. case opts.image != "":
  103. image, err = update.ParseImageSpec(opts.image)
  104. if err != nil {
  105. return err
  106. }
  107. case opts.allImages:
  108. image = update.ImageSpecLatest
  109. }
  110. var kind update.ReleaseKind = update.ReleaseKindExecute
  111. if opts.dryRun || opts.interactive {
  112. kind = update.ReleaseKindPlan
  113. }
  114. var excludes []resource.ID
  115. for _, exclude := range opts.exclude {
  116. s, err := resource.ParseIDOptionalNamespace(opts.namespace, exclude)
  117. if err != nil {
  118. return err
  119. }
  120. excludes = append(excludes, s)
  121. }
  122. if kind == update.ReleaseKindPlan {
  123. fmt.Fprintf(cmd.OutOrStderr(), "Submitting dry-run release ...\n")
  124. } else {
  125. fmt.Fprintf(cmd.OutOrStderr(), "Submitting release ...\n")
  126. }
  127. ctx := context.Background()
  128. spec := update.ReleaseImageSpec{
  129. ServiceSpecs: workloads,
  130. ImageSpec: image,
  131. Kind: kind,
  132. Excludes: excludes,
  133. Force: opts.force,
  134. }
  135. jobID, err := opts.API.UpdateManifests(ctx, update.Spec{
  136. Type: update.Images,
  137. Cause: opts.cause,
  138. Spec: spec,
  139. })
  140. if err != nil {
  141. return err
  142. }
  143. result, err := awaitJob(ctx, opts.API, jobID, opts.Timeout)
  144. if err != nil {
  145. return err
  146. }
  147. if opts.interactive {
  148. spec, err := promptSpec(cmd.OutOrStdout(), result, opts.verbosity)
  149. spec.Force = opts.force
  150. if err != nil {
  151. fmt.Fprintln(cmd.OutOrStderr(), err.Error())
  152. return nil
  153. }
  154. fmt.Fprintf(cmd.OutOrStderr(), "Submitting selected release ...\n")
  155. jobID, err = opts.API.UpdateManifests(ctx, update.Spec{
  156. Type: update.Containers,
  157. Cause: opts.cause,
  158. Spec: spec,
  159. })
  160. if err != nil {
  161. fmt.Fprintln(cmd.OutOrStderr(), err.Error())
  162. return nil
  163. }
  164. opts.dryRun = false
  165. }
  166. err = await(ctx, cmd.OutOrStdout(), cmd.OutOrStderr(), opts.API, jobID, !opts.dryRun, opts.verbosity, opts.Timeout)
  167. if !opts.watch || err != nil {
  168. return err
  169. }
  170. fmt.Fprintf(cmd.OutOrStderr(), "Monitoring rollout ...\n")
  171. for {
  172. completed := 0
  173. workloads, err := opts.API.ListServicesWithOptions(ctx, v11.ListServicesOptions{Services: result.Result.AffectedResources()})
  174. if err != nil {
  175. return err
  176. }
  177. for _, workload := range workloads {
  178. writeRolloutStatus(workload, opts.verbosity)
  179. if workload.Status == cluster.StatusReady {
  180. completed++
  181. }
  182. if workload.Rollout.Messages != nil {
  183. fmt.Fprintf(cmd.OutOrStderr(), "There was a problem releasing %s:\n", workload.ID)
  184. for _, msg := range workload.Rollout.Messages {
  185. fmt.Fprintf(cmd.OutOrStderr(), "%s\n", msg)
  186. }
  187. return nil
  188. }
  189. }
  190. if completed == len(workloads) {
  191. fmt.Fprintf(cmd.OutOrStderr(), "All workloads ready.\n")
  192. return nil
  193. }
  194. time.Sleep(2000 * time.Millisecond)
  195. }
  196. }
  197. func writeRolloutStatus(workload v6.ControllerStatus, verbosity int) {
  198. w := newTabwriter()
  199. fmt.Fprintf(w, "WORKLOAD\tCONTAINER\tIMAGE\tRELEASE\tREPLICAS\n")
  200. if len(workload.Containers) > 0 {
  201. c := workload.Containers[0]
  202. fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d/%d", workload.ID, c.Name, c.Current.ID, workload.Status, workload.Rollout.Updated, workload.Rollout.Desired)
  203. if verbosity > 0 {
  204. fmt.Fprintf(w, " (%d outdated, %d ready)", workload.Rollout.Outdated, workload.Rollout.Ready)
  205. }
  206. fmt.Fprintf(w, "\n")
  207. for _, c := range workload.Containers[1:] {
  208. fmt.Fprintf(w, "\t%s\t%s\t\t\n", c.Name, c.Current.ID)
  209. }
  210. } else {
  211. fmt.Fprintf(w, "%s\t\t\t%s\t%d/%d", workload.ID, workload.Status, workload.Rollout.Updated, workload.Rollout.Desired)
  212. if verbosity > 0 {
  213. fmt.Fprintf(w, " (%d outdated, %d ready)", workload.Rollout.Outdated, workload.Rollout.Ready)
  214. }
  215. fmt.Fprintf(w, "\n")
  216. }
  217. fmt.Fprintln(w)
  218. w.Flush()
  219. }
  220. func promptSpec(out io.Writer, result job.Result, verbosity int) (update.ReleaseContainersSpec, error) {
  221. menu := update.NewMenu(out, result.Result, verbosity)
  222. containerSpecs, err := menu.Run()
  223. return update.ReleaseContainersSpec{
  224. Kind: update.ReleaseKindExecute,
  225. ContainerSpecs: containerSpecs,
  226. SkipMismatches: false,
  227. }, err
  228. }