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 }