bluegreen.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. package cmd
  2. import (
  3. "context"
  4. "fmt"
  5. "os"
  6. "time"
  7. "github.com/fatih/color"
  8. api "github.com/porter-dev/porter/api/client"
  9. "github.com/porter-dev/porter/api/types"
  10. "github.com/spf13/cobra"
  11. appsv1 "k8s.io/api/apps/v1"
  12. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  13. intstrutil "k8s.io/apimachinery/pkg/util/intstr"
  14. )
  15. var deployCmd = &cobra.Command{
  16. Use: "deploy",
  17. }
  18. var bluegreenCmd = &cobra.Command{
  19. Use: "blue-green-switch",
  20. Short: "Automatically switches the traffic of a blue-green deployment once the new application is ready.",
  21. Run: func(cmd *cobra.Command, args []string) {
  22. err := checkLoginAndRun(args, bluegreenSwitch)
  23. if err != nil {
  24. os.Exit(1)
  25. }
  26. },
  27. }
  28. func init() {
  29. rootCmd.AddCommand(deployCmd)
  30. deployCmd.AddCommand(bluegreenCmd)
  31. bluegreenCmd.PersistentFlags().StringVar(
  32. &app,
  33. "app",
  34. "",
  35. "Application in the Porter dashboard",
  36. )
  37. bluegreenCmd.MarkPersistentFlagRequired("app")
  38. bluegreenCmd.PersistentFlags().StringVar(
  39. &tag,
  40. "tag",
  41. "",
  42. "The image tag to switch traffic to.",
  43. )
  44. bluegreenCmd.PersistentFlags().StringVar(
  45. &namespace,
  46. "namespace",
  47. "",
  48. "The namespace of the jobs.",
  49. )
  50. }
  51. func bluegreenSwitch(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
  52. // get the web release
  53. webRelease, err := client.GetRelease(context.Background(), config.Project, config.Cluster, namespace, app)
  54. if err != nil {
  55. return err
  56. }
  57. // if this application is not a web chart, throw an error
  58. if webRelease.Chart.Name() != "web" {
  59. return fmt.Errorf("target application is not a web chart")
  60. }
  61. // TODO: check that bluegreen is enabled
  62. // get the replicasets attached to the deployment
  63. sharedConf := &PorterRunSharedConfig{
  64. Client: client,
  65. }
  66. err = sharedConf.setSharedConfig()
  67. if err != nil {
  68. return fmt.Errorf("Could not retrieve kube credentials: %s", err.Error())
  69. }
  70. // if no job exists with the given revision, wait up to 30 minutes
  71. timeWait := time.Now().Add(30 * time.Minute)
  72. prevRefresh := time.Now()
  73. success := false
  74. for time.Now().Before(timeWait) {
  75. // refresh the client every 10 minutes
  76. if time.Now().After(prevRefresh.Add(10 * time.Minute)) {
  77. err = sharedConf.setSharedConfig()
  78. if err != nil {
  79. return fmt.Errorf("Could not retrieve kube credentials: %s", err.Error())
  80. }
  81. prevRefresh = time.Now()
  82. }
  83. depls, err := sharedConf.Clientset.AppsV1().Deployments(namespace).List(
  84. context.Background(),
  85. metav1.ListOptions{
  86. LabelSelector: fmt.Sprintf("app.kubernetes.io/instance=%s", app),
  87. },
  88. )
  89. if err != nil {
  90. return fmt.Errorf("could not get deployments: %s", err.Error())
  91. }
  92. foundDeployment := false
  93. // get the deployment which matches the new image tag
  94. for _, depl := range depls.Items {
  95. if depl.ObjectMeta.Name == fmt.Sprintf("%s-web-%s", app, tag) {
  96. foundDeployment = true
  97. // determine if the deployment has an appropriate number of ready replicas
  98. minUnavailable := *(depl.Spec.Replicas) - getMaxUnavailable(depl)
  99. // if the number of ready replicas is greater than the number of min unavailable,
  100. // the controller is ready for a traffic switch
  101. if minUnavailable <= depl.Status.ReadyReplicas {
  102. // push the deployment
  103. color.New(color.FgGreen).Printf("Switching traffic for app %s\n", app)
  104. deployAgent, err := updateGetAgent(client)
  105. if err != nil {
  106. return err
  107. }
  108. err = deployAgent.UpdateImageAndValues(map[string]interface{}{
  109. "bluegreen": map[string]interface{}{
  110. "enabled": true,
  111. "activeImageTag": tag,
  112. "imageTags": []string{tag},
  113. },
  114. })
  115. if err != nil {
  116. return err
  117. } else {
  118. success = true
  119. }
  120. }
  121. }
  122. }
  123. if !foundDeployment {
  124. return fmt.Errorf("target deployment not found. Did you specify the correct tag?")
  125. }
  126. if success {
  127. break
  128. }
  129. // otherwise, return no error
  130. time.Sleep(2 * time.Second)
  131. }
  132. if !success {
  133. return fmt.Errorf("new application was not ready within 30 minutes")
  134. }
  135. return nil
  136. }
  137. func getMaxUnavailable(deployment appsv1.Deployment) int32 {
  138. if deployment.Spec.Strategy.Type != appsv1.RollingUpdateDeploymentStrategyType || *(deployment.Spec.Replicas) == 0 {
  139. return int32(0)
  140. }
  141. desired := *(deployment.Spec.Replicas)
  142. maxUnavailable := deployment.Spec.Strategy.RollingUpdate.MaxUnavailable
  143. unavailable, err := intstrutil.GetScaledValueFromIntOrPercent(intstrutil.ValueOrDefault(maxUnavailable, intstrutil.FromInt(0)), int(desired), false)
  144. if err != nil {
  145. return 0
  146. }
  147. return int32(unavailable)
  148. }