github.com/redhat-appstudio/e2e-tests@v0.0.0-20240520140907-9709f6f59323/magefiles/upgrade/upgrade.go (about) 1 package upgrade 2 3 import ( 4 "context" 5 "fmt" 6 "os" 7 "strings" 8 "time" 9 10 "github.com/openshift/oc/pkg/cli/admin/upgrade" 11 "github.com/openshift/oc/pkg/cli/admin/upgrade/channel" 12 "github.com/redhat-appstudio/e2e-tests/pkg/utils" 13 "k8s.io/cli-runtime/pkg/genericclioptions" 14 "k8s.io/klog" 15 16 configv1 "github.com/openshift/api/config/v1" 17 configv1client "github.com/openshift/client-go/config/clientset/versioned" 18 corev1 "k8s.io/api/core/v1" 19 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 20 "k8s.io/apimachinery/pkg/types" 21 kubeclient "k8s.io/client-go/kubernetes" 22 podUtils "k8s.io/kubectl/pkg/util/podutils" 23 "sigs.k8s.io/controller-runtime/pkg/client/config" 24 25 k8swait "k8s.io/apimachinery/pkg/util/wait" 26 ) 27 28 const majorMinorVersionFormat = "4.%d" 29 const channelFormat = "%s-" + majorMinorVersionFormat 30 31 const spiVaultNamespaceName = "spi-vault" 32 const spiVaultPodName = "vault-0" 33 34 type statusHelper struct { 35 configClientset *configv1client.Clientset 36 kubeClientSet *kubeclient.Clientset 37 38 clusterVersion *configv1.ClusterVersion 39 currentProgress string 40 41 desiredChannel string 42 desiredMajorMinorVersion string 43 desiredFullVersion string 44 45 currentMajorMinorVersion string 46 initialVersion string 47 48 adminAckData string 49 } 50 51 func (s *statusHelper) update() error { 52 var err error 53 s.clusterVersion, err = s.configClientset.ConfigV1().ClusterVersions().Get(context.TODO(), "version", metav1.GetOptions{}) 54 if err != nil { 55 return err 56 } 57 if c := findClusterOperatorStatusCondition(s.clusterVersion.Status.Conditions, configv1.OperatorProgressing); c != nil && len(c.Message) > 0 { 58 s.currentProgress = c.Message 59 } 60 return nil 61 } 62 63 func newStatusHelper(kcs *kubeclient.Clientset, ccs *configv1client.Clientset) (*statusHelper, error) { 64 var initialVersion string 65 clusterVersion, err := ccs.ConfigV1().ClusterVersions().Get(context.TODO(), "version", metav1.GetOptions{}) 66 if err != nil { 67 return nil, err 68 } 69 for _, update := range clusterVersion.Status.History { 70 if update.State == configv1.CompletedUpdate { 71 initialVersion = update.Version 72 break 73 } 74 } 75 76 currentChannel := clusterVersion.Spec.Channel 77 klog.Infof("current channel is %q, current ocp version is %q", currentChannel, initialVersion) 78 79 sp := strings.Split(currentChannel, "-") 80 channelType, currentChannelVersion := sp[0], sp[1] 81 var minorVersion int 82 if _, err = fmt.Sscanf(currentChannelVersion, majorMinorVersionFormat, &minorVersion); err != nil { 83 return nil, fmt.Errorf("can't detect the next version channel: %+v", err) 84 } 85 currentMajorMinorVersion := fmt.Sprintf(majorMinorVersionFormat, minorVersion+1) 86 nextMajorMinorVersion := fmt.Sprintf(majorMinorVersionFormat, minorVersion+1) 87 nextVersionChannel := fmt.Sprintf(channelFormat, channelType, minorVersion+1) 88 89 var foundNextVersionChannel bool 90 for _, ch := range clusterVersion.Status.Desired.Channels { 91 if ch == nextVersionChannel { 92 foundNextVersionChannel = true 93 } 94 } 95 if !foundNextVersionChannel { 96 return nil, fmt.Errorf("the channel for updating to next version was not found in the list of desired channels: %+v", clusterVersion.Status.Desired.Channels) 97 } 98 99 cm, err := kcs.CoreV1().ConfigMaps("openshift-config-managed").Get(context.Background(), "admin-gates", metav1.GetOptions{}) 100 if err != nil { 101 return nil, fmt.Errorf("error when getting configmap admin-gates: %+v", err) 102 } 103 104 var adminAckData string 105 for k := range cm.Data { 106 if strings.Contains(k, currentMajorMinorVersion) { 107 adminAckData = fmt.Sprintf("{\"data\":{\"%s\":\"true\"}}", k) 108 break 109 } 110 } 111 112 klog.Infof("desired major.minor version is %q, desired channel is %q", nextMajorMinorVersion, nextVersionChannel) 113 114 return &statusHelper{ 115 kubeClientSet: kcs, 116 configClientset: ccs, 117 currentMajorMinorVersion: currentMajorMinorVersion, 118 desiredChannel: nextVersionChannel, 119 desiredMajorMinorVersion: nextMajorMinorVersion, 120 initialVersion: initialVersion, 121 adminAckData: adminAckData, 122 }, nil 123 } 124 125 func (s *statusHelper) isCompleted() bool { 126 if c := findClusterOperatorStatusCondition(s.clusterVersion.Status.Conditions, configv1.OperatorProgressing); c != nil && len(c.Message) > 0 { 127 if c.Status == configv1.ConditionTrue { 128 return false 129 } 130 } 131 if c := findClusterOperatorStatusCondition(s.clusterVersion.Status.Conditions, configv1.OperatorAvailable); c != nil && len(c.Message) > 0 { 132 if c.Status == configv1.ConditionTrue && strings.Contains(c.Message, s.desiredFullVersion) { 133 return true 134 } 135 } 136 return false 137 } 138 139 func (s *statusHelper) performAdminAck() error { 140 _, err := s.kubeClientSet.CoreV1().ConfigMaps("openshift-config").Patch(context.Background(), "admin-acks", types.MergePatchType, []byte(s.adminAckData), metav1.PatchOptions{}) 141 if err != nil { 142 return err 143 } 144 return nil 145 } 146 147 func PerformUpgrade() error { 148 149 u := upgrade.NewOptions(genericclioptions.IOStreams{Out: os.Stdout, ErrOut: os.Stderr}) 150 ch := channel.NewOptions(genericclioptions.IOStreams{Out: os.Stdout, ErrOut: os.Stderr}) 151 152 kubeconfig, err := config.GetConfig() 153 if err != nil { 154 return fmt.Errorf("error when getting config: %+v", err) 155 } 156 157 clientset, err := configv1client.NewForConfig(kubeconfig) 158 if err != nil { 159 return fmt.Errorf("error when creating client: %+v", err) 160 } 161 162 kubeClientset, err := kubeclient.NewForConfig(kubeconfig) 163 if err != nil { 164 return fmt.Errorf("error when creating client: %+v", err) 165 } 166 167 ch.Client = clientset 168 u.Client = clientset 169 170 us, err := newStatusHelper(kubeClientset, clientset) 171 if err != nil { 172 return fmt.Errorf("failed to initialize upgrade status helper: %+v", err) 173 } 174 175 ch.Channel = us.desiredChannel 176 177 err = ch.Run() 178 if err != nil { 179 return fmt.Errorf("failed when updating the upgrade channel to %q: %+v", ch.Channel, err) 180 } 181 182 if us.adminAckData != "" { 183 if err := us.performAdminAck(); err != nil { 184 return fmt.Errorf("unable to perform admin ack: %+v", err) 185 } 186 klog.Infof("admin ack %s successfully applied", us.adminAckData) 187 } 188 189 err = k8swait.PollUntilContextTimeout(context.Background(), 2*time.Second, 5*time.Minute, true, func(ctx context.Context) (done bool, err error) { 190 if err := us.update(); err != nil { 191 klog.Errorf("failed to get an update about upgrade status: %+v", err) 192 return false, nil 193 } 194 // Prefer standard (available) updates over conditional ones 195 for _, au := range us.clusterVersion.Status.AvailableUpdates { 196 if strings.Contains(au.Version, us.desiredMajorMinorVersion) { 197 klog.Infof("found the desired version %q in available updates", au.Version) 198 us.desiredFullVersion = au.Version 199 return true, nil 200 } 201 } 202 // https://www.redhat.com/en/blog/introducing-conditional-openshift-updates 203 for _, au := range us.clusterVersion.Status.ConditionalUpdates { 204 if strings.Contains(au.Release.Version, us.desiredMajorMinorVersion) { 205 klog.Infof("found the desired version %q in conditional updates", au.Release.Version) 206 us.desiredFullVersion = au.Release.Version 207 return true, nil 208 } 209 } 210 211 klog.Infof("desired minor version %q not yet present in available/conditional updates", us.desiredMajorMinorVersion) 212 return false, nil 213 }) 214 if err != nil { 215 return fmt.Errorf("timed out waiting for desired version %q to appear in available updates", us.desiredMajorMinorVersion) 216 } 217 218 u.ToLatestAvailable = true 219 u.AllowNotRecommended = true 220 221 if err := u.Run(); err != nil { 222 return fmt.Errorf("error when triggering the upgrade: %+v", err) 223 } 224 225 err = k8swait.PollUntilContextTimeout(context.Background(), 20*time.Second, 90*time.Minute, true, func(ctx context.Context) (done bool, err error) { 226 if err := us.update(); err != nil { 227 klog.Errorf("failed to get an update about upgrade status: %+v", err) 228 return false, nil 229 } 230 231 if us.isCompleted() { 232 klog.Infof("upgrade completed: %+v", utils.ToPrettyJSONString(us.clusterVersion.Status)) 233 return true, nil 234 } 235 klog.Infof("upgrading from %s - current progress: %s", us.initialVersion, us.currentProgress) 236 return false, nil 237 }) 238 if err != nil { 239 return fmt.Errorf("timed out waiting for the upgrade to finish: %s", utils.ToPrettyJSONString(us.clusterVersion.Status)) 240 } 241 242 if err := us.runPostUpgradeActions(); err != nil { 243 return err 244 } 245 246 return nil 247 } 248 249 func findClusterOperatorStatusCondition(conditions []configv1.ClusterOperatorStatusCondition, name configv1.ClusterStatusConditionType) *configv1.ClusterOperatorStatusCondition { 250 for i := range conditions { 251 if conditions[i].Type == name { 252 return &conditions[i] 253 } 254 } 255 return nil 256 } 257 258 func (us *statusHelper) runPostUpgradeActions() error { 259 // Restart vault pod in spi-vault namespace 260 // Required to perform on a dev cluster due to https://issues.redhat.com/browse/KFLUXBUGS-1112 261 klog.Infof("restarting pod '%s/%s' to unseal it", spiVaultNamespaceName, spiVaultPodName) 262 if err := us.kubeClientSet.CoreV1().Pods(spiVaultNamespaceName).Delete(context.Background(), spiVaultPodName, metav1.DeleteOptions{}); err != nil { 263 return fmt.Errorf("failed to restart pod '%s/%s' namespace: %+v", spiVaultNamespaceName, spiVaultNamespaceName, err) 264 } 265 266 time.Sleep(time.Second * 10) 267 268 err := k8swait.PollUntilContextTimeout(context.Background(), 5*time.Second, 5*time.Minute, true, func(ctx context.Context) (done bool, err error) { 269 pod, err := us.kubeClientSet.CoreV1().Pods(spiVaultNamespaceName).Get(context.Background(), spiVaultPodName, metav1.GetOptions{}) 270 if err != nil { 271 klog.Errorf("failed to get pod '%s/%s' namespace: %+v", spiVaultNamespaceName, spiVaultPodName, err) 272 return false, nil 273 } 274 if pod.Status.Phase == corev1.PodRunning && podUtils.IsPodReady(pod) { 275 klog.Infof("pod '%s/%s' successfully restarted and ready", spiVaultNamespaceName, spiVaultPodName) 276 return true, nil 277 } 278 klog.Infof("pod '%s/%s' is not yet ready", spiVaultNamespaceName, spiVaultPodName) 279 return false, nil 280 }) 281 if err != nil { 282 return fmt.Errorf("timed out waiting for '%s/%s' to be ready", spiVaultNamespaceName, spiVaultPodName) 283 } 284 285 return nil 286 }