github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/caas/kubernetes/provider/k8s.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 "bytes" 8 "context" 9 "encoding/json" 10 "fmt" 11 "regexp" 12 "strconv" 13 "strings" 14 "sync" 15 "time" 16 17 jujuclock "github.com/juju/clock" 18 "github.com/juju/collections/set" 19 "github.com/juju/errors" 20 "github.com/juju/loggo" 21 "github.com/juju/names/v5" 22 "github.com/juju/version/v2" 23 "github.com/kr/pretty" 24 apps "k8s.io/api/apps/v1" 25 core "k8s.io/api/core/v1" 26 networkingv1 "k8s.io/api/networking/v1" 27 storagev1 "k8s.io/api/storage/v1" 28 apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" 29 k8serrors "k8s.io/apimachinery/pkg/api/errors" 30 "k8s.io/apimachinery/pkg/api/resource" 31 v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 33 k8slabels "k8s.io/apimachinery/pkg/labels" 34 "k8s.io/apimachinery/pkg/types" 35 "k8s.io/apimachinery/pkg/util/intstr" 36 k8syaml "k8s.io/apimachinery/pkg/util/yaml" 37 "k8s.io/client-go/dynamic" 38 "k8s.io/client-go/informers" 39 "k8s.io/client-go/kubernetes" 40 "k8s.io/client-go/rest" 41 "k8s.io/utils/pointer" 42 43 "github.com/juju/juju/caas" 44 k8sapplication "github.com/juju/juju/caas/kubernetes/provider/application" 45 "github.com/juju/juju/caas/kubernetes/provider/constants" 46 k8sconstants "github.com/juju/juju/caas/kubernetes/provider/constants" 47 "github.com/juju/juju/caas/kubernetes/provider/resources" 48 k8sspecs "github.com/juju/juju/caas/kubernetes/provider/specs" 49 k8sstorage "github.com/juju/juju/caas/kubernetes/provider/storage" 50 "github.com/juju/juju/caas/kubernetes/provider/utils" 51 k8swatcher "github.com/juju/juju/caas/kubernetes/provider/watcher" 52 "github.com/juju/juju/caas/specs" 53 "github.com/juju/juju/cloudconfig/podcfg" 54 k8sannotations "github.com/juju/juju/core/annotations" 55 "github.com/juju/juju/core/arch" 56 "github.com/juju/juju/core/assumes" 57 coreconfig "github.com/juju/juju/core/config" 58 "github.com/juju/juju/core/devices" 59 "github.com/juju/juju/core/paths" 60 coreresources "github.com/juju/juju/core/resources" 61 "github.com/juju/juju/core/status" 62 "github.com/juju/juju/core/watcher" 63 "github.com/juju/juju/docker" 64 "github.com/juju/juju/environs" 65 environscloudspec "github.com/juju/juju/environs/cloudspec" 66 "github.com/juju/juju/environs/config" 67 envcontext "github.com/juju/juju/environs/context" 68 "github.com/juju/juju/storage" 69 jujuversion "github.com/juju/juju/version" 70 ) 71 72 var logger = loggo.GetLogger("juju.kubernetes.provider") 73 74 const ( 75 // labelResourceLifeCycleKey defines the label key for lifecycle of the global resources. 76 labelResourceLifeCycleKey = "juju-resource-lifecycle" 77 labelResourceLifeCycleValueModel = "model" 78 labelResourceLifeCycleValuePersistent = "persistent" 79 80 gpuAffinityNodeSelectorKey = "gpu" 81 82 operatorInitContainerName = "juju-init" 83 operatorContainerName = "juju-operator" 84 85 dataDirVolumeName = "juju-data-dir" 86 87 // InformerResyncPeriod is the default resync period set on IndexInformers 88 InformerResyncPeriod = time.Minute * 5 89 90 // A set of constants defining history limits for certain k8s deployment 91 // types. 92 // TODO We may want to make these configurable in the future. 93 94 // daemonsetRevisionHistoryLimit is the number of old history states to 95 // retain to allow rollbacks 96 daemonsetRevisionHistoryLimit int32 = 0 97 // deploymentRevisionHistoryLimit is the number of old ReplicaSets to retain 98 // to allow rollback 99 deploymentRevisionHistoryLimit int32 = 0 100 // statefulSetRevisionHistoryLimit is the maximum number of revisions that 101 // will be maintained in the StatefulSet's revision history 102 statefulSetRevisionHistoryLimit int32 = 0 103 ) 104 105 type kubernetesClient struct { 106 clock jujuclock.Clock 107 108 // namespace is the k8s namespace to use when 109 // creating k8s resources. 110 namespace string 111 112 annotations k8sannotations.Annotation 113 114 lock sync.Mutex 115 envCfgUnlocked *config.Config 116 k8sCfgUnlocked *rest.Config 117 clientUnlocked kubernetes.Interface 118 apiextensionsClientUnlocked apiextensionsclientset.Interface 119 dynamicClientUnlocked dynamic.Interface 120 121 newClient NewK8sClientFunc 122 newRestClient k8sspecs.NewK8sRestClientFunc 123 124 // modelUUID is the UUID of the model this client acts on. 125 modelUUID string 126 127 // newWatcher is the k8s watcher generator. 128 newWatcher k8swatcher.NewK8sWatcherFunc 129 newStringsWatcher k8swatcher.NewK8sStringsWatcherFunc 130 131 // informerFactoryUnlocked informer factory setup for tracking this model 132 informerFactoryUnlocked informers.SharedInformerFactory 133 134 // isLegacyLabels describes if this client should use and implement legacy 135 // labels or new ones 136 isLegacyLabels bool 137 138 // randomPrefix generates an annotation for stateful sets. 139 randomPrefix utils.RandomPrefixFunc 140 } 141 142 // To regenerate the mocks for the kubernetes Client used by this broker, 143 // run "go generate" from the package directory. 144 //go:generate go run go.uber.org/mock/mockgen -package mocks -destination mocks/k8sclient_mock.go k8s.io/client-go/kubernetes Interface 145 //go:generate go run go.uber.org/mock/mockgen -package mocks -destination mocks/context_mock.go context Context 146 //go:generate go run go.uber.org/mock/mockgen -package mocks -destination mocks/appv1_mock.go k8s.io/client-go/kubernetes/typed/apps/v1 AppsV1Interface,DeploymentInterface,StatefulSetInterface,DaemonSetInterface 147 //go:generate go run go.uber.org/mock/mockgen -package mocks -destination mocks/corev1_mock.go k8s.io/client-go/kubernetes/typed/core/v1 EventInterface,CoreV1Interface,NamespaceInterface,PodInterface,ServiceInterface,ConfigMapInterface,PersistentVolumeInterface,PersistentVolumeClaimInterface,SecretInterface,NodeInterface 148 //go:generate go run go.uber.org/mock/mockgen -package mocks -destination mocks/networkingv1beta1_mock.go -mock_names=IngressInterface=MockIngressV1Beta1Interface k8s.io/client-go/kubernetes/typed/networking/v1beta1 NetworkingV1beta1Interface,IngressInterface 149 //go:generate go run go.uber.org/mock/mockgen -package mocks -destination mocks/networkingv1_mock.go -mock_names=IngressInterface=MockIngressV1Interface k8s.io/client-go/kubernetes/typed/networking/v1 NetworkingV1Interface,IngressInterface,IngressClassInterface 150 //go:generate go run go.uber.org/mock/mockgen -package mocks -destination mocks/storagev1_mock.go k8s.io/client-go/kubernetes/typed/storage/v1 StorageV1Interface,StorageClassInterface 151 //go:generate go run go.uber.org/mock/mockgen -package mocks -destination mocks/rbacv1_mock.go k8s.io/client-go/kubernetes/typed/rbac/v1 RbacV1Interface,ClusterRoleBindingInterface,ClusterRoleInterface,RoleInterface,RoleBindingInterface 152 //go:generate go run go.uber.org/mock/mockgen -package mocks -destination mocks/apiextensionsv1beta1_mock.go -mock_names=CustomResourceDefinitionInterface=MockCustomResourceDefinitionV1Beta1Interface k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1 ApiextensionsV1beta1Interface,CustomResourceDefinitionInterface 153 //go:generate go run go.uber.org/mock/mockgen -package mocks -destination mocks/apiextensionsv1_mock.go -mock_names=CustomResourceDefinitionInterface=MockCustomResourceDefinitionV1Interface k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1 ApiextensionsV1Interface,CustomResourceDefinitionInterface 154 //go:generate go run go.uber.org/mock/mockgen -package mocks -destination mocks/apiextensionsclientset_mock.go -mock_names=Interface=MockApiExtensionsClientInterface k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset Interface 155 //go:generate go run go.uber.org/mock/mockgen -package mocks -destination mocks/discovery_mock.go k8s.io/client-go/discovery DiscoveryInterface 156 //go:generate go run go.uber.org/mock/mockgen -package mocks -destination mocks/dynamic_mock.go -mock_names=Interface=MockDynamicInterface k8s.io/client-go/dynamic Interface,ResourceInterface,NamespaceableResourceInterface 157 //go:generate go run go.uber.org/mock/mockgen -package mocks -destination mocks/admissionregistrationv1beta1_mock.go -mock_names=MutatingWebhookConfigurationInterface=MockMutatingWebhookConfigurationV1Beta1Interface,ValidatingWebhookConfigurationInterface=MockValidatingWebhookConfigurationV1Beta1Interface k8s.io/client-go/kubernetes/typed/admissionregistration/v1beta1 AdmissionregistrationV1beta1Interface,MutatingWebhookConfigurationInterface,ValidatingWebhookConfigurationInterface 158 //go:generate go run go.uber.org/mock/mockgen -package mocks -destination mocks/admissionregistrationv1_mock.go -mock_names=MutatingWebhookConfigurationInterface=MockMutatingWebhookConfigurationV1Interface,ValidatingWebhookConfigurationInterface=MockValidatingWebhookConfigurationV1Interface k8s.io/client-go/kubernetes/typed/admissionregistration/v1 AdmissionregistrationV1Interface,MutatingWebhookConfigurationInterface,ValidatingWebhookConfigurationInterface 159 //go:generate go run go.uber.org/mock/mockgen -package mocks -destination mocks/serviceaccountinformer_mock.go k8s.io/client-go/informers/core/v1 ServiceAccountInformer 160 //go:generate go run go.uber.org/mock/mockgen -package mocks -destination mocks/serviceaccountlister_mock.go k8s.io/client-go/listers/core/v1 ServiceAccountLister,ServiceAccountNamespaceLister 161 //go:generate go run go.uber.org/mock/mockgen -package mocks -destination mocks/sharedindexinformer_mock.go k8s.io/client-go/tools/cache SharedIndexInformer 162 //go:generate go run go.uber.org/mock/mockgen -package mocks -destination mocks/restclient_mock.go -mock_names=Interface=MockRestClientInterface k8s.io/client-go/rest Interface 163 //go:generate go run go.uber.org/mock/mockgen -package mocks -destination mocks/serviceaccount_mock.go k8s.io/client-go/kubernetes/typed/core/v1 ServiceAccountInterface 164 165 // NewK8sClientFunc defines a function which returns a k8s client based on the supplied config. 166 type NewK8sClientFunc func(c *rest.Config) (kubernetes.Interface, apiextensionsclientset.Interface, dynamic.Interface, error) 167 168 // newK8sBroker returns a kubernetes client for the specified k8s cluster. 169 func newK8sBroker( 170 controllerUUID string, 171 k8sRestConfig *rest.Config, 172 cfg *config.Config, 173 namespace string, 174 newClient NewK8sClientFunc, 175 newRestClient k8sspecs.NewK8sRestClientFunc, 176 newWatcher k8swatcher.NewK8sWatcherFunc, 177 newStringsWatcher k8swatcher.NewK8sStringsWatcherFunc, 178 randomPrefix utils.RandomPrefixFunc, 179 clock jujuclock.Clock, 180 ) (*kubernetesClient, error) { 181 k8sClient, apiextensionsClient, dynamicClient, err := newClient(k8sRestConfig) 182 if err != nil { 183 return nil, errors.Trace(err) 184 } 185 newCfg, err := providerInstance.newConfig(cfg) 186 if err != nil { 187 return nil, errors.Trace(err) 188 } 189 190 modelUUID := newCfg.UUID() 191 if modelUUID == "" { 192 return nil, errors.NotValidf("modelUUID is required") 193 } 194 195 isLegacy := false 196 if namespace != "" { 197 isLegacy, err = utils.IsLegacyModelLabels( 198 namespace, newCfg.Config.Name(), k8sClient.CoreV1().Namespaces()) 199 if err != nil { 200 return nil, errors.Trace(err) 201 } 202 } 203 204 client := &kubernetesClient{ 205 clock: clock, 206 clientUnlocked: k8sClient, 207 apiextensionsClientUnlocked: apiextensionsClient, 208 dynamicClientUnlocked: dynamicClient, 209 envCfgUnlocked: newCfg.Config, 210 k8sCfgUnlocked: k8sRestConfig, 211 informerFactoryUnlocked: informers.NewSharedInformerFactoryWithOptions( 212 k8sClient, 213 InformerResyncPeriod, 214 informers.WithNamespace(namespace), 215 ), 216 namespace: namespace, 217 modelUUID: modelUUID, 218 newWatcher: newWatcher, 219 newStringsWatcher: newStringsWatcher, 220 newClient: newClient, 221 newRestClient: newRestClient, 222 randomPrefix: randomPrefix, 223 annotations: k8sannotations.New(nil). 224 Add(utils.AnnotationModelUUIDKey(isLegacy), modelUUID), 225 isLegacyLabels: isLegacy, 226 } 227 if len(controllerUUID) > 0 { 228 client.annotations.Add(utils.AnnotationControllerUUIDKey(isLegacy), controllerUUID) 229 } 230 if namespace == "" { 231 return client, nil 232 } 233 234 ns, err := client.getNamespaceByName(namespace) 235 if errors.Is(err, errors.NotFound) { 236 return client, nil 237 } else if err != nil { 238 return nil, errors.Trace(err) 239 } 240 241 if !isK8sObjectOwnedByJuju(ns.ObjectMeta) { 242 return client, nil 243 } 244 245 if err := client.ensureNamespaceAnnotationForControllerUUID(ns, controllerUUID, isLegacy); err != nil { 246 return nil, errors.Trace(err) 247 } 248 return client, nil 249 } 250 251 func (k *kubernetesClient) ensureNamespaceAnnotationForControllerUUID( 252 ns *core.Namespace, 253 controllerUUID string, 254 isLegacy bool, 255 ) error { 256 if len(controllerUUID) == 0 { 257 // controllerUUID could be empty in add-k8s without -c because there might be no controller yet. 258 return nil 259 } 260 261 annotationControllerUUIDKey := utils.AnnotationControllerUUIDKey(isLegacy) 262 263 if !isLegacy { 264 // Ignore the controller uuid since it is handled below for model migrations. 265 expected := k.annotations.Copy() 266 expected.Remove(annotationControllerUUIDKey) 267 if ns != nil && !k8sannotations.New(ns.Annotations).HasAll(expected) { 268 // This should never happen unless we changed annotations for a new juju version. 269 // But in this case, we should have already managed to fix it in upgrade steps. 270 return fmt.Errorf("annotations %v for namespace %q %w must include %v", 271 ns.Annotations, k.namespace, errors.NotValid, k.annotations) 272 } 273 } 274 if ns.Annotations[annotationControllerUUIDKey] == controllerUUID { 275 // No change needs to be done. 276 return nil 277 } 278 // The model was just migrated from a different controller. 279 logger.Debugf("model %q was migrated from controller %q, updating the controller annotation to %q", k.namespace, 280 ns.Annotations[annotationControllerUUIDKey], controllerUUID, 281 ) 282 if err := k.ensureNamespaceAnnotations(ns); err != nil { 283 return errors.Trace(err) 284 } 285 _, err := k.client().CoreV1().Namespaces().Update(context.TODO(), ns, v1.UpdateOptions{}) 286 return errors.Trace(err) 287 } 288 289 // GetAnnotations returns current namespace's annotations. 290 func (k *kubernetesClient) GetAnnotations() k8sannotations.Annotation { 291 return k.annotations 292 } 293 294 var k8sversionNumberExtractor = regexp.MustCompile("[0-9]+") 295 296 // Version returns cluster version information. 297 func (k *kubernetesClient) Version() (ver *version.Number, err error) { 298 k8sver, err := k.client().Discovery().ServerVersion() 299 if err != nil { 300 return nil, errors.Trace(err) 301 } 302 303 clean := func(s string) string { 304 return k8sversionNumberExtractor.FindString(s) 305 } 306 307 ver = &version.Number{} 308 if ver.Major, err = strconv.Atoi(clean(k8sver.Major)); err != nil { 309 return nil, errors.Trace(err) 310 } 311 if ver.Minor, err = strconv.Atoi(clean(k8sver.Minor)); err != nil { 312 return nil, errors.Trace(err) 313 } 314 return ver, nil 315 } 316 317 // addAnnotations set an annotation to current namespace's annotations. 318 func (k *kubernetesClient) addAnnotations(key, value string) k8sannotations.Annotation { 319 return k.annotations.Add(key, value) 320 } 321 322 func (k *kubernetesClient) client() kubernetes.Interface { 323 k.lock.Lock() 324 defer k.lock.Unlock() 325 client := k.clientUnlocked 326 return client 327 } 328 329 func (k *kubernetesClient) extendedClient() apiextensionsclientset.Interface { 330 k.lock.Lock() 331 defer k.lock.Unlock() 332 client := k.apiextensionsClientUnlocked 333 return client 334 } 335 336 func (k *kubernetesClient) dynamicClient() dynamic.Interface { 337 k.lock.Lock() 338 defer k.lock.Unlock() 339 client := k.dynamicClientUnlocked 340 return client 341 } 342 343 // Config returns environ config. 344 func (k *kubernetesClient) Config() *config.Config { 345 k.lock.Lock() 346 defer k.lock.Unlock() 347 cfg := k.envCfgUnlocked 348 return cfg 349 } 350 351 func (k *kubernetesClient) k8sConfig() *rest.Config { 352 k.lock.Lock() 353 defer k.lock.Unlock() 354 return rest.CopyConfig(k.k8sCfgUnlocked) 355 } 356 357 // SetConfig is specified in the Environ interface. 358 func (k *kubernetesClient) SetConfig(cfg *config.Config) error { 359 k.lock.Lock() 360 defer k.lock.Unlock() 361 newCfg, err := providerInstance.newConfig(cfg) 362 if err != nil { 363 return errors.Trace(err) 364 } 365 k.envCfgUnlocked = newCfg.Config 366 return nil 367 } 368 369 // SetCloudSpec is specified in the environs.Environ interface. 370 func (k *kubernetesClient) SetCloudSpec(_ context.Context, spec environscloudspec.CloudSpec) error { 371 if k.namespace == "" { 372 return errNoNamespace 373 } 374 k.lock.Lock() 375 defer k.lock.Unlock() 376 377 k8sRestConfig, err := CloudSpecToK8sRestConfig(spec) 378 if err != nil { 379 return errors.Annotate(err, "cannot set cloud spec") 380 } 381 382 k.clientUnlocked, k.apiextensionsClientUnlocked, k.dynamicClientUnlocked, err = k.newClient(k8sRestConfig) 383 if err != nil { 384 return errors.Annotate(err, "cannot set cloud spec") 385 } 386 k.k8sCfgUnlocked = rest.CopyConfig(k8sRestConfig) 387 388 k.informerFactoryUnlocked = informers.NewSharedInformerFactoryWithOptions( 389 k.clientUnlocked, 390 InformerResyncPeriod, 391 informers.WithNamespace(k.namespace), 392 ) 393 return nil 394 } 395 396 // PrepareForBootstrap prepares for bootstrapping a controller. 397 func (k *kubernetesClient) PrepareForBootstrap(ctx environs.BootstrapContext, controllerName string) error { 398 alreadyExistErr := errors.NewAlreadyExists(nil, 399 fmt.Sprintf(`a controller called %q already exists on this k8s cluster. 400 Please bootstrap again and choose a different controller name.`, controllerName), 401 ) 402 403 k.namespace = DecideControllerNamespace(controllerName) 404 405 // ensure no existing namespace has the same name. 406 _, err := k.getNamespaceByName(k.namespace) 407 if err == nil { 408 return alreadyExistErr 409 } 410 if !errors.IsNotFound(err) { 411 return errors.Trace(err) 412 } 413 // Good, no existing namespace has the same name. 414 // Now, try to find if there is any existing controller running in this cluster. 415 // Note: we have to do this check before we are confident to support multi controllers running in same k8s cluster. 416 417 _, err = k.listNamespacesByAnnotations(k.annotations) 418 if err == nil { 419 return alreadyExistErr 420 } 421 if !errors.IsNotFound(err) { 422 return errors.Trace(err) 423 } 424 // All good, no existing controller found on the cluster. 425 // The namespace will be set to controller-name in newcontrollerStack. 426 427 // do validation on storage class. 428 _, err = k.validateOperatorStorage() 429 return errors.Trace(err) 430 } 431 432 // Create (environs.BootstrapEnviron) creates a new environ. 433 // It must raise an error satisfying IsAlreadyExists if the 434 // namespace is already used by another model. 435 func (k *kubernetesClient) Create(envcontext.ProviderCallContext, environs.CreateParams) error { 436 return errors.Trace(k.createNamespace(k.namespace)) 437 } 438 439 // EnsureImageRepoSecret ensures the image pull secret gets created. 440 func (k *kubernetesClient) EnsureImageRepoSecret(imageRepo docker.ImageRepoDetails) error { 441 if !imageRepo.IsPrivate() { 442 return nil 443 } 444 logger.Debugf("creating secret for image repo %q", imageRepo.Repository) 445 secretData, err := imageRepo.SecretData() 446 if err != nil { 447 return errors.Trace(err) 448 } 449 _, err = k.ensureOCIImageSecret( 450 constants.CAASImageRepoSecretName, 451 utils.LabelsJuju, secretData, 452 k.annotations, 453 ) 454 return errors.Trace(err) 455 } 456 457 // Bootstrap deploys controller with mongoDB together into k8s cluster. 458 func (k *kubernetesClient) Bootstrap( 459 ctx environs.BootstrapContext, 460 callCtx envcontext.ProviderCallContext, 461 args environs.BootstrapParams, 462 ) (*environs.BootstrapResult, error) { 463 464 if args.BootstrapSeries != "" { 465 return nil, errors.NotSupportedf("set series for bootstrapping to kubernetes") 466 } 467 468 storageClass, err := k.validateOperatorStorage() 469 if err != nil { 470 return nil, errors.Trace(err) 471 } 472 473 finalizer := func(ctx environs.BootstrapContext, pcfg *podcfg.ControllerPodConfig, opts environs.BootstrapDialOpts) (err error) { 474 podcfg.FinishControllerPodConfig(pcfg, k.Config(), args.ExtraAgentValuesForTesting) 475 if err = pcfg.VerifyConfig(); err != nil { 476 return errors.Trace(err) 477 } 478 479 logger.Debugf("controller pod config: \n%+v", pcfg) 480 481 // validate initial model name if we need to create it. 482 if initialModelName, has := pcfg.GetInitialModel(); has { 483 _, err := k.getNamespaceByName(initialModelName) 484 if err == nil { 485 return errors.NewAlreadyExists(nil, 486 fmt.Sprintf(` 487 namespace %q already exists in the cluster, 488 please choose a different initial model name then try again.`, initialModelName), 489 ) 490 } 491 if !errors.IsNotFound(err) { 492 return errors.Trace(err) 493 } 494 // hosted model is all good. 495 } 496 497 // we use controller name to name controller namespace in bootstrap time. 498 setControllerNamespace := func(controllerName string, broker *kubernetesClient) error { 499 nsName := DecideControllerNamespace(controllerName) 500 501 _, err := broker.GetNamespace(nsName) 502 if errors.IsNotFound(err) { 503 // all good. 504 // ensure controller specific annotations. 505 _ = broker.addAnnotations(utils.AnnotationControllerIsControllerKey(k.IsLegacyLabels()), "true") 506 return nil 507 } 508 if err == nil { 509 // this should never happen because we avoid it in broker.PrepareForBootstrap before reaching here. 510 return errors.NotValidf("existing namespace %q found", broker.namespace) 511 } 512 return errors.Trace(err) 513 } 514 515 if err := setControllerNamespace(pcfg.ControllerName, k); err != nil { 516 return errors.Trace(err) 517 } 518 519 // create configmap, secret, volume, statefulset, etc resources for controller stack. 520 controllerStack, err := newcontrollerStack(ctx, k8sconstants.JujuControllerStackName, storageClass, k, pcfg) 521 if err != nil { 522 return errors.Trace(err) 523 } 524 return errors.Annotate( 525 controllerStack.Deploy(), 526 "creating controller stack", 527 ) 528 } 529 530 podArch := arch.AMD64 531 if args.BootstrapConstraints.HasArch() { 532 podArch = *args.BootstrapConstraints.Arch 533 } 534 // TODO(wallyworld) - use actual series of controller pod image 535 return &environs.BootstrapResult{ 536 Arch: podArch, 537 Base: jujuversion.DefaultSupportedLTSBase(), 538 CaasBootstrapFinalizer: finalizer, 539 }, nil 540 } 541 542 // DestroyController implements the Environ interface. 543 func (k *kubernetesClient) DestroyController(ctx envcontext.ProviderCallContext, controllerUUID string) error { 544 // ensures all annnotations are set correctly, then we will accurately find the controller namespace to destroy it. 545 k.annotations.Merge( 546 k8sannotations.New(nil). 547 Add(utils.AnnotationControllerUUIDKey(k.IsLegacyLabels()), controllerUUID). 548 Add(utils.AnnotationControllerIsControllerKey(k.IsLegacyLabels()), "true"), 549 ) 550 return k.Destroy(ctx) 551 } 552 553 // SharedInformerFactory returns the default k8s SharedInformerFactory used by 554 // this broker. 555 func (k *kubernetesClient) SharedInformerFactory() informers.SharedInformerFactory { 556 k.lock.Lock() 557 defer k.lock.Unlock() 558 return k.informerFactoryUnlocked 559 } 560 561 func (k *kubernetesClient) CurrentModel() string { 562 k.lock.Lock() 563 defer k.lock.Unlock() 564 return k.envCfgUnlocked.Name() 565 } 566 567 // Provider is part of the Broker interface. 568 func (*kubernetesClient) Provider() caas.ContainerEnvironProvider { 569 return providerInstance 570 } 571 572 // Destroy is part of the Broker interface. 573 func (k *kubernetesClient) Destroy(ctx envcontext.ProviderCallContext) (err error) { 574 defer func() { 575 if errors.Cause(err) == context.DeadlineExceeded { 576 logger.Warningf("destroy k8s model timeout") 577 return 578 } 579 if err != nil && k8serrors.ReasonForError(err) == v1.StatusReasonUnknown { 580 logger.Warningf("k8s cluster is not accessible: %v", err) 581 err = nil 582 } 583 }() 584 585 errChan := make(chan error, 1) 586 done := make(chan struct{}) 587 588 destroyCtx, cancel := context.WithCancel(ctx) 589 defer cancel() 590 591 var wg sync.WaitGroup 592 wg.Add(1) 593 go k.deleteClusterScopeResourcesModelTeardown(destroyCtx, &wg, errChan) 594 wg.Add(1) 595 go k.deleteNamespaceModelTeardown(destroyCtx, &wg, errChan) 596 597 go func() { 598 wg.Wait() 599 close(done) 600 }() 601 602 for { 603 select { 604 case err = <-errChan: 605 if err != nil { 606 return errors.Trace(err) 607 } 608 case <-destroyCtx.Done(): 609 return destroyCtx.Err() 610 case <-done: 611 return destroyCtx.Err() 612 } 613 } 614 } 615 616 // APIVersion returns the version info for the cluster. 617 func (k *kubernetesClient) APIVersion() (string, error) { 618 ver, err := k.Version() 619 if err != nil { 620 return "", errors.Trace(err) 621 } 622 return ver.String(), nil 623 } 624 625 // getStorageClass returns a named storage class, first looking for 626 // one which is qualified by the current namespace if it's available. 627 func (k *kubernetesClient) getStorageClass(name string) (*storagev1.StorageClass, error) { 628 if k.namespace == "" { 629 return nil, errNoNamespace 630 } 631 storageClasses := k.client().StorageV1().StorageClasses() 632 qualifiedName := constants.QualifiedStorageClassName(k.namespace, name) 633 sc, err := storageClasses.Get(context.TODO(), qualifiedName, v1.GetOptions{}) 634 if err == nil { 635 return sc, nil 636 } 637 if !k8serrors.IsNotFound(err) { 638 return nil, errors.Trace(err) 639 } 640 return storageClasses.Get(context.TODO(), name, v1.GetOptions{}) 641 } 642 643 // GetService returns the service for the specified application. 644 func (k *kubernetesClient) GetService(appName string, mode caas.DeploymentMode, includeClusterIP bool) (*caas.Service, error) { 645 if k.namespace == "" { 646 return nil, errNoNamespace 647 } 648 services := k.client().CoreV1().Services(k.namespace) 649 labels := utils.LabelsForApp(appName, k.IsLegacyLabels()) 650 if mode == caas.ModeOperator { 651 labels = utils.LabelsForOperator(appName, OperatorAppTarget, k.IsLegacyLabels()) 652 } 653 if !k.IsLegacyLabels() { 654 labels = utils.LabelsMerge(labels, utils.LabelsJuju) 655 } 656 657 servicesList, err := services.List(context.TODO(), v1.ListOptions{ 658 LabelSelector: utils.LabelsToSelector(labels).String(), 659 }) 660 if err != nil { 661 return nil, errors.Trace(err) 662 } 663 var ( 664 result caas.Service 665 svc *core.Service 666 ) 667 // We may have the stateful set or deployment but service not done yet. 668 if len(servicesList.Items) > 0 { 669 for _, v := range servicesList.Items { 670 s := v 671 // Ignore any headless service for this app. 672 if !strings.HasSuffix(s.Name, "-endpoints") { 673 svc = &s 674 break 675 } 676 } 677 if svc != nil { 678 result.Id = string(svc.GetUID()) 679 result.Addresses = utils.GetSvcAddresses(svc, includeClusterIP) 680 } 681 } 682 683 if mode == caas.ModeOperator { 684 appName = k.operatorName(appName) 685 } 686 deploymentName := k.deploymentName(appName, true) 687 statefulsets := k.client().AppsV1().StatefulSets(k.namespace) 688 ss, err := statefulsets.Get(context.TODO(), deploymentName, v1.GetOptions{}) 689 if err != nil && !k8serrors.IsNotFound(err) { 690 return nil, errors.Trace(err) 691 } 692 if err == nil { 693 if ss.Spec.Replicas != nil { 694 scale := int(*ss.Spec.Replicas) 695 result.Scale = &scale 696 } 697 gen := ss.GetGeneration() 698 result.Generation = &gen 699 message, ssStatus, err := k.getStatefulSetStatus(ss) 700 if err != nil { 701 return nil, errors.Annotatef(err, "getting status for %s", ss.Name) 702 } 703 result.Status = status.StatusInfo{ 704 Status: ssStatus, 705 Message: message, 706 } 707 return &result, nil 708 } 709 710 deployments := k.client().AppsV1().Deployments(k.namespace) 711 deployment, err := deployments.Get(context.TODO(), deploymentName, v1.GetOptions{}) 712 if err != nil && !k8serrors.IsNotFound(err) { 713 return nil, errors.Trace(err) 714 } 715 if err == nil { 716 if deployment.Spec.Replicas != nil { 717 scale := int(*deployment.Spec.Replicas) 718 result.Scale = &scale 719 } 720 gen := deployment.GetGeneration() 721 result.Generation = &gen 722 message, deployStatus, err := k.getDeploymentStatus(deployment) 723 if err != nil { 724 return nil, errors.Annotatef(err, "getting status for %s", ss.Name) 725 } 726 result.Status = status.StatusInfo{ 727 Status: deployStatus, 728 Message: message, 729 } 730 return &result, nil 731 } 732 733 daemonsets := k.client().AppsV1().DaemonSets(k.namespace) 734 ds, err := daemonsets.Get(context.TODO(), deploymentName, v1.GetOptions{}) 735 if err != nil && !k8serrors.IsNotFound(err) { 736 return nil, errors.Trace(err) 737 } 738 if err == nil { 739 // The total number of nodes that should be running the daemon pod (including nodes correctly running the daemon pod). 740 scale := int(ds.Status.DesiredNumberScheduled) 741 result.Scale = &scale 742 743 gen := ds.GetGeneration() 744 result.Generation = &gen 745 message, dsStatus, err := k.getDaemonSetStatus(ds) 746 if err != nil { 747 return nil, errors.Annotatef(err, "getting status for %s", ss.Name) 748 } 749 result.Status = status.StatusInfo{ 750 Status: dsStatus, 751 Message: message, 752 } 753 } 754 return &result, nil 755 } 756 757 // DeleteService deletes the specified service with all related resources. 758 func (k *kubernetesClient) DeleteService(appName string) (err error) { 759 logger.Debugf("deleting application %s", appName) 760 761 // We prefer deleting resources using labels to do bulk deletion. 762 // Deleting resources using deployment name has been deprecated. 763 // But we keep it for now because some old resources created by 764 // very old Juju probably do not have proper labels set. 765 deploymentName := k.deploymentName(appName, true) 766 if err := k.deleteService(deploymentName); err != nil { 767 return errors.Trace(err) 768 } 769 if err := k.deleteStatefulSet(deploymentName); err != nil { 770 return errors.Trace(err) 771 } 772 if err := k.deleteService(headlessServiceName(deploymentName)); err != nil { 773 return errors.Trace(err) 774 } 775 if err := k.deleteDeployment(deploymentName); err != nil { 776 return errors.Trace(err) 777 } 778 779 if err := k.deleteStatefulSets(appName); err != nil { 780 return errors.Trace(err) 781 } 782 if err := k.deleteDeployments(appName); err != nil { 783 return errors.Trace(err) 784 } 785 if err := k.deleteServices(appName); err != nil { 786 return errors.Trace(err) 787 } 788 789 if err := k.deleteSecrets(appName); err != nil { 790 return errors.Trace(err) 791 } 792 if err := k.deleteConfigMaps(appName); err != nil { 793 return errors.Trace(err) 794 } 795 if err := k.deleteAllServiceAccountResources(appName); err != nil { 796 return errors.Trace(err) 797 } 798 // Order matters: delete custom resources first then custom resource definitions. 799 if err := k.deleteCustomResourcesForApp(appName); err != nil { 800 return errors.Trace(err) 801 } 802 if err := k.deleteCustomResourceDefinitionsForApp(appName); err != nil { 803 return errors.Trace(err) 804 } 805 806 if err := k.deleteMutatingWebhookConfigurationsForApp(appName); err != nil { 807 return errors.Trace(err) 808 } 809 if err := k.deleteValidatingWebhookConfigurationsForApp(appName); err != nil { 810 return errors.Trace(err) 811 } 812 813 if err := k.deleteIngressResources(appName); err != nil { 814 return errors.Trace(err) 815 } 816 817 if err := k.deleteDaemonSets(appName); err != nil { 818 return errors.Trace(err) 819 } 820 return nil 821 } 822 823 const applyRawSpecTimeoutSeconds = 20 824 825 func (k *kubernetesClient) applyRawK8sSpec( 826 appName, deploymentName string, 827 statusCallback caas.StatusCallbackFunc, 828 params *caas.ServiceParams, 829 numUnits int, 830 config coreconfig.ConfigAttributes, 831 ) (err error) { 832 if k.namespace == "" { 833 return errNoNamespace 834 } 835 836 if params == nil || len(params.RawK8sSpec) == 0 { 837 return errors.Errorf("missing raw k8s spec") 838 } 839 840 if params.Deployment.DeploymentType == "" { 841 params.Deployment.DeploymentType = caas.DeploymentStateless 842 if len(params.Filesystems) > 0 { 843 params.Deployment.DeploymentType = caas.DeploymentStateful 844 } 845 } 846 847 // TODO(caas): support Constraints, FileSystems, Devices, InitContainer for actions, etc. 848 if err := params.Deployment.DeploymentType.Validate(); err != nil { 849 return errors.Trace(err) 850 } 851 852 labelGetter := func(isNamespaced bool) map[string]string { 853 labels := utils.SelectorLabelsForApp(appName, k.IsLegacyLabels()) 854 if !isNamespaced { 855 labels = utils.LabelsMerge( 856 labels, 857 utils.LabelsForModel(k.CurrentModel(), k.IsLegacyLabels()), 858 ) 859 } 860 return labels 861 } 862 annotations := utils.ResourceTagsToAnnotations(params.ResourceTags, k.IsLegacyLabels()) 863 864 builder := k8sspecs.New( 865 deploymentName, k.namespace, params.Deployment, k.k8sConfig(), 866 labelGetter, annotations, k.newRestClient, 867 ) 868 ctx, cancel := context.WithTimeout(context.Background(), applyRawSpecTimeoutSeconds*time.Second) 869 defer cancel() 870 return builder.Deploy(ctx, params.RawK8sSpec, true) 871 } 872 873 // EnsureService creates or updates a service for pods with the given params. 874 func (k *kubernetesClient) EnsureService( 875 appName string, 876 statusCallback caas.StatusCallbackFunc, 877 params *caas.ServiceParams, 878 numUnits int, 879 config coreconfig.ConfigAttributes, 880 ) (err error) { 881 defer func() { 882 if err != nil { 883 _ = statusCallback(appName, status.Error, err.Error(), nil) 884 } 885 }() 886 887 logger.Debugf("creating/updating application %s", appName) 888 deploymentName := k.deploymentName(appName, true) 889 890 if numUnits < 0 { 891 return errors.Errorf("number of units must be >= 0") 892 } 893 if numUnits == 0 { 894 return k.deleteAllPods(appName, deploymentName) 895 } 896 if params.PodSpec != nil && len(params.RawK8sSpec) > 0 { 897 // This should never happen. 898 return errors.NotValidf("both pod spec and raw k8s spec provided") 899 } 900 901 if params.PodSpec != nil { 902 if config == nil { 903 return errors.Errorf("config for k8s app %q cannot be nil", appName) 904 } 905 return k.ensureService(appName, deploymentName, statusCallback, params, numUnits, config) 906 } 907 if len(params.RawK8sSpec) > 0 { 908 return k.applyRawK8sSpec(appName, deploymentName, statusCallback, params, numUnits, config) 909 } 910 return nil 911 } 912 913 func (k *kubernetesClient) ensureService( 914 appName, deploymentName string, 915 statusCallback caas.StatusCallbackFunc, 916 params *caas.ServiceParams, 917 numUnits int, 918 config coreconfig.ConfigAttributes, 919 ) (err error) { 920 921 if params == nil || params.PodSpec == nil { 922 return errors.Errorf("missing pod spec") 923 } 924 925 if err := params.Deployment.DeploymentType.Validate(); err != nil { 926 return errors.Trace(err) 927 } 928 929 var cleanups []func() 930 defer func() { 931 if err == nil { 932 return 933 } 934 for _, f := range cleanups { 935 f() 936 } 937 }() 938 939 workloadSpec, err := prepareWorkloadSpec(appName, deploymentName, params.PodSpec, params.ImageDetails) 940 if err != nil { 941 return errors.Annotatef(err, "parsing unit spec for %s", appName) 942 } 943 944 annotations := utils.ResourceTagsToAnnotations(params.ResourceTags, k.IsLegacyLabels()) 945 946 // ensure services. 947 if len(workloadSpec.Services) > 0 { 948 servicesCleanUps, err := k.ensureServicesForApp(appName, deploymentName, annotations, workloadSpec.Services) 949 cleanups = append(cleanups, servicesCleanUps...) 950 if err != nil { 951 return errors.Annotate(err, "creating or updating services") 952 } 953 } 954 955 // ensure configmap. 956 if len(workloadSpec.ConfigMaps) > 0 { 957 cmsCleanUps, err := k.ensureConfigMaps(appName, annotations, workloadSpec.ConfigMaps) 958 cleanups = append(cleanups, cmsCleanUps...) 959 if err != nil { 960 return errors.Annotate(err, "creating or updating configmaps") 961 } 962 } 963 964 // ensure secrets. 965 if len(workloadSpec.Secrets) > 0 { 966 secretsCleanUps, err := k.ensureSecrets(appName, annotations, workloadSpec.Secrets) 967 cleanups = append(cleanups, secretsCleanUps...) 968 if err != nil { 969 return errors.Annotate(err, "creating or updating secrets") 970 } 971 } 972 973 // ensure custom resource definitions. 974 crds := workloadSpec.CustomResourceDefinitions 975 if len(crds) > 0 { 976 crdCleanUps, err := k.ensureCustomResourceDefinitions(appName, annotations, crds) 977 cleanups = append(cleanups, crdCleanUps...) 978 if err != nil { 979 return errors.Annotate(err, "creating or updating custom resource definitions") 980 } 981 logger.Debugf("created/updated custom resource definition for %q.", appName) 982 983 } 984 // ensure custom resources. 985 crs := workloadSpec.CustomResources 986 if len(crs) > 0 { 987 crCleanUps, err := k.ensureCustomResources(appName, annotations, crs) 988 cleanups = append(cleanups, crCleanUps...) 989 if err != nil { 990 return errors.Annotate(err, "creating or updating custom resources") 991 } 992 logger.Debugf("created/updated custom resources for %q.", appName) 993 } 994 995 // ensure mutating webhook configurations. 996 mutatingWebhookConfigurations := workloadSpec.MutatingWebhookConfigurations 997 if len(mutatingWebhookConfigurations) > 0 { 998 cfgCleanUps, err := k.ensureMutatingWebhookConfigurations(appName, annotations, mutatingWebhookConfigurations) 999 cleanups = append(cleanups, cfgCleanUps...) 1000 if err != nil { 1001 return errors.Annotate(err, "creating or updating mutating webhook configurations") 1002 } 1003 logger.Debugf("created/updated mutating webhook configurations for %q.", appName) 1004 } 1005 // ensure validating webhook configurations. 1006 validatingWebhookConfigurations := workloadSpec.ValidatingWebhookConfigurations 1007 if len(validatingWebhookConfigurations) > 0 { 1008 cfgCleanUps, err := k.ensureValidatingWebhookConfigurations(appName, annotations, validatingWebhookConfigurations) 1009 cleanups = append(cleanups, cfgCleanUps...) 1010 if err != nil { 1011 return errors.Annotate(err, "creating or updating validating webhook configurations") 1012 } 1013 logger.Debugf("created/updated validating webhook configurations for %q.", appName) 1014 } 1015 1016 // ensure ingress resources. 1017 ings := workloadSpec.IngressResources 1018 if len(ings) > 0 { 1019 ingCleanUps, err := k.ensureIngressResources(appName, annotations, workloadSpec.IngressResources) 1020 cleanups = append(cleanups, ingCleanUps...) 1021 if err != nil { 1022 return errors.Annotate(err, "creating or updating ingress resources") 1023 } 1024 logger.Debugf("created/updated ingress resources for %q.", appName) 1025 } 1026 1027 for _, sa := range workloadSpec.ServiceAccounts { 1028 saCleanups, err := k.ensureServiceAccountForApp(appName, annotations, sa) 1029 cleanups = append(cleanups, saCleanups...) 1030 if err != nil { 1031 return errors.Annotate(err, "creating or updating service account") 1032 } 1033 } 1034 1035 if len(params.Devices) > 0 { 1036 if err = k.configureDevices(workloadSpec, params.Devices); err != nil { 1037 return errors.Annotatef(err, "configuring devices for %s", appName) 1038 } 1039 } 1040 if err := k8sapplication.ApplyConstraints( 1041 &workloadSpec.Pod.PodSpec, appName, params.Constraints, 1042 func(pod *core.PodSpec, resourceName core.ResourceName, value string) error { 1043 if len(pod.Containers) == 0 { 1044 return nil 1045 } 1046 // Just the first container is enough for scheduling purposes. 1047 pod.Containers[0].Resources.Requests, err = k8sapplication.MergeConstraint( 1048 resourceName, value, pod.Containers[0].Resources.Requests, 1049 ) 1050 if err != nil { 1051 return errors.Annotatef(err, "merging request constraint %s=%s", resourceName, value) 1052 } 1053 return nil 1054 }, 1055 ); err != nil { 1056 return errors.Trace(err) 1057 } 1058 1059 for _, c := range params.PodSpec.Containers { 1060 if c.ImageDetails.Password == "" { 1061 continue 1062 } 1063 imageSecretName := appSecretName(deploymentName, c.Name) 1064 if err := k.ensureOCIImageSecretForApp( 1065 imageSecretName, appName, &c.ImageDetails, annotations.Copy(), 1066 ); err != nil { 1067 return errors.Annotatef(err, "creating secrets for container: %s", c.Name) 1068 } 1069 cleanups = append(cleanups, func() { _ = k.deleteSecret(imageSecretName, "") }) 1070 } 1071 // Add a deployment controller or stateful set configured to create the specified number of units/pods. 1072 // Defensively check to see if a stateful set is already used. 1073 if params.Deployment.DeploymentType == "" { 1074 // TODO(caas): we should really change `params.Deployment` to be required. 1075 params.Deployment.DeploymentType = caas.DeploymentStateless 1076 if len(params.Filesystems) > 0 { 1077 params.Deployment.DeploymentType = caas.DeploymentStateful 1078 } 1079 } 1080 if params.Deployment.DeploymentType != caas.DeploymentStateful { 1081 // TODO(caas): make sure deployment type is immutable. 1082 // Either not found or params.Deployment.DeploymentType == existing resource type. 1083 _, err := k.getStatefulSet(deploymentName) 1084 if err != nil && !errors.IsNotFound(err) { 1085 return errors.Trace(err) 1086 } 1087 if err == nil { 1088 params.Deployment.DeploymentType = caas.DeploymentStateful 1089 logger.Debugf("no updated filesystems but already using stateful set for %q", appName) 1090 } 1091 } 1092 1093 if err = validateDeploymentType(params.Deployment.DeploymentType, params, workloadSpec.Service); err != nil { 1094 return errors.Trace(err) 1095 } 1096 1097 hasService := !params.PodSpec.OmitServiceFrontend && !params.Deployment.ServiceType.IsOmit() 1098 if hasService { 1099 var ports []core.ContainerPort 1100 for _, c := range workloadSpec.Pod.Containers { 1101 for _, p := range c.Ports { 1102 if p.ContainerPort == 0 { 1103 continue 1104 } 1105 ports = append(ports, p) 1106 } 1107 } 1108 if len(ports) == 0 { 1109 return errors.Errorf("ports are required for kubernetes service %q", appName) 1110 } 1111 1112 serviceAnnotations := annotations.Copy() 1113 // Merge any service annotations from the charm. 1114 if workloadSpec.Service != nil { 1115 serviceAnnotations.Merge(k8sannotations.New(workloadSpec.Service.Annotations)) 1116 } 1117 // Merge any service annotations from the CLI. 1118 deployAnnotations, err := config.GetStringMap(serviceAnnotationsKey, nil) 1119 if err != nil { 1120 return errors.Annotatef(err, "unexpected annotations: %#v", config.Get(serviceAnnotationsKey, nil)) 1121 } 1122 serviceAnnotations.Merge(k8sannotations.New(deployAnnotations)) 1123 1124 config[serviceAnnotationsKey] = serviceAnnotations.ToMap() 1125 if err := k.configureService(appName, deploymentName, ports, params, config); err != nil { 1126 return errors.Annotatef(err, "creating or updating service for %v", appName) 1127 } 1128 } 1129 1130 numPods := int32(numUnits) 1131 workloadResourceAnnotations := annotations.Copy(). 1132 // To solve https://bugs.launchpad.net/juju/+bug/1875481/comments/23 (`jujud caas-unit-init --upgrade` 1133 // does NOT work on containers are not using root as default USER), 1134 // CharmModifiedVersion is added for triggering rolling upgrade on workload pods to synchronise 1135 // charm files to workload pods via init container when charm was upgraded. 1136 // This approach was inspired from `kubectl rollout restart`. 1137 Add(utils.AnnotationCharmModifiedVersionKey(k.IsLegacyLabels()), strconv.Itoa(params.CharmModifiedVersion)) 1138 1139 switch params.Deployment.DeploymentType { 1140 case caas.DeploymentStateful: 1141 if err := k.configureHeadlessService(appName, deploymentName, annotations.Copy()); err != nil { 1142 return errors.Annotate(err, "creating or updating headless service") 1143 } 1144 cleanups = append(cleanups, func() { _ = k.deleteService(headlessServiceName(deploymentName)) }) 1145 if err := k.configureStatefulSet(appName, deploymentName, workloadResourceAnnotations.Copy(), workloadSpec, params.PodSpec.Containers, &numPods, params.Filesystems); err != nil { 1146 return errors.Annotate(err, "creating or updating StatefulSet") 1147 } 1148 cleanups = append(cleanups, func() { _ = k.deleteDeployment(appName) }) 1149 case caas.DeploymentStateless: 1150 cleanUpDeployment, err := k.configureDeployment(appName, deploymentName, workloadResourceAnnotations.Copy(), workloadSpec, params.PodSpec.Containers, &numPods, params.Filesystems) 1151 cleanups = append(cleanups, cleanUpDeployment...) 1152 if err != nil { 1153 return errors.Annotate(err, "creating or updating Deployment") 1154 } 1155 case caas.DeploymentDaemon: 1156 cleanUpDaemonSet, err := k.configureDaemonSet(appName, deploymentName, workloadResourceAnnotations.Copy(), workloadSpec, params.PodSpec.Containers, params.Filesystems) 1157 cleanups = append(cleanups, cleanUpDaemonSet...) 1158 if err != nil { 1159 return errors.Annotate(err, "creating or updating DaemonSet") 1160 } 1161 default: 1162 // This should never happened because we have validated both in this method and in `charm.v6`. 1163 return errors.NotSupportedf("deployment type %q", params.Deployment.DeploymentType) 1164 } 1165 return nil 1166 } 1167 1168 func validateDeploymentType(deploymentType caas.DeploymentType, params *caas.ServiceParams, svcSpec *specs.ServiceSpec) error { 1169 if svcSpec == nil { 1170 return nil 1171 } 1172 if deploymentType != caas.DeploymentStateful { 1173 if svcSpec.ScalePolicy != "" { 1174 return errors.NewNotValid(nil, fmt.Sprintf("ScalePolicy is only supported for %s applications", caas.DeploymentStateful)) 1175 } 1176 } 1177 return nil 1178 } 1179 1180 func (k *kubernetesClient) deleteAllPods(appName, deploymentName string) error { 1181 if k.namespace == "" { 1182 return errNoNamespace 1183 } 1184 zero := int32(0) 1185 statefulsets := k.client().AppsV1().StatefulSets(k.namespace) 1186 statefulSet, err := statefulsets.Get(context.TODO(), deploymentName, v1.GetOptions{}) 1187 if err != nil && !k8serrors.IsNotFound(err) { 1188 return errors.Trace(err) 1189 } 1190 if err == nil { 1191 statefulSet.Spec.Replicas = &zero 1192 _, err = statefulsets.Update(context.TODO(), statefulSet, v1.UpdateOptions{}) 1193 return errors.Trace(err) 1194 } 1195 1196 deployments := k.client().AppsV1().Deployments(k.namespace) 1197 deployment, err := deployments.Get(context.TODO(), deploymentName, v1.GetOptions{}) 1198 if k8serrors.IsNotFound(err) { 1199 return nil 1200 } 1201 if err != nil { 1202 return errors.Trace(err) 1203 } 1204 deployment.Spec.Replicas = &zero 1205 _, err = deployments.Update(context.TODO(), deployment, v1.UpdateOptions{}) 1206 return errors.Trace(err) 1207 } 1208 1209 type annotationGetter interface { 1210 GetAnnotations() map[string]string 1211 } 1212 1213 // This random snippet will be included to the pvc name so that if the same app 1214 // is deleted and redeployed again, the pvc retains a unique name. 1215 // Only generate it once, and record it on the workload resource annotations . 1216 func (k *kubernetesClient) getStorageUniqPrefix(getMeta func() (annotationGetter, error)) (string, error) { 1217 r, err := getMeta() 1218 if err == nil { 1219 if uniqID := r.GetAnnotations()[utils.AnnotationKeyApplicationUUID(k.IsLegacyLabels())]; uniqID != "" { 1220 return uniqID, nil 1221 } 1222 } else if !errors.IsNotFound(err) { 1223 return "", errors.Trace(err) 1224 } 1225 return k.randomPrefix() 1226 } 1227 1228 func (k *kubernetesClient) configureDevices(unitSpec *workloadSpec, devices []devices.KubernetesDeviceParams) error { 1229 for i := range unitSpec.Pod.Containers { 1230 resources := unitSpec.Pod.Containers[i].Resources 1231 for _, dev := range devices { 1232 err := mergeDeviceConstraints(dev, &resources) 1233 if err != nil { 1234 return errors.Annotatef(err, "merging device constraint %+v to %#v", dev, resources) 1235 } 1236 } 1237 unitSpec.Pod.Containers[i].Resources = resources 1238 } 1239 nodeLabel, err := getNodeSelectorFromDeviceConstraints(devices) 1240 if err != nil { 1241 return err 1242 } 1243 if nodeLabel != "" { 1244 nodeSelector := buildNodeSelector(nodeLabel) 1245 if unitSpec.Pod.NodeSelector != nil { 1246 for k, v := range nodeSelector { 1247 unitSpec.Pod.NodeSelector[k] = v 1248 } 1249 } else { 1250 unitSpec.Pod.NodeSelector = nodeSelector 1251 } 1252 } 1253 return nil 1254 } 1255 1256 type configMapNameFunc func(fileSetName string) string 1257 1258 func (k *kubernetesClient) configurePodFiles( 1259 appName string, 1260 annotations map[string]string, 1261 workloadSpec *workloadSpec, 1262 containers []specs.ContainerSpec, 1263 cfgMapName configMapNameFunc, 1264 ) error { 1265 for _, container := range containers { 1266 for _, fileSet := range container.VolumeConfig { 1267 vol, err := k.fileSetToVolume(appName, annotations, workloadSpec, fileSet, cfgMapName) 1268 if err != nil { 1269 return errors.Trace(err) 1270 } 1271 if err = k8sstorage.PushUniqueVolume(&workloadSpec.Pod.PodSpec, vol, false); err != nil { 1272 return errors.Trace(err) 1273 } 1274 if err := configVolumeMount( 1275 container, workloadSpec, 1276 core.VolumeMount{ 1277 // TODO(caas): add more config fields support(SubPath, ReadOnly, etc). 1278 Name: vol.Name, 1279 MountPath: fileSet.MountPath, 1280 }, 1281 ); err != nil { 1282 return errors.Trace(err) 1283 } 1284 } 1285 } 1286 return nil 1287 } 1288 1289 func configVolumeMount(container specs.ContainerSpec, workloadSpec *workloadSpec, volMount core.VolumeMount) error { 1290 if container.Init { 1291 for i, c := range workloadSpec.Pod.InitContainers { 1292 if c.Name == container.Name { 1293 workloadSpec.Pod.InitContainers[i].VolumeMounts = append(workloadSpec.Pod.InitContainers[i].VolumeMounts, volMount) 1294 return nil 1295 } 1296 } 1297 return errors.Annotatef(errors.NotFoundf("init container %q", container.Name), "configuring volume mount %q", volMount.Name) 1298 } 1299 for i, c := range workloadSpec.Pod.Containers { 1300 if c.Name == container.Name { 1301 workloadSpec.Pod.Containers[i].VolumeMounts = append(workloadSpec.Pod.Containers[i].VolumeMounts, volMount) 1302 return nil 1303 } 1304 } 1305 return errors.Annotatef(errors.NotFoundf("container %q", container.Name), "configuring volume mount %q", volMount.Name) 1306 } 1307 1308 func (k *kubernetesClient) configureStorage( 1309 appName string, legacy bool, uniquePrefix string, 1310 filesystems []storage.KubernetesFilesystemParams, 1311 podSpec *core.PodSpec, 1312 handlePVC func(core.PersistentVolumeClaim, string, bool) error, 1313 ) error { 1314 pvcNameGetter := func(i int, storageName string) string { 1315 s := fmt.Sprintf("%s-%s", storageName, uniquePrefix) 1316 if legacy { 1317 s = fmt.Sprintf("juju-%s-%d", storageName, i) 1318 } 1319 return s 1320 } 1321 fsNames := set.NewStrings() 1322 for i, fs := range filesystems { 1323 if fsNames.Contains(fs.StorageName) { 1324 return errors.NotValidf("duplicated storage name %q for %q", fs.StorageName, appName) 1325 } 1326 fsNames.Add(fs.StorageName) 1327 1328 readOnly := false 1329 if fs.Attachment != nil { 1330 readOnly = fs.Attachment.ReadOnly 1331 } 1332 1333 vol, pvc, err := k.filesystemToVolumeInfo(i, fs, pvcNameGetter) 1334 if err != nil { 1335 return errors.Trace(err) 1336 } 1337 mountPath := k8sstorage.GetMountPathForFilesystem(i, appName, fs) 1338 if vol != nil { 1339 logger.Debugf("using volume for %s filesystem %s: %s", appName, fs.StorageName, pretty.Sprint(*vol)) 1340 if err = k8sstorage.PushUniqueVolume(podSpec, *vol, false); err != nil { 1341 return errors.Trace(err) 1342 } 1343 podSpec.Containers[0].VolumeMounts = append(podSpec.Containers[0].VolumeMounts, core.VolumeMount{ 1344 Name: vol.Name, 1345 MountPath: mountPath, 1346 }) 1347 } 1348 if pvc != nil && handlePVC != nil { 1349 logger.Debugf("using persistent volume claim for %s filesystem %s: %s", appName, fs.StorageName, pretty.Sprint(*pvc)) 1350 if err = handlePVC(*pvc, mountPath, readOnly); err != nil { 1351 return errors.Trace(err) 1352 } 1353 } 1354 } 1355 return nil 1356 } 1357 1358 func ensureJujuInitContainer(podSpec *core.PodSpec, operatorImagePath string) error { 1359 initContainer, vol, volMounts, err := getJujuInitContainerAndStorageInfo(operatorImagePath) 1360 if err != nil { 1361 return errors.Trace(err) 1362 } 1363 1364 replaceOrUpdateInitContainer := func() { 1365 for i, v := range podSpec.InitContainers { 1366 if v.Name == initContainer.Name { 1367 podSpec.InitContainers[i] = initContainer 1368 return 1369 } 1370 } 1371 podSpec.InitContainers = append(podSpec.InitContainers, initContainer) 1372 } 1373 replaceOrUpdateInitContainer() 1374 1375 if err = k8sstorage.PushUniqueVolume(podSpec, vol, true); err != nil { 1376 return errors.Trace(err) 1377 } 1378 1379 for i := range podSpec.Containers { 1380 container := &podSpec.Containers[i] 1381 for _, volMount := range volMounts { 1382 k8sstorage.PushUniqueVolumeMount(container, volMount) 1383 } 1384 } 1385 return nil 1386 } 1387 1388 func getJujuInitContainerAndStorageInfo(operatorImagePath string) (container core.Container, vol core.Volume, volMounts []core.VolumeMount, err error) { 1389 dataDir := paths.DataDir(paths.OSUnixLike) 1390 jujuExec := paths.JujuExec(paths.OSUnixLike) 1391 jujudCmd := ` 1392 initCmd=$($JUJU_TOOLS_DIR/jujud help commands | grep caas-unit-init) 1393 if test -n "$initCmd"; then 1394 exec $JUJU_TOOLS_DIR/jujud caas-unit-init --debug --wait; 1395 else 1396 exit 0 1397 fi`[1:] 1398 container = core.Container{ 1399 Name: caas.InitContainerName, 1400 Image: operatorImagePath, 1401 ImagePullPolicy: core.PullIfNotPresent, 1402 VolumeMounts: []core.VolumeMount{{ 1403 Name: dataDirVolumeName, 1404 MountPath: dataDir, 1405 }}, 1406 WorkingDir: dataDir, 1407 Command: []string{ 1408 "/bin/sh", 1409 }, 1410 Args: []string{ 1411 "-c", 1412 fmt.Sprintf( 1413 caas.JujudStartUpSh, 1414 dataDir, 1415 "tools", 1416 jujudCmd, 1417 ), 1418 }, 1419 } 1420 vol = core.Volume{ 1421 Name: dataDirVolumeName, 1422 VolumeSource: core.VolumeSource{ 1423 EmptyDir: &core.EmptyDirVolumeSource{}, 1424 }, 1425 } 1426 volMounts = []core.VolumeMount{ 1427 {Name: dataDirVolumeName, MountPath: dataDir}, 1428 {Name: dataDirVolumeName, MountPath: jujuExec, SubPath: "tools/jujud"}, 1429 } 1430 return container, vol, volMounts, nil 1431 } 1432 1433 func podAnnotations(annotations k8sannotations.Annotation) k8sannotations.Annotation { 1434 // Add standard security annotations. 1435 return annotations. 1436 Add("apparmor.security.beta.kubernetes.io/pod", "runtime/default"). 1437 Add("seccomp.security.beta.kubernetes.io/pod", "docker/default") 1438 } 1439 1440 // https://kubernetes.io/docs/tasks/manage-daemon/update-daemon-set/#daemonset-update-strategy 1441 func updateStrategyForDaemonSet(strategy specs.UpdateStrategy) (o apps.DaemonSetUpdateStrategy, err error) { 1442 strategyType := apps.DaemonSetUpdateStrategyType(strategy.Type) 1443 1444 o = apps.DaemonSetUpdateStrategy{Type: strategyType} 1445 switch strategyType { 1446 case apps.OnDeleteDaemonSetStrategyType: 1447 if strategy.RollingUpdate != nil { 1448 return o, errors.NewNotValid(nil, fmt.Sprintf("rolling update spec is not supported for %q", strategyType)) 1449 } 1450 case apps.RollingUpdateDaemonSetStrategyType: 1451 if strategy.RollingUpdate != nil { 1452 if strategy.RollingUpdate.Partition != nil || strategy.RollingUpdate.MaxSurge != nil { 1453 return o, errors.NotValidf("rolling update spec for daemonset") 1454 } 1455 if strategy.RollingUpdate.MaxUnavailable == nil { 1456 return o, errors.NewNotValid(nil, "rolling update spec maxUnavailable is missing") 1457 } 1458 o.RollingUpdate = &apps.RollingUpdateDaemonSet{ 1459 MaxUnavailable: k8sspecs.IntOrStringToK8s(*strategy.RollingUpdate.MaxUnavailable), 1460 } 1461 } 1462 default: 1463 return o, errors.NotValidf("strategy type %q for daemonset", strategyType) 1464 } 1465 return o, nil 1466 } 1467 1468 func (k *kubernetesClient) configureDaemonSet( 1469 appName, deploymentName string, 1470 annotations k8sannotations.Annotation, 1471 workloadSpec *workloadSpec, 1472 containers []specs.ContainerSpec, 1473 filesystems []storage.KubernetesFilesystemParams, 1474 ) (cleanUps []func(), err error) { 1475 logger.Debugf("creating/updating daemon set for %s", appName) 1476 1477 // Add the specified file to the pod spec. 1478 cfgName := func(fileSetName string) string { 1479 return applicationConfigMapName(deploymentName, fileSetName) 1480 } 1481 if err := k.configurePodFiles(appName, annotations, workloadSpec, containers, cfgName); err != nil { 1482 return cleanUps, errors.Trace(err) 1483 } 1484 1485 storageUniqueID, err := k.getStorageUniqPrefix(func() (annotationGetter, error) { 1486 return k.getDaemonSet(deploymentName) 1487 }) 1488 if err != nil { 1489 return cleanUps, errors.Trace(err) 1490 } 1491 1492 selectorLabels := utils.SelectorLabelsForApp(appName, k.IsLegacyLabels()) 1493 daemonSet := &apps.DaemonSet{ 1494 ObjectMeta: v1.ObjectMeta{ 1495 Name: deploymentName, 1496 Labels: utils.LabelsForApp(appName, k.IsLegacyLabels()), 1497 Annotations: k8sannotations.New(nil). 1498 Merge(annotations). 1499 Add(utils.AnnotationKeyApplicationUUID(k.IsLegacyLabels()), storageUniqueID).ToMap(), 1500 }, 1501 Spec: apps.DaemonSetSpec{ 1502 // TODO(caas): MinReadySeconds support. 1503 Selector: &v1.LabelSelector{ 1504 MatchLabels: selectorLabels, 1505 }, 1506 RevisionHistoryLimit: pointer.Int32Ptr(daemonsetRevisionHistoryLimit), 1507 Template: core.PodTemplateSpec{ 1508 ObjectMeta: v1.ObjectMeta{ 1509 GenerateName: deploymentName + "-", 1510 Labels: utils.LabelsMerge(workloadSpec.Pod.Labels, selectorLabels), 1511 Annotations: podAnnotations(k8sannotations.New(workloadSpec.Pod.Annotations).Merge(annotations).Copy()).ToMap(), 1512 }, 1513 Spec: workloadSpec.Pod.PodSpec, 1514 }, 1515 }, 1516 } 1517 if workloadSpec.Service != nil && workloadSpec.Service.UpdateStrategy != nil { 1518 if daemonSet.Spec.UpdateStrategy, err = updateStrategyForDaemonSet(*workloadSpec.Service.UpdateStrategy); err != nil { 1519 return cleanUps, errors.Trace(err) 1520 } 1521 } 1522 1523 handlePVC := func(pvc core.PersistentVolumeClaim, mountPath string, readOnly bool) error { 1524 cs, err := k.configurePVCForStatelessResource(pvc, mountPath, readOnly, &daemonSet.Spec.Template.Spec) 1525 cleanUps = append(cleanUps, cs...) 1526 return errors.Trace(err) 1527 } 1528 // Storage support for daemonset is new. 1529 legacy := false 1530 if err := k.configureStorage(appName, legacy, storageUniqueID, filesystems, &daemonSet.Spec.Template.Spec, handlePVC); err != nil { 1531 return cleanUps, errors.Trace(err) 1532 } 1533 1534 cU, err := k.ensureDaemonSet(daemonSet) 1535 cleanUps = append(cleanUps, cU) 1536 return cleanUps, errors.Trace(err) 1537 } 1538 1539 // https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#strategy 1540 func updateStrategyForDeployment(strategy specs.UpdateStrategy) (o apps.DeploymentStrategy, err error) { 1541 strategyType := apps.DeploymentStrategyType(strategy.Type) 1542 1543 o = apps.DeploymentStrategy{Type: strategyType} 1544 switch strategyType { 1545 case apps.RecreateDeploymentStrategyType: 1546 if strategy.RollingUpdate != nil { 1547 return o, errors.NewNotValid(nil, fmt.Sprintf("rolling update spec is not supported for %q", strategyType)) 1548 } 1549 case apps.RollingUpdateDeploymentStrategyType: 1550 if strategy.RollingUpdate != nil { 1551 if strategy.RollingUpdate.Partition != nil { 1552 return o, errors.NotValidf("rolling update spec for deployment") 1553 } 1554 if strategy.RollingUpdate.MaxSurge == nil && strategy.RollingUpdate.MaxUnavailable == nil { 1555 return o, errors.NewNotValid(nil, "empty rolling update spec") 1556 } 1557 o.RollingUpdate = &apps.RollingUpdateDeployment{} 1558 if strategy.RollingUpdate.MaxSurge != nil { 1559 o.RollingUpdate.MaxSurge = k8sspecs.IntOrStringToK8s(*strategy.RollingUpdate.MaxSurge) 1560 } 1561 if strategy.RollingUpdate.MaxUnavailable != nil { 1562 o.RollingUpdate.MaxUnavailable = k8sspecs.IntOrStringToK8s(*strategy.RollingUpdate.MaxUnavailable) 1563 } 1564 } 1565 default: 1566 return o, errors.NotValidf("strategy type %q for deployment", strategyType) 1567 } 1568 return o, nil 1569 } 1570 1571 func (k *kubernetesClient) configureDeployment( 1572 appName, deploymentName string, 1573 annotations k8sannotations.Annotation, 1574 workloadSpec *workloadSpec, 1575 containers []specs.ContainerSpec, 1576 replicas *int32, 1577 filesystems []storage.KubernetesFilesystemParams, 1578 ) (cleanUps []func(), err error) { 1579 logger.Debugf("creating/updating deployment for %s", appName) 1580 1581 // Add the specified file to the pod spec. 1582 cfgName := func(fileSetName string) string { 1583 return applicationConfigMapName(deploymentName, fileSetName) 1584 } 1585 if err := k.configurePodFiles(appName, annotations, workloadSpec, containers, cfgName); err != nil { 1586 return cleanUps, errors.Trace(err) 1587 } 1588 1589 storageUniqueID, err := k.getStorageUniqPrefix(func() (annotationGetter, error) { 1590 return k.getDeployment(deploymentName) 1591 }) 1592 if err != nil { 1593 return cleanUps, errors.Trace(err) 1594 } 1595 1596 selectorLabels := utils.SelectorLabelsForApp(appName, k.IsLegacyLabels()) 1597 deployment := &apps.Deployment{ 1598 ObjectMeta: v1.ObjectMeta{ 1599 Name: deploymentName, 1600 Labels: utils.LabelsForApp(appName, k.IsLegacyLabels()), 1601 Annotations: k8sannotations.New(nil). 1602 Merge(annotations). 1603 Add(utils.AnnotationKeyApplicationUUID(k.IsLegacyLabels()), storageUniqueID).ToMap(), 1604 }, 1605 Spec: apps.DeploymentSpec{ 1606 // TODO(caas): MinReadySeconds, ProgressDeadlineSeconds support. 1607 Replicas: replicas, 1608 RevisionHistoryLimit: pointer.Int32Ptr(deploymentRevisionHistoryLimit), 1609 Selector: &v1.LabelSelector{ 1610 MatchLabels: selectorLabels, 1611 }, 1612 Template: core.PodTemplateSpec{ 1613 ObjectMeta: v1.ObjectMeta{ 1614 GenerateName: deploymentName + "-", 1615 Labels: utils.LabelsMerge(workloadSpec.Pod.Labels, selectorLabels), 1616 Annotations: podAnnotations(k8sannotations.New(workloadSpec.Pod.Annotations).Merge(annotations).Copy()).ToMap(), 1617 }, 1618 Spec: workloadSpec.Pod.PodSpec, 1619 }, 1620 }, 1621 } 1622 if workloadSpec.Service != nil && workloadSpec.Service.UpdateStrategy != nil { 1623 if deployment.Spec.Strategy, err = updateStrategyForDeployment(*workloadSpec.Service.UpdateStrategy); err != nil { 1624 return cleanUps, errors.Trace(err) 1625 } 1626 } 1627 handlePVC := func(pvc core.PersistentVolumeClaim, mountPath string, readOnly bool) error { 1628 cs, err := k.configurePVCForStatelessResource(pvc, mountPath, readOnly, &deployment.Spec.Template.Spec) 1629 cleanUps = append(cleanUps, cs...) 1630 return errors.Trace(err) 1631 } 1632 // Storage support for deployment is new. 1633 legacy := false 1634 if err := k.configureStorage(appName, legacy, storageUniqueID, filesystems, &deployment.Spec.Template.Spec, handlePVC); err != nil { 1635 return cleanUps, errors.Trace(err) 1636 } 1637 if err = k.ensureDeployment(deployment); err != nil { 1638 return cleanUps, errors.Trace(err) 1639 } 1640 cleanUps = append(cleanUps, func() { _ = k.deleteDeployment(appName) }) 1641 return cleanUps, nil 1642 } 1643 1644 func (k *kubernetesClient) configurePVCForStatelessResource( 1645 spec core.PersistentVolumeClaim, mountPath string, readOnly bool, podSpec *core.PodSpec, 1646 ) (cleanUps []func(), err error) { 1647 pvc, pvcCleanUp, err := k.ensurePVC(&spec) 1648 cleanUps = append(cleanUps, pvcCleanUp) 1649 if err != nil { 1650 return cleanUps, errors.Annotatef(err, "ensuring PVC %q", spec.GetName()) 1651 } 1652 vol := core.Volume{ 1653 Name: pvc.GetName(), 1654 VolumeSource: core.VolumeSource{ 1655 PersistentVolumeClaim: &core.PersistentVolumeClaimVolumeSource{ 1656 ClaimName: pvc.GetName(), 1657 ReadOnly: readOnly, 1658 }, 1659 }, 1660 } 1661 if err = k8sstorage.PushUniqueVolume(podSpec, vol, false); err != nil { 1662 return cleanUps, errors.Trace(err) 1663 } 1664 podSpec.Containers[0].VolumeMounts = append(podSpec.Containers[0].VolumeMounts, core.VolumeMount{ 1665 Name: vol.Name, 1666 MountPath: mountPath, 1667 }) 1668 return cleanUps, nil 1669 } 1670 1671 func (k *kubernetesClient) ensureDeployment(spec *apps.Deployment) error { 1672 if k.namespace == "" { 1673 return errNoNamespace 1674 } 1675 deployments := k.client().AppsV1().Deployments(k.namespace) 1676 _, err := k.createDeployment(spec) 1677 if err == nil || !errors.IsAlreadyExists(err) { 1678 return errors.Annotatef(err, "ensuring deployment %q", spec.GetName()) 1679 } 1680 existing, err := k.getDeployment(spec.GetName()) 1681 if err != nil { 1682 return errors.Trace(err) 1683 } 1684 existing.SetAnnotations(spec.GetAnnotations()) 1685 existing.Spec = spec.Spec 1686 _, err = deployments.Update(context.TODO(), existing, v1.UpdateOptions{}) 1687 if err != nil { 1688 return errors.Annotatef(err, "ensuring deployment %q", spec.GetName()) 1689 } 1690 return errors.Trace(err) 1691 } 1692 1693 func (k *kubernetesClient) createDeployment(spec *apps.Deployment) (*apps.Deployment, error) { 1694 out, err := k.client().AppsV1().Deployments(k.namespace).Create(context.TODO(), spec, v1.CreateOptions{}) 1695 if k8serrors.IsAlreadyExists(err) { 1696 return nil, errors.AlreadyExistsf("deployment %q", spec.GetName()) 1697 } 1698 if k8serrors.IsInvalid(err) { 1699 return nil, errors.NotValidf("deployment %q", spec.GetName()) 1700 } 1701 return out, errors.Trace(err) 1702 } 1703 1704 func (k *kubernetesClient) getDeployment(name string) (*apps.Deployment, error) { 1705 if k.namespace == "" { 1706 return nil, errNoNamespace 1707 } 1708 out, err := k.client().AppsV1().Deployments(k.namespace).Get(context.TODO(), name, v1.GetOptions{}) 1709 if k8serrors.IsNotFound(err) { 1710 return nil, errors.NotFoundf("deployment %q", name) 1711 } 1712 return out, errors.Trace(err) 1713 } 1714 1715 func (k *kubernetesClient) deleteDeployment(name string) error { 1716 if k.namespace == "" { 1717 return errNoNamespace 1718 } 1719 err := k.client().AppsV1().Deployments(k.namespace).Delete(context.TODO(), name, v1.DeleteOptions{ 1720 PropagationPolicy: constants.DefaultPropagationPolicy(), 1721 }) 1722 if k8serrors.IsNotFound(err) { 1723 return nil 1724 } 1725 return errors.Trace(err) 1726 } 1727 1728 func (k *kubernetesClient) deleteDeployments(appName string) error { 1729 err := k.client().AppsV1().Deployments(k.namespace).DeleteCollection(context.TODO(), v1.DeleteOptions{ 1730 PropagationPolicy: constants.DefaultPropagationPolicy(), 1731 }, v1.ListOptions{ 1732 LabelSelector: utils.LabelsToSelector( 1733 utils.LabelsForApp(appName, k.IsLegacyLabels())).String(), 1734 }) 1735 if k8serrors.IsNotFound(err) { 1736 return nil 1737 } 1738 return errors.Trace(err) 1739 } 1740 1741 func getPodManagementPolicy(svc *specs.ServiceSpec) (out apps.PodManagementPolicyType) { 1742 // Default to "Parallel". 1743 out = apps.ParallelPodManagement 1744 if svc == nil || svc.ScalePolicy == "" { 1745 return out 1746 } 1747 1748 switch svc.ScalePolicy { 1749 case specs.SerialScale: 1750 return apps.OrderedReadyPodManagement 1751 case specs.ParallelScale: 1752 return apps.ParallelPodManagement 1753 // no need to consider other cases because we have done validation in podspec parsing stage. 1754 } 1755 return out 1756 } 1757 1758 func (k *kubernetesClient) deleteVolumeClaims(appName string, p *core.Pod) ([]string, error) { 1759 if k.namespace == "" { 1760 return nil, errNoNamespace 1761 } 1762 volumesByName := make(map[string]core.Volume) 1763 for _, pv := range p.Spec.Volumes { 1764 volumesByName[pv.Name] = pv 1765 } 1766 1767 var deletedClaimVolumes []string 1768 for _, volMount := range p.Spec.Containers[0].VolumeMounts { 1769 vol, ok := volumesByName[volMount.Name] 1770 if !ok { 1771 logger.Warningf("volume for volume mount %q not found", volMount.Name) 1772 continue 1773 } 1774 if vol.PersistentVolumeClaim == nil { 1775 // Ignore volumes which are not Juju managed filesystems. 1776 continue 1777 } 1778 pvClaims := k.client().CoreV1().PersistentVolumeClaims(k.namespace) 1779 logger.Infof("deleting operator PVC %s for application %s due to call to kubernetesClient.deleteVolumeClaims", vol.PersistentVolumeClaim.ClaimName, appName) 1780 err := pvClaims.Delete(context.TODO(), vol.PersistentVolumeClaim.ClaimName, v1.DeleteOptions{ 1781 PropagationPolicy: constants.DefaultPropagationPolicy(), 1782 }) 1783 if err != nil && !k8serrors.IsNotFound(err) { 1784 return nil, errors.Annotatef(err, "deleting persistent volume claim %v for %v", 1785 vol.PersistentVolumeClaim.ClaimName, p.Name) 1786 } 1787 deletedClaimVolumes = append(deletedClaimVolumes, vol.Name) 1788 } 1789 return deletedClaimVolumes, nil 1790 } 1791 1792 // CaasServiceToK8s translates a caas service type to a k8s one. 1793 func CaasServiceToK8s(in caas.ServiceType) (core.ServiceType, error) { 1794 serviceType := defaultServiceType 1795 if in != "" { 1796 switch in { 1797 case caas.ServiceCluster: 1798 serviceType = core.ServiceTypeClusterIP 1799 case caas.ServiceLoadBalancer: 1800 serviceType = core.ServiceTypeLoadBalancer 1801 case caas.ServiceExternal: 1802 serviceType = core.ServiceTypeExternalName 1803 case caas.ServiceOmit: 1804 logger.Debugf("no service to be created because service type is %q", in) 1805 return "", nil 1806 default: 1807 return "", errors.NotSupportedf("service type %q", in) 1808 } 1809 } 1810 return serviceType, nil 1811 } 1812 1813 func (k *kubernetesClient) configureService( 1814 appName, deploymentName string, 1815 containerPorts []core.ContainerPort, 1816 params *caas.ServiceParams, 1817 config coreconfig.ConfigAttributes, 1818 ) error { 1819 logger.Debugf("creating/updating service for %s", appName) 1820 1821 var ports []core.ServicePort 1822 for i, cp := range containerPorts { 1823 // We normally expect a single container port for most use cases. 1824 // We allow the user to specify what first service port should be, 1825 // otherwise it just defaults to the container port. 1826 // TODO(caas) - consider allowing all service ports to be specified 1827 var targetPort intstr.IntOrString 1828 if i == 0 { 1829 targetPort = intstr.FromInt(config.GetInt(serviceTargetPortConfigKey, int(cp.ContainerPort))) 1830 } 1831 ports = append(ports, core.ServicePort{ 1832 Name: cp.Name, 1833 Protocol: cp.Protocol, 1834 Port: cp.ContainerPort, 1835 TargetPort: targetPort, 1836 }) 1837 } 1838 1839 serviceType := caas.ServiceType(config.GetString(ServiceTypeConfigKey, string(params.Deployment.ServiceType))) 1840 k8sServiceType, err := CaasServiceToK8s(serviceType) 1841 if err != nil { 1842 return errors.Trace(err) 1843 } 1844 annotations, err := config.GetStringMap(serviceAnnotationsKey, nil) 1845 if err != nil { 1846 return errors.Annotatef(err, "unexpected annotations: %#v", config.Get(serviceAnnotationsKey, nil)) 1847 } 1848 service := &core.Service{ 1849 ObjectMeta: v1.ObjectMeta{ 1850 Name: deploymentName, 1851 Labels: utils.LabelsForApp(appName, k.IsLegacyLabels()), 1852 Annotations: annotations, 1853 }, 1854 Spec: core.ServiceSpec{ 1855 Selector: utils.SelectorLabelsForApp(appName, k.IsLegacyLabels()), 1856 Type: k8sServiceType, 1857 Ports: ports, 1858 ExternalIPs: config.Get(serviceExternalIPsConfigKey, []string(nil)).([]string), 1859 LoadBalancerIP: config.GetString(serviceLoadBalancerIPKey, ""), 1860 LoadBalancerSourceRanges: config.Get(serviceLoadBalancerSourceRangesKey, []string(nil)).([]string), 1861 ExternalName: config.GetString(serviceExternalNameKey, ""), 1862 }, 1863 } 1864 _, err = k.ensureK8sService(service) 1865 return err 1866 } 1867 1868 func (k *kubernetesClient) configureHeadlessService( 1869 appName, deploymentName string, annotations k8sannotations.Annotation, 1870 ) error { 1871 logger.Debugf("creating/updating headless service for %s", appName) 1872 service := &core.Service{ 1873 ObjectMeta: v1.ObjectMeta{ 1874 Name: headlessServiceName(deploymentName), 1875 Labels: utils.LabelsForApp(appName, k.IsLegacyLabels()), 1876 Annotations: k8sannotations.New(nil). 1877 Merge(annotations). 1878 Add("service.alpha.kubernetes.io/tolerate-unready-endpoints", "true").ToMap(), 1879 }, 1880 Spec: core.ServiceSpec{ 1881 Selector: utils.SelectorLabelsForApp(appName, k.IsLegacyLabels()), 1882 Type: core.ServiceTypeClusterIP, 1883 ClusterIP: "None", 1884 PublishNotReadyAddresses: true, 1885 }, 1886 } 1887 _, err := k.ensureK8sService(service) 1888 return err 1889 } 1890 1891 func (k *kubernetesClient) findDefaultIngressClassResource() (*string, error) { 1892 ics, err := k.listIngressClasses(nil) 1893 if err != nil { 1894 return nil, errors.Annotate(err, "finding the default ingress class") 1895 } 1896 for _, ic := range ics { 1897 if k8sannotations.New(ic.GetAnnotations()).Has("ingressclass.kubernetes.io/is-default-class", "true") { 1898 return &ic.Name, nil 1899 } 1900 } 1901 return nil, errors.NotFoundf("default ingress class") 1902 } 1903 1904 // ExposeService sets up external access to the specified application. 1905 func (k *kubernetesClient) ExposeService(appName string, resourceTags map[string]string, config coreconfig.ConfigAttributes) error { 1906 if k.namespace == "" { 1907 return errNoNamespace 1908 } 1909 logger.Debugf("creating/updating ingress resource for %s", appName) 1910 1911 host := config.GetString(caas.JujuExternalHostNameKey, "") 1912 if host == "" { 1913 return errors.Errorf("external hostname required") 1914 } 1915 1916 ingressSSLRedirect := config.GetBool(ingressSSLRedirectKey, defaultIngressSSLRedirect) 1917 ingressSSLPassthrough := config.GetBool(ingressSSLPassthroughKey, defaultIngressSSLPassthrough) 1918 ingressAllowHTTP := config.GetBool(ingressAllowHTTPKey, defaultIngressAllowHTTPKey) 1919 httpPath := config.GetString(caas.JujuApplicationPath, caas.JujuDefaultApplicationPath) 1920 if httpPath == "$appname" { 1921 httpPath = appName 1922 } 1923 if !strings.HasPrefix(httpPath, "/") { 1924 httpPath = "/" + httpPath 1925 } 1926 1927 deploymentName := k.deploymentName(appName, true) 1928 svc, err := k.client().CoreV1().Services(k.namespace).Get(context.TODO(), deploymentName, v1.GetOptions{}) 1929 if err != nil { 1930 return errors.Trace(err) 1931 } 1932 if len(svc.Spec.Ports) == 0 { 1933 return errors.Errorf("cannot create ingress rule for service %q without a port", svc.Name) 1934 } 1935 spec := &networkingv1.Ingress{ 1936 ObjectMeta: v1.ObjectMeta{ 1937 Name: deploymentName, 1938 Labels: k8slabels.Merge(resourceTags, k.getIngressLabels(appName)), 1939 Annotations: map[string]string{ 1940 "ingress.kubernetes.io/rewrite-target": "", 1941 "ingress.kubernetes.io/ssl-redirect": strconv.FormatBool(ingressSSLRedirect), 1942 "kubernetes.io/ingress.allow-http": strconv.FormatBool(ingressAllowHTTP), 1943 "ingress.kubernetes.io/ssl-passthrough": strconv.FormatBool(ingressSSLPassthrough), 1944 }, 1945 }, 1946 Spec: networkingv1.IngressSpec{}, 1947 } 1948 1949 ingressClass := config.GetString(ingressClassKey, defaultIngressClass) 1950 if ingressClass == defaultIngressClass { 1951 if spec.Spec.IngressClassName, err = k.findDefaultIngressClassResource(); err != nil && !errors.IsNotFound(err) { 1952 return errors.Trace(err) 1953 } 1954 } 1955 pathType := networkingv1.PathTypeImplementationSpecific 1956 if spec.Spec.IngressClassName == nil { 1957 spec.Annotations["kubernetes.io/ingress.class"] = ingressClass 1958 pathType = networkingv1.PathTypePrefix 1959 } 1960 spec.Spec.Rules = append(spec.Spec.Rules, 1961 networkingv1.IngressRule{ 1962 Host: host, 1963 IngressRuleValue: networkingv1.IngressRuleValue{ 1964 HTTP: &networkingv1.HTTPIngressRuleValue{ 1965 Paths: []networkingv1.HTTPIngressPath{ 1966 { 1967 Path: httpPath, 1968 PathType: &pathType, 1969 Backend: networkingv1.IngressBackend{ 1970 Service: &networkingv1.IngressServiceBackend{ 1971 Name: svc.Name, 1972 Port: networkingv1.ServiceBackendPort{ 1973 Number: int32(svc.Spec.Ports[0].TargetPort.IntValue()), 1974 }, 1975 }, 1976 }, 1977 }, 1978 }, 1979 }, 1980 }, 1981 }, 1982 ) 1983 1984 // TODO(caas): refactor juju expose to solve potential conflict with ingress definition in podspec. 1985 // https://bugs.launchpad.net/juju/+bug/1854123 1986 _, err = k.ensureIngressV1(appName, spec, true) 1987 return errors.Trace(err) 1988 } 1989 1990 // UnexposeService removes external access to the specified service. 1991 func (k *kubernetesClient) UnexposeService(appName string) error { 1992 logger.Debugf("deleting ingress resource for %s", appName) 1993 deploymentName := k.deploymentName(appName, true) 1994 return errors.Trace(k.deleteIngress(deploymentName, "")) 1995 } 1996 1997 func (k *kubernetesClient) applicationSelector(appName string, mode caas.DeploymentMode) string { 1998 if mode == caas.ModeOperator { 1999 return operatorSelector(appName, k.IsLegacyLabels()) 2000 } 2001 return utils.LabelsToSelector( 2002 utils.SelectorLabelsForApp(appName, k.IsLegacyLabels())).String() 2003 } 2004 2005 // AnnotateUnit annotates the specified pod (name or uid) with a unit tag. 2006 func (k *kubernetesClient) AnnotateUnit(appName string, mode caas.DeploymentMode, podName string, unit names.UnitTag) error { 2007 if k.namespace == "" { 2008 return errNoNamespace 2009 } 2010 pods := k.client().CoreV1().Pods(k.namespace) 2011 2012 pod, err := pods.Get(context.TODO(), podName, v1.GetOptions{}) 2013 if err != nil { 2014 if !k8serrors.IsNotFound(err) { 2015 return errors.Trace(err) 2016 } 2017 pods, err := pods.List(context.TODO(), v1.ListOptions{ 2018 LabelSelector: k.applicationSelector(appName, mode), 2019 }) 2020 // TODO(caas): remove getting pod by Id (a bit expensive) once we started to store podName in cloudContainer doc. 2021 if err != nil { 2022 return errors.Trace(err) 2023 } 2024 for _, v := range pods.Items { 2025 if string(v.GetUID()) == podName { 2026 p := v 2027 pod = &p 2028 break 2029 } 2030 } 2031 } 2032 if pod == nil { 2033 return errors.NotFoundf("pod %q", podName) 2034 } 2035 2036 unitID := unit.Id() 2037 if pod.Annotations != nil && pod.Annotations[utils.AnnotationUnitKey(k.IsLegacyLabels())] == unitID { 2038 return nil 2039 } 2040 2041 patch := struct { 2042 ObjectMeta struct { 2043 Annotations map[string]string `json:"annotations"` 2044 } `json:"metadata"` 2045 }{} 2046 patch.ObjectMeta.Annotations = map[string]string{ 2047 utils.AnnotationUnitKey(k.IsLegacyLabels()): unitID, 2048 } 2049 jsonPatch, err := json.Marshal(patch) 2050 if err != nil { 2051 return errors.Trace(err) 2052 } 2053 2054 _, err = pods.Patch(context.TODO(), pod.Name, types.MergePatchType, jsonPatch, v1.PatchOptions{}) 2055 if k8serrors.IsNotFound(err) { 2056 return errors.NotFoundf("pod %q", podName) 2057 } 2058 return errors.Trace(err) 2059 } 2060 2061 // WatchUnits returns a watcher which notifies when there 2062 // are changes to units of the specified application. 2063 func (k *kubernetesClient) WatchUnits(appName string, mode caas.DeploymentMode) (watcher.NotifyWatcher, error) { 2064 selector := k.applicationSelector(appName, mode) 2065 logger.Debugf("selecting units %q to watch", selector) 2066 factory := informers.NewSharedInformerFactoryWithOptions(k.client(), 0, 2067 informers.WithNamespace(k.namespace), 2068 informers.WithTweakListOptions(func(o *v1.ListOptions) { 2069 o.LabelSelector = selector 2070 }), 2071 ) 2072 return k.newWatcher(factory.Core().V1().Pods().Informer(), appName, k.clock) 2073 } 2074 2075 // WatchContainerStart returns a watcher which is notified when a container matching containerName regexp 2076 // is starting/restarting. Each string represents the provider id for the unit the container belongs to. 2077 // If containerName regexp matches empty string, then the first workload container 2078 // is used. 2079 func (k *kubernetesClient) WatchContainerStart(appName string, containerName string) (watcher.StringsWatcher, error) { 2080 if k.namespace == "" { 2081 return nil, errNoNamespace 2082 } 2083 pods := k.client().CoreV1().Pods(k.namespace) 2084 selector := k.applicationSelector(appName, caas.ModeWorkload) 2085 logger.Debugf("selecting units %q to watch", selector) 2086 factory := informers.NewSharedInformerFactoryWithOptions(k.client(), 0, 2087 informers.WithNamespace(k.namespace), 2088 informers.WithTweakListOptions(func(o *v1.ListOptions) { 2089 o.LabelSelector = selector 2090 }), 2091 ) 2092 2093 podsList, err := pods.List(context.TODO(), v1.ListOptions{ 2094 LabelSelector: selector, 2095 }) 2096 if err != nil { 2097 return nil, errors.Trace(err) 2098 } 2099 2100 containerNameRegex, err := regexp.Compile("^" + containerName + "$") 2101 if err != nil { 2102 return nil, errors.Trace(err) 2103 } 2104 2105 running := func(pod *core.Pod) set.Strings { 2106 if _, ok := pod.Annotations[utils.AnnotationUnitKey(k.IsLegacyLabels())]; !ok { 2107 // Ignore pods that aren't annotated as a unit yet. 2108 return set.Strings{} 2109 } 2110 running := set.Strings{} 2111 for _, cs := range pod.Status.InitContainerStatuses { 2112 if containerNameRegex.MatchString(cs.Name) { 2113 if cs.State.Running != nil { 2114 running.Add(cs.Name) 2115 } 2116 } 2117 } 2118 for i, cs := range pod.Status.ContainerStatuses { 2119 useDefault := i == 0 && containerNameRegex.MatchString("") 2120 if containerNameRegex.MatchString(cs.Name) || useDefault { 2121 if cs.State.Running != nil { 2122 running.Add(cs.Name) 2123 } 2124 } 2125 } 2126 return running 2127 } 2128 2129 podInitState := map[string]set.Strings{} 2130 var initialEvents []string 2131 for _, pod := range podsList.Items { 2132 if containers := running(&pod); !containers.IsEmpty() { 2133 podInitState[string(pod.GetUID())] = containers 2134 initialEvents = append(initialEvents, providerID(&pod)) 2135 } 2136 } 2137 2138 filterEvent := func(evt k8swatcher.WatchEvent, obj interface{}) (string, bool) { 2139 pod, ok := obj.(*core.Pod) 2140 if !ok { 2141 return "", false 2142 } 2143 key := string(pod.GetUID()) 2144 if evt == k8swatcher.WatchEventDelete { 2145 delete(podInitState, key) 2146 return "", false 2147 } 2148 if containers := running(pod); !containers.IsEmpty() { 2149 if last, ok := podInitState[key]; ok { 2150 podInitState[key] = containers 2151 if !containers.Difference(last).IsEmpty() { 2152 return providerID(pod), true 2153 } 2154 } else { 2155 podInitState[key] = containers 2156 return providerID(pod), true 2157 } 2158 } else { 2159 delete(podInitState, key) 2160 } 2161 return "", false 2162 } 2163 2164 return k.newStringsWatcher(factory.Core().V1().Pods().Informer(), 2165 appName, k.clock, initialEvents, filterEvent) 2166 } 2167 2168 // WatchService returns a watcher which notifies when there 2169 // are changes to the deployment of the specified application. 2170 func (k *kubernetesClient) WatchService(appName string, mode caas.DeploymentMode) (watcher.NotifyWatcher, error) { 2171 if k.namespace == "" { 2172 return nil, errNoNamespace 2173 } 2174 // Application may be a statefulset or deployment. It may not have 2175 // been set up when the watcher is started so we don't know which it 2176 // is ahead of time. So use a multi-watcher to cover both cases. 2177 factory := informers.NewSharedInformerFactoryWithOptions(k.client(), 0, 2178 informers.WithNamespace(k.namespace), 2179 informers.WithTweakListOptions(func(o *v1.ListOptions) { 2180 o.LabelSelector = k.applicationSelector(appName, mode) 2181 }), 2182 ) 2183 2184 w1, err := k.newWatcher(factory.Apps().V1().StatefulSets().Informer(), appName, k.clock) 2185 if err != nil { 2186 return nil, errors.Trace(err) 2187 } 2188 w2, err := k.newWatcher(factory.Apps().V1().Deployments().Informer(), appName, k.clock) 2189 if err != nil { 2190 return nil, errors.Trace(err) 2191 } 2192 w3, err := k.newWatcher(factory.Core().V1().Services().Informer(), appName, k.clock) 2193 if err != nil { 2194 return nil, errors.Trace(err) 2195 } 2196 2197 return watcher.NewMultiNotifyWatcher(w1, w2, w3), nil 2198 } 2199 2200 // CheckCloudCredentials verifies the the cloud credentials provided to the 2201 // broker are functioning. 2202 func (k *kubernetesClient) CheckCloudCredentials() error { 2203 if _, err := k.Namespaces(); err != nil { 2204 // If this call could not be made with provided credential, we 2205 // know that the credential is invalid. 2206 return errors.Trace(err) 2207 } 2208 return nil 2209 } 2210 2211 // Units returns all units and any associated filesystems of the specified application. 2212 // Filesystems are mounted via volumes bound to the unit. 2213 func (k *kubernetesClient) Units(appName string, mode caas.DeploymentMode) ([]caas.Unit, error) { 2214 if k.namespace == "" { 2215 return nil, errNoNamespace 2216 } 2217 pods := k.client().CoreV1().Pods(k.namespace) 2218 podsList, err := pods.List(context.TODO(), v1.ListOptions{ 2219 LabelSelector: k.applicationSelector(appName, mode), 2220 }) 2221 if err != nil { 2222 return nil, errors.Trace(err) 2223 } 2224 2225 var units []caas.Unit 2226 now := k.clock.Now() 2227 for _, p := range podsList.Items { 2228 var ports []string 2229 for _, c := range p.Spec.Containers { 2230 for _, p := range c.Ports { 2231 ports = append(ports, fmt.Sprintf("%v/%v", p.ContainerPort, p.Protocol)) 2232 } 2233 } 2234 2235 eventGetter := func() ([]core.Event, error) { 2236 return k.getEvents(p.Name, "Pod") 2237 } 2238 2239 terminated := p.DeletionTimestamp != nil 2240 statusMessage, unitStatus, since, err := resources.PodToJujuStatus(p, now, eventGetter) 2241 2242 if err != nil { 2243 return nil, errors.Trace(err) 2244 } 2245 unitInfo := caas.Unit{ 2246 Id: providerID(&p), 2247 Address: p.Status.PodIP, 2248 Ports: ports, 2249 Dying: terminated, 2250 Stateful: isStateful(&p), 2251 Status: status.StatusInfo{ 2252 Status: unitStatus, 2253 Message: statusMessage, 2254 Since: &since, 2255 }, 2256 } 2257 2258 volumesByName := make(map[string]core.Volume) 2259 for _, pv := range p.Spec.Volumes { 2260 volumesByName[pv.Name] = pv 2261 } 2262 2263 // Gather info about how filesystems are attached/mounted to the pod. 2264 // The mount name represents the filesystem tag name used by Juju. 2265 for _, volMount := range p.Spec.Containers[0].VolumeMounts { 2266 vol, ok := volumesByName[volMount.Name] 2267 if !ok { 2268 logger.Warningf("volume for volume mount %q not found", volMount.Name) 2269 continue 2270 } 2271 var fsInfo *caas.FilesystemInfo 2272 if vol.PersistentVolumeClaim != nil && vol.PersistentVolumeClaim.ClaimName != "" { 2273 fsInfo, err = k.volumeInfoForPVC(vol, volMount, vol.PersistentVolumeClaim.ClaimName, now) 2274 } else if vol.EmptyDir != nil { 2275 fsInfo, err = k.volumeInfoForEmptyDir(vol, volMount, now) 2276 } else { 2277 // Ignore volumes which are not Juju managed filesystems. 2278 logger.Debugf("ignoring blank EmptyDir, PersistentVolumeClaim or ClaimName") 2279 continue 2280 } 2281 if err != nil { 2282 return nil, errors.Annotatef(err, "finding filesystem info for %v", volMount.Name) 2283 } 2284 if fsInfo == nil { 2285 continue 2286 } 2287 if fsInfo.StorageName == "" { 2288 if valid := constants.LegacyPVNameRegexp.MatchString(volMount.Name); valid { 2289 fsInfo.StorageName = constants.LegacyPVNameRegexp.ReplaceAllString(volMount.Name, "$storageName") 2290 } else if valid := constants.PVNameRegexp.MatchString(volMount.Name); valid { 2291 fsInfo.StorageName = constants.PVNameRegexp.ReplaceAllString(volMount.Name, "$storageName") 2292 } 2293 } 2294 logger.Debugf("filesystem info for %v: %+v", volMount.Name, *fsInfo) 2295 unitInfo.FilesystemInfo = append(unitInfo.FilesystemInfo, *fsInfo) 2296 } 2297 units = append(units, unitInfo) 2298 } 2299 return units, nil 2300 } 2301 2302 // ListPods filters a list of pods for the provided namespace and labels. 2303 func (k *kubernetesClient) ListPods(namespace string, selector k8slabels.Selector) ([]core.Pod, error) { 2304 listOps := v1.ListOptions{ 2305 LabelSelector: selector.String(), 2306 } 2307 list, err := k.client().CoreV1().Pods(namespace).List(context.TODO(), listOps) 2308 if err != nil { 2309 return nil, errors.Trace(err) 2310 } 2311 if len(list.Items) == 0 { 2312 return nil, errors.NotFoundf("pods with selector %q", selector) 2313 } 2314 return list.Items, nil 2315 } 2316 2317 func (k *kubernetesClient) getPod(podName string) (*core.Pod, error) { 2318 if k.namespace == "" { 2319 return nil, errNoNamespace 2320 } 2321 pods := k.client().CoreV1().Pods(k.namespace) 2322 pod, err := pods.Get(context.TODO(), podName, v1.GetOptions{}) 2323 if k8serrors.IsNotFound(err) { 2324 return nil, errors.NotFoundf("pod %q", podName) 2325 } else if err != nil { 2326 return nil, errors.Trace(err) 2327 } 2328 return pod, nil 2329 } 2330 2331 func (k *kubernetesClient) getStatefulSetStatus(ss *apps.StatefulSet) (string, status.Status, error) { 2332 terminated := ss.DeletionTimestamp != nil 2333 jujuStatus := status.Waiting 2334 if terminated { 2335 jujuStatus = status.Terminated 2336 } 2337 if ss.Status.ReadyReplicas == ss.Status.Replicas { 2338 jujuStatus = status.Active 2339 } 2340 return k.getStatusFromEvents(ss.Name, "StatefulSet", jujuStatus) 2341 } 2342 2343 func (k *kubernetesClient) getDeploymentStatus(deployment *apps.Deployment) (string, status.Status, error) { 2344 terminated := deployment.DeletionTimestamp != nil 2345 jujuStatus := status.Waiting 2346 if terminated { 2347 jujuStatus = status.Terminated 2348 } 2349 if deployment.Status.ReadyReplicas == deployment.Status.Replicas { 2350 jujuStatus = status.Active 2351 } 2352 return k.getStatusFromEvents(deployment.Name, "Deployment", jujuStatus) 2353 } 2354 2355 func (k *kubernetesClient) getDaemonSetStatus(ds *apps.DaemonSet) (string, status.Status, error) { 2356 terminated := ds.DeletionTimestamp != nil 2357 jujuStatus := status.Waiting 2358 if terminated { 2359 jujuStatus = status.Terminated 2360 } 2361 if ds.Status.NumberReady == ds.Status.DesiredNumberScheduled { 2362 jujuStatus = status.Active 2363 } 2364 return k.getStatusFromEvents(ds.Name, "DaemonSet", jujuStatus) 2365 } 2366 2367 func (k *kubernetesClient) getStatusFromEvents(name, kind string, jujuStatus status.Status) (string, status.Status, error) { 2368 events, err := k.getEvents(name, kind) 2369 if err != nil { 2370 return "", "", errors.Trace(err) 2371 } 2372 var statusMessage string 2373 // Take the most recent event. 2374 if count := len(events); count > 0 { 2375 evt := events[count-1] 2376 if jujuStatus == "" { 2377 if evt.Type == core.EventTypeWarning && evt.Reason == "FailedCreate" { 2378 jujuStatus = status.Error 2379 statusMessage = evt.Message 2380 } 2381 } 2382 } 2383 return statusMessage, jujuStatus, nil 2384 } 2385 2386 // filesetConfigMap returns a *core.ConfigMap for a pod 2387 // of the specified unit, with the specified files. 2388 func filesetConfigMap(configMapName string, labels, annotations map[string]string, files *specs.FileSet) *core.ConfigMap { 2389 result := &core.ConfigMap{ 2390 ObjectMeta: v1.ObjectMeta{ 2391 Name: configMapName, 2392 Labels: labels, 2393 Annotations: annotations, 2394 }, 2395 Data: map[string]string{}, 2396 } 2397 for _, f := range files.Files { 2398 result.Data[f.Path] = f.Content 2399 } 2400 return result 2401 } 2402 2403 // workloadSpec represents the k8s resources need to be created for the workload. 2404 type workloadSpec struct { 2405 Pod k8sspecs.PodSpecWithAnnotations 2406 Service *specs.ServiceSpec 2407 2408 Secrets []k8sspecs.K8sSecret 2409 Services []k8sspecs.K8sService 2410 ConfigMaps map[string]specs.ConfigMap 2411 ServiceAccounts []k8sspecs.K8sRBACSpecConverter 2412 CustomResourceDefinitions []k8sspecs.K8sCustomResourceDefinition 2413 CustomResources map[string][]unstructured.Unstructured 2414 MutatingWebhookConfigurations []k8sspecs.K8sMutatingWebhook 2415 ValidatingWebhookConfigurations []k8sspecs.K8sValidatingWebhook 2416 IngressResources []k8sspecs.K8sIngress 2417 } 2418 2419 func processContainers(deploymentName string, podSpec *specs.PodSpec, spec *core.PodSpec) error { 2420 2421 type containers struct { 2422 Containers []specs.ContainerSpec 2423 InitContainers []specs.ContainerSpec 2424 } 2425 2426 var cs containers 2427 for _, c := range podSpec.Containers { 2428 if c.Init { 2429 cs.InitContainers = append(cs.InitContainers, c) 2430 } else { 2431 cs.Containers = append(cs.Containers, c) 2432 } 2433 } 2434 2435 // Fill out the easy bits using a template. 2436 var buf bytes.Buffer 2437 if err := defaultPodTemplate.Execute(&buf, cs); err != nil { 2438 logger.Debugf("unable to execute template for containers: %+v, err: %+v", cs, err) 2439 return errors.Trace(err) 2440 } 2441 2442 workloadSpecString := buf.String() 2443 decoder := k8syaml.NewYAMLOrJSONDecoder(strings.NewReader(workloadSpecString), len(workloadSpecString)) 2444 if err := decoder.Decode(&spec); err != nil { 2445 logger.Debugf("unable to parse pod spec, unit spec: \n%v", workloadSpecString) 2446 return errors.Trace(err) 2447 } 2448 2449 // Now fill in the hard bits progamatically. 2450 if err := populateContainerDetails(deploymentName, spec, spec.Containers, cs.Containers); err != nil { 2451 return errors.Trace(err) 2452 } 2453 if err := populateContainerDetails(deploymentName, spec, spec.InitContainers, cs.InitContainers); err != nil { 2454 return errors.Trace(err) 2455 } 2456 return nil 2457 } 2458 2459 func prepareWorkloadSpec( 2460 appName, deploymentName string, podSpec *specs.PodSpec, imageDetails coreresources.DockerImageDetails, 2461 ) (*workloadSpec, error) { 2462 var spec workloadSpec 2463 if err := processContainers(deploymentName, podSpec, &spec.Pod.PodSpec); err != nil { 2464 logger.Errorf("unable to parse %q pod spec: \n%+v", appName, *podSpec) 2465 return nil, errors.Annotatef(err, "processing container specs for app %q", appName) 2466 } 2467 if err := ensureJujuInitContainer(&spec.Pod.PodSpec, imageDetails.RegistryPath); err != nil { 2468 return nil, errors.Annotatef(err, "adding init container for app %q", appName) 2469 } 2470 if imageDetails.IsPrivate() { 2471 spec.Pod.PodSpec.ImagePullSecrets = append( 2472 spec.Pod.PodSpec.ImagePullSecrets, 2473 core.LocalObjectReference{Name: constants.CAASImageRepoSecretName}, 2474 ) 2475 } 2476 2477 spec.Service = podSpec.Service 2478 spec.ConfigMaps = podSpec.ConfigMaps 2479 if podSpec.ServiceAccount != nil { 2480 // Use application name for the prime service account name. 2481 podSpec.ServiceAccount.SetName(appName) 2482 primeSA, err := k8sspecs.PrimeServiceAccountToK8sRBACResources(*podSpec.ServiceAccount) 2483 if err != nil { 2484 return nil, errors.Annotatef(err, "converting prime service account for app %q", appName) 2485 } 2486 spec.ServiceAccounts = append(spec.ServiceAccounts, primeSA) 2487 2488 spec.Pod.ServiceAccountName = podSpec.ServiceAccount.GetName() 2489 spec.Pod.AutomountServiceAccountToken = podSpec.ServiceAccount.AutomountServiceAccountToken 2490 } 2491 if podSpec.ProviderPod != nil { 2492 pSpec, ok := podSpec.ProviderPod.(*k8sspecs.K8sPodSpec) 2493 if !ok { 2494 return nil, errors.Errorf("unexpected kubernetes pod spec type %T", podSpec.ProviderPod) 2495 } 2496 2497 k8sResources := pSpec.KubernetesResources 2498 if k8sResources != nil { 2499 spec.Secrets = k8sResources.Secrets 2500 spec.Services = k8sResources.Services 2501 spec.CustomResourceDefinitions = k8sResources.CustomResourceDefinitions 2502 spec.CustomResources = k8sResources.CustomResources 2503 spec.MutatingWebhookConfigurations = k8sResources.MutatingWebhookConfigurations 2504 spec.ValidatingWebhookConfigurations = k8sResources.ValidatingWebhookConfigurations 2505 spec.IngressResources = k8sResources.IngressResources 2506 if k8sResources.Pod != nil { 2507 spec.Pod.Labels = utils.LabelsMerge(nil, k8sResources.Pod.Labels) 2508 spec.Pod.Annotations = k8sResources.Pod.Annotations.Copy() 2509 spec.Pod.RestartPolicy = k8sResources.Pod.RestartPolicy 2510 spec.Pod.ActiveDeadlineSeconds = k8sResources.Pod.ActiveDeadlineSeconds 2511 spec.Pod.TerminationGracePeriodSeconds = k8sResources.Pod.TerminationGracePeriodSeconds 2512 spec.Pod.SecurityContext = k8sResources.Pod.SecurityContext 2513 spec.Pod.ReadinessGates = k8sResources.Pod.ReadinessGates 2514 spec.Pod.DNSPolicy = k8sResources.Pod.DNSPolicy 2515 spec.Pod.HostNetwork = k8sResources.Pod.HostNetwork 2516 spec.Pod.HostPID = k8sResources.Pod.HostPID 2517 spec.Pod.PriorityClassName = k8sResources.Pod.PriorityClassName 2518 spec.Pod.Priority = k8sResources.Pod.Priority 2519 } 2520 spec.ServiceAccounts = append(spec.ServiceAccounts, &k8sResources.K8sRBACResources) 2521 } 2522 } 2523 return &spec, nil 2524 } 2525 2526 func boolPtr(b bool) *bool { 2527 return &b 2528 } 2529 2530 func defaultSecurityContext() *core.SecurityContext { 2531 // TODO(caas): consider locking this down more but charms will break 2532 return &core.SecurityContext{ 2533 AllowPrivilegeEscalation: boolPtr(true), // allow privilege for juju run and actions. 2534 ReadOnlyRootFilesystem: boolPtr(false), 2535 RunAsNonRoot: boolPtr(false), 2536 } 2537 } 2538 2539 func populateContainerDetails(deploymentName string, pod *core.PodSpec, podContainers []core.Container, containers []specs.ContainerSpec) (err error) { 2540 for i, c := range containers { 2541 pc := &podContainers[i] 2542 if c.Image != "" { 2543 logger.Warningf("Image parameter deprecated, use ImageDetails") 2544 pc.Image = c.Image 2545 } else { 2546 pc.Image = c.ImageDetails.ImagePath 2547 } 2548 if c.ImageDetails.Password != "" { 2549 pod.ImagePullSecrets = append(pod.ImagePullSecrets, core.LocalObjectReference{Name: appSecretName(deploymentName, c.Name)}) 2550 } 2551 if c.ImagePullPolicy != "" { 2552 pc.ImagePullPolicy = core.PullPolicy(c.ImagePullPolicy) 2553 } 2554 2555 if pc.Env, pc.EnvFrom, err = k8sspecs.ContainerConfigToK8sEnvConfig(c.EnvConfig); err != nil { 2556 return errors.Trace(err) 2557 } 2558 2559 pc.SecurityContext = defaultSecurityContext() 2560 if c.ProviderContainer == nil { 2561 continue 2562 } 2563 spec, ok := c.ProviderContainer.(*k8sspecs.K8sContainerSpec) 2564 if !ok { 2565 return errors.Errorf("unexpected kubernetes container spec type %T", c.ProviderContainer) 2566 } 2567 if spec.LivenessProbe != nil { 2568 pc.LivenessProbe = spec.LivenessProbe 2569 } 2570 if spec.ReadinessProbe != nil { 2571 pc.ReadinessProbe = spec.ReadinessProbe 2572 } 2573 if spec.StartupProbe != nil { 2574 pc.StartupProbe = spec.StartupProbe 2575 } 2576 if spec.SecurityContext != nil { 2577 pc.SecurityContext = spec.SecurityContext 2578 } 2579 } 2580 return nil 2581 } 2582 2583 // legacyAppName returns true if there are any artifacts for 2584 // appName which indicate that this deployment was for Juju 2.5.0. 2585 func (k *kubernetesClient) legacyAppName(appName string) bool { 2586 legacyName := "juju-operator-" + appName 2587 _, err := k.getStatefulSet(legacyName) 2588 return err == nil 2589 } 2590 2591 func (k *kubernetesClient) operatorName(appName string) string { 2592 if k.legacyAppName(appName) { 2593 return "juju-operator-" + appName 2594 } 2595 return appName + "-operator" 2596 } 2597 2598 func (k *kubernetesClient) deploymentName(appName string, legacySupport bool) string { 2599 if !legacySupport { 2600 // No need to check old operator statefulset for brand new features like raw k8s spec. 2601 return appName 2602 } 2603 if k.legacyAppName(appName) { 2604 return "juju-" + appName 2605 } 2606 return appName 2607 } 2608 2609 // SupportedFeatures implements environs.SupportedFeatureEnumerator. 2610 func (k *kubernetesClient) SupportedFeatures() (assumes.FeatureSet, error) { 2611 var fs assumes.FeatureSet 2612 2613 k8sAPIVersion, err := k.Version() 2614 if err != nil { 2615 return fs, errors.Annotatef(err, "querying kubernetes API version") 2616 } 2617 2618 fs.Add( 2619 assumes.Feature{ 2620 Name: "k8s-api", 2621 Description: assumes.UserFriendlyFeatureDescriptions["k8s-api"], 2622 Version: k8sAPIVersion, 2623 }, 2624 ) 2625 return fs, nil 2626 } 2627 2628 func isLegacyName(resourceName string) bool { 2629 return strings.HasPrefix(resourceName, "juju-") 2630 } 2631 2632 func operatorConfigMapName(operatorName string) string { 2633 return operatorName + "-config" 2634 } 2635 2636 func applicationConfigMapName(deploymentName, fileSetName string) string { 2637 return fmt.Sprintf("%v-%v-config", deploymentName, fileSetName) 2638 } 2639 2640 func appSecretName(deploymentName, containerName string) string { 2641 // A pod may have multiple containers with different images and thus different secrets 2642 return deploymentName + "-" + containerName + "-secret" 2643 } 2644 2645 func mergeDeviceConstraints(device devices.KubernetesDeviceParams, resources *core.ResourceRequirements) error { 2646 if resources.Limits == nil { 2647 resources.Limits = core.ResourceList{} 2648 } 2649 if resources.Requests == nil { 2650 resources.Requests = core.ResourceList{} 2651 } 2652 2653 resourceName := core.ResourceName(device.Type) 2654 if v, ok := resources.Limits[resourceName]; ok { 2655 return errors.NotValidf("resource limit for %q has already been set to %v! resource limit %q", resourceName, v, resourceName) 2656 } 2657 if v, ok := resources.Requests[resourceName]; ok { 2658 return errors.NotValidf("resource request for %q has already been set to %v! resource limit %q", resourceName, v, resourceName) 2659 } 2660 // GPU request/limit have to be set to same value equals to the Count. 2661 // - https://kubernetes.io/docs/tasks/manage-gpus/scheduling-gpus/#clusters-containing-different-types-of-nvidia-gpus 2662 resources.Limits[resourceName] = *resource.NewQuantity(device.Count, resource.DecimalSI) 2663 resources.Requests[resourceName] = *resource.NewQuantity(device.Count, resource.DecimalSI) 2664 return nil 2665 } 2666 2667 func buildNodeSelector(nodeLabel string) map[string]string { 2668 // TODO(caas): to support GKE, set it to `cloud.google.com/gke-accelerator`, 2669 // current only set to generic `accelerator` because we do not have k8s provider concept yet. 2670 key := "accelerator" 2671 return map[string]string{key: nodeLabel} 2672 } 2673 2674 func getNodeSelectorFromDeviceConstraints(devices []devices.KubernetesDeviceParams) (string, error) { 2675 var nodeSelector string 2676 for _, device := range devices { 2677 if device.Attributes == nil { 2678 continue 2679 } 2680 if label, ok := device.Attributes[gpuAffinityNodeSelectorKey]; ok { 2681 if nodeSelector != "" && nodeSelector != label { 2682 return "", errors.NotValidf( 2683 "node affinity labels have to be same for all device constraints in same pod - containers in same pod are scheduled in same node.") 2684 } 2685 nodeSelector = label 2686 } 2687 } 2688 return nodeSelector, nil 2689 } 2690 2691 func headlessServiceName(deploymentName string) string { 2692 return fmt.Sprintf("%s-endpoints", deploymentName) 2693 } 2694 2695 func providerID(pod *core.Pod) string { 2696 // Pods managed by a stateful set use the pod name 2697 // as the provider id as this is stable across pod restarts. 2698 if isStateful(pod) { 2699 return pod.Name 2700 } 2701 return string(pod.GetUID()) 2702 } 2703 2704 func isStateful(pod *core.Pod) bool { 2705 for _, ref := range pod.OwnerReferences { 2706 if ref.Kind == "StatefulSet" { 2707 return true 2708 } 2709 } 2710 return false 2711 }