github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/worker/caasunitprovisioner/deployment_worker.go (about)

     1  // Copyright 2017 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package caasunitprovisioner
     5  
     6  import (
     7  	"reflect"
     8  
     9  	"github.com/juju/errors"
    10  	"github.com/juju/names/v5"
    11  	"github.com/juju/worker/v3"
    12  	"github.com/juju/worker/v3/catacomb"
    13  
    14  	apicaasunitprovisioner "github.com/juju/juju/api/controller/caasunitprovisioner"
    15  	"github.com/juju/juju/caas"
    16  	k8sprovider "github.com/juju/juju/caas/kubernetes/provider"
    17  	k8sspecs "github.com/juju/juju/caas/kubernetes/provider/specs"
    18  	"github.com/juju/juju/core/status"
    19  	"github.com/juju/juju/core/watcher"
    20  	"github.com/juju/juju/rpc/params"
    21  )
    22  
    23  // deploymentWorker informs the CAAS broker of how many pods to run and their spec, and
    24  // lets the broker figure out how to make that all happen.
    25  type deploymentWorker struct {
    26  	catacomb                 catacomb.Catacomb
    27  	application              string
    28  	provisioningStatusSetter ProvisioningStatusSetter
    29  	broker                   ServiceBroker
    30  	applicationGetter        ApplicationGetter
    31  	applicationUpdater       ApplicationUpdater
    32  	provisioningInfoGetter   ProvisioningInfoGetter
    33  	logger                   Logger
    34  }
    35  
    36  func newDeploymentWorker(
    37  	application string,
    38  	provisioningStatusSetter ProvisioningStatusSetter,
    39  	broker ServiceBroker,
    40  	provisioningInfoGetter ProvisioningInfoGetter,
    41  	applicationGetter ApplicationGetter,
    42  	applicationUpdater ApplicationUpdater,
    43  	logger Logger,
    44  ) (worker.Worker, error) {
    45  	w := &deploymentWorker{
    46  		application:              application,
    47  		provisioningStatusSetter: provisioningStatusSetter,
    48  		broker:                   broker,
    49  		provisioningInfoGetter:   provisioningInfoGetter,
    50  		applicationGetter:        applicationGetter,
    51  		applicationUpdater:       applicationUpdater,
    52  		logger:                   logger,
    53  	}
    54  	if err := catacomb.Invoke(catacomb.Plan{
    55  		Site: &w.catacomb,
    56  		Work: w.loop,
    57  	}); err != nil {
    58  		return nil, errors.Trace(err)
    59  	}
    60  	return w, nil
    61  }
    62  
    63  // Kill is part of the worker.Worker interface.
    64  func (w *deploymentWorker) Kill() {
    65  	w.catacomb.Kill(nil)
    66  }
    67  
    68  // Wait is part of the worker.Worker interface.
    69  func (w *deploymentWorker) Wait() error {
    70  	return w.catacomb.Wait()
    71  }
    72  
    73  func (w *deploymentWorker) loop() error {
    74  	appScaleWatcher, err := w.applicationGetter.WatchApplicationScale(w.application)
    75  	if err != nil {
    76  		return errors.Trace(err)
    77  	}
    78  	_ = w.catacomb.Add(appScaleWatcher)
    79  
    80  	var (
    81  		pw            watcher.NotifyWatcher
    82  		provisionChan watcher.NotifyChannel
    83  
    84  		currentScale int
    85  		currentInfo  *apicaasunitprovisioner.ProvisioningInfo
    86  	)
    87  
    88  	gotSpecNotify := false
    89  	serviceUpdated := false
    90  	desiredScale := 0
    91  	logger := w.logger
    92  	for {
    93  		select {
    94  		case <-w.catacomb.Dying():
    95  			return w.catacomb.ErrDying()
    96  		case _, ok := <-appScaleWatcher.Changes():
    97  			if !ok {
    98  				return errors.New("watcher closed channel")
    99  			}
   100  			var err error
   101  			desiredScale, err = w.applicationGetter.ApplicationScale(w.application)
   102  			if err != nil {
   103  				return errors.Trace(err)
   104  			}
   105  			logger.Debugf("desiredScale changed to %d", desiredScale)
   106  			if desiredScale > 0 && provisionChan == nil {
   107  				var err error
   108  				pw, err = w.provisioningInfoGetter.WatchPodSpec(w.application)
   109  				if err != nil {
   110  					return errors.Trace(err)
   111  				}
   112  				_ = w.catacomb.Add(pw)
   113  				provisionChan = pw.Changes()
   114  			}
   115  		case _, ok := <-provisionChan:
   116  			if !ok {
   117  				return errors.New("watcher closed channel")
   118  			}
   119  			gotSpecNotify = true
   120  		}
   121  		if desiredScale > 0 && !gotSpecNotify {
   122  			continue
   123  		}
   124  		info, err := w.provisioningInfoGetter.ProvisioningInfo(w.application)
   125  		if errors.IsNotFound(err) {
   126  			// No pod spec defined for a unit yet;
   127  			// wait for one to be set.
   128  			continue
   129  		} else if err != nil {
   130  			return errors.Trace(err)
   131  		}
   132  
   133  		if desiredScale == 0 {
   134  			if pw != nil {
   135  				_ = worker.Stop(pw)
   136  				provisionChan = nil
   137  			}
   138  			logger.Debugf("no units for %v", w.application)
   139  			err = w.broker.EnsureService(w.application, w.provisioningStatusSetter.SetOperatorStatus, &caas.ServiceParams{}, 0, nil)
   140  			if err != nil {
   141  				return errors.Trace(err)
   142  			}
   143  			currentScale = 0
   144  			continue
   145  		}
   146  
   147  		if desiredScale == currentScale && isProvisionInfoEqual(info, currentInfo) {
   148  			continue
   149  		}
   150  
   151  		// We need to disallow updates that k8s does not yet support,
   152  		// eg changing the filesystem or device directives, or deployment info.
   153  		// TODO(wallyworld) - support resizing of existing storage.
   154  		if currentInfo != nil {
   155  			var unsupportedReason string
   156  			if !reflect.DeepEqual(info.DeploymentInfo, currentInfo.DeploymentInfo) {
   157  				unsupportedReason = "k8s does not support updating deployment info"
   158  			} else if !reflect.DeepEqual(info.Filesystems, currentInfo.Filesystems) {
   159  				unsupportedReason = "k8s does not support updating storage"
   160  			} else if !reflect.DeepEqual(info.Devices, currentInfo.Devices) {
   161  				unsupportedReason = "k8s does not support updating devices"
   162  			}
   163  
   164  			if unsupportedReason != "" {
   165  				if err = w.provisioningStatusSetter.SetOperatorStatus(
   166  					w.application,
   167  					status.Error,
   168  					unsupportedReason,
   169  					nil,
   170  				); err != nil {
   171  					return errors.Trace(err)
   172  				}
   173  				continue
   174  			}
   175  		}
   176  
   177  		currentScale = desiredScale
   178  		currentInfo = info
   179  
   180  		appConfig, err := w.applicationGetter.ApplicationConfig(w.application)
   181  		if err != nil {
   182  			return errors.Trace(err)
   183  		}
   184  
   185  		serviceParams, err := provisionInfoToServiceParams(info)
   186  		if err != nil {
   187  			return errors.Trace(err)
   188  		}
   189  		err = w.broker.EnsureService(w.application, w.provisioningStatusSetter.SetOperatorStatus, serviceParams, desiredScale, appConfig)
   190  		if err != nil {
   191  			// Some errors we don't want to exit the worker.
   192  			if k8sprovider.MaskError(err) {
   193  				logger.Errorf(err.Error())
   194  				continue
   195  			}
   196  			return errors.Trace(err)
   197  		}
   198  		logger.Debugf("ensured deployment for %s for %v units", w.application, desiredScale)
   199  		if serviceParams.PodSpec == nil {
   200  			continue
   201  		}
   202  		if !serviceUpdated && !serviceParams.PodSpec.OmitServiceFrontend {
   203  			service, err := w.broker.GetService(w.application, caas.ModeWorkload, false)
   204  			if err != nil && !errors.IsNotFound(err) {
   205  				return errors.Annotate(err, "cannot get new service details")
   206  			}
   207  			if err = updateApplicationService(
   208  				names.NewApplicationTag(w.application), service, w.applicationUpdater,
   209  			); err != nil {
   210  				return errors.Trace(err)
   211  			}
   212  			serviceUpdated = true
   213  		}
   214  	}
   215  }
   216  
   217  func provisionInfoToServiceParams(info *apicaasunitprovisioner.ProvisioningInfo) (serviceParams *caas.ServiceParams, err error) {
   218  	if len(info.PodSpec) > 0 && len(info.RawK8sSpec) > 0 {
   219  		// This should never happen.
   220  		return nil, errors.NewForbidden(nil, "either PodSpec or RawK8sSpec can be set for each application, but not both")
   221  	}
   222  
   223  	serviceParams = &caas.ServiceParams{
   224  		Constraints:          info.Constraints,
   225  		ResourceTags:         info.Tags,
   226  		Filesystems:          info.Filesystems,
   227  		Devices:              info.Devices,
   228  		ImageDetails:         info.ImageDetails,
   229  		CharmModifiedVersion: info.CharmModifiedVersion,
   230  		Deployment: caas.DeploymentParams{
   231  			DeploymentType: caas.DeploymentType(info.DeploymentInfo.DeploymentType),
   232  			ServiceType:    caas.ServiceType(info.DeploymentInfo.ServiceType),
   233  		},
   234  	}
   235  	if len(info.PodSpec) > 0 {
   236  		if serviceParams.PodSpec, err = k8sspecs.ParsePodSpec(info.PodSpec); err != nil {
   237  			return nil, errors.Annotate(err, "cannot parse pod spec")
   238  		}
   239  	} else if len(info.RawK8sSpec) > 0 {
   240  		if serviceParams.RawK8sSpec, err = k8sspecs.ParseRawK8sSpec(info.RawK8sSpec); err != nil {
   241  			return nil, errors.Annotate(err, "cannot parse raw k8s spec")
   242  		}
   243  	}
   244  	return serviceParams, nil
   245  }
   246  
   247  // isProvisionInfoChanged checks if podspec or raw k8s spec changed or not.
   248  func isProvisionInfoEqual(newInfo, oldInfo *apicaasunitprovisioner.ProvisioningInfo) bool {
   249  	if newInfo == nil && oldInfo == nil {
   250  		return true
   251  	} else if newInfo == nil || oldInfo == nil {
   252  		return false
   253  	}
   254  
   255  	return newInfo.PodSpec == oldInfo.PodSpec &&
   256  		newInfo.RawK8sSpec == oldInfo.RawK8sSpec &&
   257  		newInfo.CharmModifiedVersion == oldInfo.CharmModifiedVersion
   258  }
   259  
   260  func updateApplicationService(appTag names.ApplicationTag, svc *caas.Service, updater ApplicationUpdater) error {
   261  	if svc == nil || svc.Id == "" {
   262  		return nil
   263  	}
   264  	return updater.UpdateApplicationService(
   265  		params.UpdateApplicationServiceArg{
   266  			ApplicationTag: appTag.String(),
   267  			ProviderId:     svc.Id,
   268  			Addresses:      params.FromProviderAddresses(svc.Addresses...),
   269  			Scale:          svc.Scale,
   270  			Generation:     svc.Generation,
   271  		},
   272  	)
   273  }