github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/caas/kubernetes/provider/upgrade.go (about)

     1  // Copyright 2017 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package provider
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  
    10  	"github.com/juju/errors"
    11  	"github.com/juju/names/v5"
    12  	"github.com/juju/version/v2"
    13  	apps "k8s.io/api/apps/v1"
    14  	core "k8s.io/api/core/v1"
    15  	k8serrors "k8s.io/apimachinery/pkg/api/errors"
    16  	meta "k8s.io/apimachinery/pkg/apis/meta/v1"
    17  	appstyped "k8s.io/client-go/kubernetes/typed/apps/v1"
    18  
    19  	"github.com/juju/juju/caas"
    20  	"github.com/juju/juju/caas/kubernetes/provider/utils"
    21  	"github.com/juju/juju/cloudconfig/podcfg"
    22  	k8sannotations "github.com/juju/juju/core/annotations"
    23  	"github.com/juju/juju/core/paths"
    24  )
    25  
    26  func (k *kubernetesClient) Upgrade(agentTag string, vers version.Number) error {
    27  	tag, err := names.ParseTag(agentTag)
    28  	if err != nil {
    29  		return errors.Annotate(err, "parsing agent tag to upgrade")
    30  	}
    31  
    32  	logger.Infof("handling upgrade request for tag %q to %s", tag, vers.String())
    33  
    34  	switch tag.Kind() {
    35  	case names.MachineTagKind:
    36  	case names.ControllerAgentTagKind:
    37  		return k.upgradeController(vers)
    38  	case names.ApplicationTagKind:
    39  		return k.upgradeOperator(tag, vers)
    40  	case names.ModelTagKind:
    41  		return k.upgradeModelOperator(tag, vers)
    42  	case names.UnitTagKind:
    43  		// Sidecar charms don't have an upgrade step.
    44  		// See PR 14974
    45  		return nil
    46  	}
    47  	return errors.NotImplementedf("k8s upgrade for agent tag %q", agentTag)
    48  }
    49  
    50  func upgradeDeployment(
    51  	name,
    52  	imagePath string,
    53  	vers version.Number,
    54  	legacyLabels bool,
    55  	broker appstyped.DeploymentInterface,
    56  ) error {
    57  	de, err := broker.Get(context.TODO(), name, meta.GetOptions{})
    58  	if k8serrors.IsNotFound(err) {
    59  		return errors.NotFoundf(
    60  			"deployment %q", name)
    61  	} else if err != nil {
    62  		return errors.Annotatef(err,
    63  			"getting deployment to upgrade for name %q", name)
    64  	}
    65  
    66  	newInitContainers, err := upgradePodTemplateSpec(de.Spec.Template.Spec.InitContainers, imagePath, vers)
    67  	if err != nil {
    68  		return errors.Annotatef(err, "deployment %q", name)
    69  	}
    70  	de.Spec.Template.Spec.InitContainers = newInitContainers
    71  
    72  	newContainers, err := upgradePodTemplateSpec(de.Spec.Template.Spec.Containers, imagePath, vers)
    73  	if err != nil {
    74  		return errors.Annotatef(err, "deployment %q", name)
    75  	}
    76  	de.Spec.Template.Spec.Containers = newContainers
    77  
    78  	// update juju-version annotation.
    79  	// TODO(caas): consider how to upgrade to current annotations format safely.
    80  	// just ensure juju-version to current version for now.
    81  	de.SetAnnotations(
    82  		k8sannotations.New(de.GetAnnotations()).
    83  			Merge(utils.AnnotationsForVersion(vers.String(), legacyLabels)).ToMap(),
    84  	)
    85  	de.Spec.Template.SetAnnotations(
    86  		k8sannotations.New(de.Spec.Template.GetAnnotations()).
    87  			Merge(utils.AnnotationsForVersion(vers.String(), legacyLabels)).ToMap(),
    88  	)
    89  
    90  	if _, err := broker.Update(context.TODO(), de, meta.UpdateOptions{}); err != nil {
    91  		return errors.Annotatef(err, "updating deployment %q to %s",
    92  			name, vers)
    93  	}
    94  	return nil
    95  }
    96  
    97  func upgradeOperatorOrControllerStatefulSet(
    98  	appName string,
    99  	name string,
   100  	isOperator bool,
   101  	imagePath string,
   102  	baseImagePath string,
   103  	vers version.Number,
   104  	legacyLabels bool,
   105  	broker appstyped.StatefulSetInterface,
   106  ) error {
   107  	ss, err := broker.Get(context.TODO(), name, meta.GetOptions{})
   108  	if k8serrors.IsNotFound(err) {
   109  		return errors.NotFoundf(
   110  			"statefulset %q", name)
   111  	} else if err != nil {
   112  		return errors.Annotatef(err,
   113  			"getting statefulset to upgrade for name %q", name)
   114  	}
   115  
   116  	newInitContainers, err := upgradePodTemplateSpec(ss.Spec.Template.Spec.InitContainers, imagePath, vers)
   117  	if err != nil {
   118  		return errors.Annotatef(err, "statefulset %q", name)
   119  	}
   120  	ss.Spec.Template.Spec.InitContainers = newInitContainers
   121  
   122  	newContainers, err := upgradePodTemplateSpec(ss.Spec.Template.Spec.Containers, imagePath, vers)
   123  	if err != nil {
   124  		return errors.Annotatef(err, "statefulset %q", name)
   125  	}
   126  	ss.Spec.Template.Spec.Containers = newContainers
   127  
   128  	if isOperator {
   129  		err := patchOperatorToCharmBase(ss, appName, imagePath, baseImagePath)
   130  		if err != nil {
   131  			return errors.Annotatef(err, "unable to patch operator to charm base")
   132  		}
   133  	}
   134  
   135  	// update juju-version annotation.
   136  	// TODO(caas): consider how to upgrade to current annotations format safely.
   137  	// just ensure juju-version to current version for now.
   138  	ss.SetAnnotations(
   139  		k8sannotations.New(ss.GetAnnotations()).
   140  			Merge(utils.AnnotationsForVersion(vers.String(), legacyLabels)).ToMap(),
   141  	)
   142  	ss.Spec.Template.SetAnnotations(
   143  		k8sannotations.New(ss.Spec.Template.GetAnnotations()).
   144  			Merge(utils.AnnotationsForVersion(vers.String(), legacyLabels)).ToMap(),
   145  	)
   146  
   147  	if _, err := broker.Update(context.TODO(), ss, meta.UpdateOptions{}); err != nil {
   148  		return errors.Annotatef(err, "updating statefulset %q to %s",
   149  			name, vers)
   150  	}
   151  	return nil
   152  }
   153  
   154  func upgradePodTemplateSpec(
   155  	containers []core.Container,
   156  	imagePath string,
   157  	vers version.Number,
   158  ) ([]core.Container, error) {
   159  	jujudContainerIdx, found := findJujudContainer(containers)
   160  	if !found {
   161  		return containers, nil
   162  	}
   163  
   164  	if imagePath == "" {
   165  		var err error
   166  		imagePath, err = podcfg.RebuildOldOperatorImagePath(
   167  			containers[jujudContainerIdx].Image, vers)
   168  		if err != nil {
   169  			return nil, errors.Trace(err)
   170  		}
   171  	}
   172  
   173  	containersCopy := append([]core.Container(nil), containers...)
   174  	if containersCopy[jujudContainerIdx].Image != imagePath {
   175  		containersCopy[jujudContainerIdx].Image = imagePath
   176  	}
   177  	return containersCopy, nil
   178  }
   179  
   180  func findJujudContainer(containers []core.Container) (int, bool) {
   181  	for i, c := range containers {
   182  		if podcfg.IsJujuOCIImage(c.Image) {
   183  			return i, true
   184  		}
   185  	}
   186  	return -1, false
   187  }
   188  
   189  func patchOperatorToCharmBase(ss *apps.StatefulSet, appName string, imagePath string, baseImagePath string) error {
   190  	for _, container := range ss.Spec.Template.Spec.InitContainers {
   191  		if container.Name == operatorInitContainerName {
   192  			// Already patched.
   193  			return nil
   194  		}
   195  	}
   196  
   197  	ss.Spec.Template.Spec.InitContainers = append(ss.Spec.Template.Spec.InitContainers, core.Container{
   198  		Name:            operatorInitContainerName,
   199  		ImagePullPolicy: core.PullIfNotPresent,
   200  		Image:           imagePath,
   201  		Command: []string{
   202  			"/bin/sh",
   203  		},
   204  		Args: []string{
   205  			"-c",
   206  			fmt.Sprintf(
   207  				caas.JujudCopySh,
   208  				"/opt/juju",
   209  				"",
   210  			),
   211  		},
   212  		VolumeMounts: []core.VolumeMount{{
   213  			Name:      "juju-bins",
   214  			MountPath: "/opt/juju",
   215  		}},
   216  	})
   217  
   218  	ss.Spec.Template.Spec.Volumes = append(ss.Spec.Template.Spec.Volumes, core.Volume{
   219  		Name: "juju-bins",
   220  		VolumeSource: core.VolumeSource{
   221  			EmptyDir: &core.EmptyDirVolumeSource{},
   222  		},
   223  	})
   224  
   225  	for i, container := range ss.Spec.Template.Spec.Containers {
   226  		if !podcfg.IsJujuOCIImage(container.Image) {
   227  			continue
   228  		}
   229  
   230  		jujudCmd := fmt.Sprintf("exec $JUJU_TOOLS_DIR/jujud caasoperator --application-name=%s --debug", appName)
   231  		jujuDataDir := paths.DataDir(paths.OSUnixLike)
   232  		container.Image = baseImagePath
   233  		container.Args = []string{
   234  			"-c",
   235  			fmt.Sprintf(
   236  				caas.JujudStartUpAltSh,
   237  				jujuDataDir,
   238  				"tools",
   239  				"/opt/juju",
   240  				jujudCmd,
   241  			),
   242  		}
   243  		container.VolumeMounts = append(container.VolumeMounts, core.VolumeMount{
   244  			Name:      "juju-bins",
   245  			MountPath: "/opt/juju",
   246  		})
   247  
   248  		ss.Spec.Template.Spec.Containers[i] = container
   249  		break
   250  	}
   251  
   252  	return nil
   253  }