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  }