github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/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 "fmt" 9 "path/filepath" 10 "regexp" 11 "strconv" 12 "strings" 13 "sync" 14 "text/template" 15 "time" 16 17 jujuclock "github.com/juju/clock" 18 "github.com/juju/errors" 19 "github.com/juju/loggo" 20 "github.com/juju/utils/arch" 21 "github.com/juju/utils/keyvalues" 22 "github.com/juju/utils/set" 23 "gopkg.in/juju/names.v2" 24 apps "k8s.io/api/apps/v1" 25 core "k8s.io/api/core/v1" 26 "k8s.io/api/extensions/v1beta1" 27 k8sstorage "k8s.io/api/storage/v1" 28 apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 29 apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" 30 k8serrors "k8s.io/apimachinery/pkg/api/errors" 31 "k8s.io/apimachinery/pkg/api/resource" 32 "k8s.io/apimachinery/pkg/apis/meta/v1" 33 "k8s.io/apimachinery/pkg/fields" 34 "k8s.io/apimachinery/pkg/util/intstr" 35 "k8s.io/apimachinery/pkg/util/yaml" 36 "k8s.io/apimachinery/pkg/watch" 37 "k8s.io/client-go/kubernetes" 38 "k8s.io/client-go/rest" 39 40 "github.com/juju/juju/agent" 41 "github.com/juju/juju/caas" 42 "github.com/juju/juju/cloudconfig/podcfg" 43 "github.com/juju/juju/core/application" 44 "github.com/juju/juju/core/devices" 45 "github.com/juju/juju/core/status" 46 "github.com/juju/juju/core/watcher" 47 "github.com/juju/juju/environs" 48 "github.com/juju/juju/environs/config" 49 "github.com/juju/juju/environs/context" 50 "github.com/juju/juju/juju/paths" 51 "github.com/juju/juju/network" 52 "github.com/juju/juju/storage" 53 ) 54 55 var logger = loggo.GetLogger("juju.kubernetes.provider") 56 57 const ( 58 labelOperator = "juju-operator" 59 labelStorage = "juju-storage" 60 labelVersion = "juju-version" 61 labelApplication = "juju-application" 62 labelModel = "juju-model" 63 64 defaultOperatorStorageClassName = "juju-operator-storage" 65 66 gpuAffinityNodeSelectorKey = "gpu" 67 ) 68 69 var defaultPropagationPolicy = v1.DeletePropagationForeground 70 71 type kubernetesClient struct { 72 clock jujuclock.Clock 73 kubernetes.Interface 74 apiextensionsClient apiextensionsclientset.Interface 75 76 // namespace is the k8s namespace to use when 77 // creating k8s resources. 78 namespace string 79 80 lock sync.Mutex 81 envCfg *config.Config 82 83 // modelUUID is the UUID of the model this client acts on. 84 modelUUID string 85 86 // newWatcher is the k8s watcher generator. 87 newWatcher NewK8sWatcherFunc 88 } 89 90 // To regenerate the mocks for the kubernetes Client used by this broker, 91 // run "go generate" from the package directory. 92 //go:generate mockgen -package mocks -destination mocks/k8sclient_mock.go k8s.io/client-go/kubernetes Interface 93 //go:generate mockgen -package mocks -destination mocks/appv1_mock.go k8s.io/client-go/kubernetes/typed/apps/v1 AppsV1Interface,DeploymentInterface,StatefulSetInterface 94 //go:generate mockgen -package mocks -destination mocks/corev1_mock.go k8s.io/client-go/kubernetes/typed/core/v1 CoreV1Interface,NamespaceInterface,PodInterface,ServiceInterface,ConfigMapInterface,PersistentVolumeInterface,PersistentVolumeClaimInterface,SecretInterface,NodeInterface 95 //go:generate mockgen -package mocks -destination mocks/extenstionsv1_mock.go k8s.io/client-go/kubernetes/typed/extensions/v1beta1 ExtensionsV1beta1Interface,IngressInterface 96 //go:generate mockgen -package mocks -destination mocks/storagev1_mock.go k8s.io/client-go/kubernetes/typed/storage/v1 StorageV1Interface,StorageClassInterface 97 98 // NewK8sClientFunc defines a function which returns a k8s client based on the supplied config. 99 type NewK8sClientFunc func(c *rest.Config) (kubernetes.Interface, apiextensionsclientset.Interface, error) 100 101 // NewK8sWatcherFunc defines a function which returns a k8s watcher based on the supplied config. 102 type NewK8sWatcherFunc func(wi watch.Interface, name string, clock jujuclock.Clock) (*kubernetesWatcher, error) 103 104 // NewK8sBroker returns a kubernetes client for the specified k8s cluster. 105 func NewK8sBroker( 106 k8sRestConfig *rest.Config, 107 cfg *config.Config, 108 newClient NewK8sClientFunc, 109 newWatcher NewK8sWatcherFunc, 110 clock jujuclock.Clock, 111 ) (caas.Broker, error) { 112 k8sClient, apiextensionsClient, err := newClient(k8sRestConfig) 113 if err != nil { 114 return nil, errors.Trace(err) 115 } 116 newCfg, err := providerInstance.newConfig(cfg) 117 if err != nil { 118 return nil, errors.Trace(err) 119 } 120 return &kubernetesClient{ 121 clock: clock, 122 Interface: k8sClient, 123 apiextensionsClient: apiextensionsClient, 124 namespace: newCfg.Name(), 125 envCfg: newCfg, 126 modelUUID: newCfg.UUID(), 127 newWatcher: newWatcher, 128 }, nil 129 } 130 131 // Config returns environ config. 132 func (k *kubernetesClient) Config() *config.Config { 133 k.lock.Lock() 134 defer k.lock.Unlock() 135 cfg := k.envCfg 136 return cfg 137 } 138 139 // SetConfig is specified in the Environ interface. 140 func (k *kubernetesClient) SetConfig(cfg *config.Config) error { 141 k.lock.Lock() 142 defer k.lock.Unlock() 143 newCfg, err := providerInstance.newConfig(cfg) 144 if err != nil { 145 return errors.Trace(err) 146 } 147 k.envCfg = newCfg 148 return nil 149 } 150 151 // PrepareForBootstrap prepares for bootstraping a controller. 152 func (k *kubernetesClient) PrepareForBootstrap(ctx environs.BootstrapContext) error { 153 return nil 154 } 155 156 const regionLabelName = "failure-domain.beta.kubernetes.io/region" 157 158 // ListHostCloudRegions lists all the cloud regions that this cluster has worker nodes/instances running in. 159 func (k *kubernetesClient) ListHostCloudRegions() (set.Strings, error) { 160 // we only check 5 worker nodes as of now just run in the one region and 161 // we are just looking for a running worker to sniff its region. 162 nodes, err := k.CoreV1().Nodes().List(v1.ListOptions{Limit: 5}) 163 if err != nil { 164 return nil, errors.Annotate(err, "listing nodes") 165 } 166 result := set.NewStrings() 167 for _, n := range nodes.Items { 168 var cloudRegion, v string 169 var ok bool 170 if v = getCloudProviderFromNodeMeta(n); v == "" { 171 continue 172 } 173 cloudRegion += v 174 if v, ok = n.Labels[regionLabelName]; !ok || v == "" { 175 continue 176 } 177 cloudRegion += "/" + v 178 result.Add(cloudRegion) 179 } 180 return result, nil 181 } 182 183 // Bootstrap deploys controller with mongoDB together into k8s cluster. 184 func (k *kubernetesClient) Bootstrap(ctx environs.BootstrapContext, callCtx context.ProviderCallContext, args environs.BootstrapParams) (*environs.BootstrapResult, error) { 185 const ( 186 // TODO(caas): how to get these from oci path. 187 Series = "bionic" 188 Arch = arch.AMD64 189 ) 190 191 finalizer := func(ctx environs.BootstrapContext, pcfg *podcfg.ControllerPodConfig, opts environs.BootstrapDialOpts) error { 192 envConfig := k.Config() 193 if err := podcfg.FinishControllerPodConfig(pcfg, envConfig); err != nil { 194 return errors.Trace(err) 195 } 196 197 if err := pcfg.VerifyConfig(); err != nil { 198 return errors.Trace(err) 199 } 200 201 // prepare bootstrapParamsFile 202 bootstrapParamsFileContent, err := pcfg.Bootstrap.StateInitializationParams.Marshal() 203 if err != nil { 204 return errors.Trace(err) 205 } 206 logger.Debugf("bootstrapParams file content: \n%s", string(bootstrapParamsFileContent)) 207 208 // TODO(caas): we'll need a different tag type other than machine tag. 209 machineTag := names.NewMachineTag(pcfg.MachineId) 210 acfg, err := pcfg.AgentConfig(machineTag) 211 if err != nil { 212 return errors.Trace(err) 213 } 214 agentConfigFileContent, err := acfg.Render() 215 if err != nil { 216 return errors.Trace(err) 217 } 218 logger.Debugf("agentConfig file content: \n%s", string(agentConfigFileContent)) 219 220 // TODO(caas): prepare 221 // agent.conf, 222 // bootstrap-params, 223 // server.pem, 224 // system-identity, 225 // shared-secret, then generate configmap/secret. 226 // Lastly, create StatefulSet for controller. 227 return nil 228 } 229 return &environs.BootstrapResult{ 230 Arch: Arch, 231 Series: Series, 232 CaasBootstrapFinalizer: finalizer, 233 }, nil 234 } 235 236 // DestroyController implements the Environ interface. 237 func (k *kubernetesClient) DestroyController(ctx context.ProviderCallContext, controllerUUID string) error { 238 // TODO(caas): destroy controller and all models 239 logger.Warningf("DestroyController is not supported yet on CAAS.") 240 return nil 241 } 242 243 // Provider is part of the Broker interface. 244 func (*kubernetesClient) Provider() caas.ContainerEnvironProvider { 245 return providerInstance 246 } 247 248 // Destroy is part of the Broker interface. 249 func (k *kubernetesClient) Destroy(callbacks context.ProviderCallContext) error { 250 watcher, err := k.WatchNamespace() 251 if err != nil { 252 return errors.Trace(err) 253 } 254 defer watcher.Kill() 255 256 if err := k.deleteNamespace(); err != nil { 257 return errors.Annotate(err, "deleting model namespace") 258 } 259 260 // Delete any storage classes created as part of this model. 261 // Storage classes live outside the namespace so need to be deleted separately. 262 modelSelector := fmt.Sprintf("%s==%s", labelModel, k.namespace) 263 err = k.StorageV1().StorageClasses().DeleteCollection(&v1.DeleteOptions{ 264 PropagationPolicy: &defaultPropagationPolicy, 265 }, v1.ListOptions{ 266 LabelSelector: modelSelector, 267 }) 268 if err != nil && !k8serrors.IsNotFound(err) { 269 return errors.Annotate(err, "deleting model storage classes") 270 } 271 for { 272 select { 273 case <-callbacks.Dying(): 274 return nil 275 case <-watcher.Changes(): 276 // ensure namespace has been deleted - notfound error expected. 277 _, err := k.GetNamespace("") 278 if errors.IsNotFound(err) { 279 // namespace ha been deleted. 280 return nil 281 } 282 if err != nil { 283 return errors.Trace(err) 284 } 285 logger.Debugf("namespace %q is still been terminating", k.namespace) 286 } 287 } 288 } 289 290 // Namespaces returns names of the namespaces on the cluster. 291 func (k *kubernetesClient) Namespaces() ([]string, error) { 292 namespaces := k.CoreV1().Namespaces() 293 ns, err := namespaces.List(v1.ListOptions{IncludeUninitialized: true}) 294 if err != nil { 295 return nil, errors.Annotate(err, "listing namespaces") 296 } 297 result := make([]string, len(ns.Items)) 298 for i, n := range ns.Items { 299 result[i] = n.Name 300 } 301 return result, nil 302 } 303 304 // GetNamespace returns the namespace for the specified name or current namespace. 305 func (k *kubernetesClient) GetNamespace(name string) (*core.Namespace, error) { 306 if name == "" { 307 name = k.namespace 308 } 309 ns, err := k.CoreV1().Namespaces().Get(name, v1.GetOptions{IncludeUninitialized: true}) 310 if k8serrors.IsNotFound(err) { 311 return nil, errors.NotFoundf("namespace %q", name) 312 } 313 if err != nil { 314 return nil, errors.Annotate(err, "getting namespaces") 315 } 316 return ns, nil 317 } 318 319 // EnsureNamespace ensures this broker's namespace is created. 320 func (k *kubernetesClient) EnsureNamespace() error { 321 ns := &core.Namespace{ObjectMeta: v1.ObjectMeta{Name: k.namespace}} 322 namespaces := k.CoreV1().Namespaces() 323 _, err := namespaces.Update(ns) 324 if k8serrors.IsNotFound(err) { 325 _, err = namespaces.Create(ns) 326 } 327 return errors.Trace(err) 328 } 329 330 func (k *kubernetesClient) deleteNamespace() error { 331 // deleteNamespace is used as a means to implement Destroy(). 332 // All model resources are provisioned in the namespace; 333 // deleting the namespace will also delete those resources. 334 err := k.CoreV1().Namespaces().Delete(k.namespace, &v1.DeleteOptions{ 335 PropagationPolicy: &defaultPropagationPolicy, 336 }) 337 if k8serrors.IsNotFound(err) { 338 return nil 339 } 340 return errors.Trace(err) 341 } 342 343 // WatchNamespace returns a watcher which notifies when there 344 // are changes to current namespace. 345 func (k *kubernetesClient) WatchNamespace() (watcher.NotifyWatcher, error) { 346 w, err := k.CoreV1().Namespaces().Watch( 347 v1.ListOptions{ 348 FieldSelector: fields.OneTermEqualSelector("metadata.name", k.namespace).String(), 349 IncludeUninitialized: true, 350 }, 351 ) 352 if err != nil { 353 return nil, errors.Trace(err) 354 } 355 return k.newWatcher(w, k.namespace, k.clock) 356 } 357 358 // EnsureSecret ensures a secret exists for use with retrieving images from private registries 359 func (k *kubernetesClient) ensureSecret(imageSecretName, appName string, imageDetails *caas.ImageDetails, resourceTags map[string]string) error { 360 if imageDetails.Password == "" { 361 return errors.New("attempting to create a secret with no password") 362 } 363 secretData, err := createDockerConfigJSON(imageDetails) 364 if err != nil { 365 return errors.Trace(err) 366 } 367 secrets := k.CoreV1().Secrets(k.namespace) 368 369 newSecret := &core.Secret{ 370 ObjectMeta: v1.ObjectMeta{ 371 Name: imageSecretName, 372 Namespace: k.namespace, 373 Labels: resourceTags}, 374 Type: core.SecretTypeDockerConfigJson, 375 Data: map[string][]byte{ 376 core.DockerConfigJsonKey: secretData, 377 }, 378 } 379 380 _, err = secrets.Update(newSecret) 381 if k8serrors.IsNotFound(err) { 382 _, err = secrets.Create(newSecret) 383 } 384 return errors.Trace(err) 385 } 386 387 func (k *kubernetesClient) deleteSecret(imageSecretName string) error { 388 secrets := k.CoreV1().Secrets(k.namespace) 389 err := secrets.Delete(imageSecretName, &v1.DeleteOptions{ 390 PropagationPolicy: &defaultPropagationPolicy, 391 }) 392 if k8serrors.IsNotFound(err) { 393 return nil 394 } 395 return errors.Trace(err) 396 } 397 398 // OperatorExists returns true if the operator for the specified 399 // application exists. 400 func (k *kubernetesClient) OperatorExists(appName string) (bool, error) { 401 statefulsets := k.AppsV1().StatefulSets(k.namespace) 402 _, err := statefulsets.Get(k.operatorName(appName), v1.GetOptions{IncludeUninitialized: true}) 403 if k8serrors.IsNotFound(err) { 404 return false, nil 405 } 406 if err != nil { 407 return false, errors.Trace(err) 408 } 409 return true, nil 410 } 411 412 // EnsureOperator creates or updates an operator pod with the given application 413 // name, agent path, and operator config. 414 func (k *kubernetesClient) EnsureOperator(appName, agentPath string, config *caas.OperatorConfig) error { 415 logger.Debugf("creating/updating %s operator", appName) 416 417 // TODO(caas) - this is a stop gap until we implement a CAAS model manager worker 418 // First up, ensure the namespace eis there if not already created. 419 if err := k.EnsureNamespace(); err != nil { 420 return errors.Annotatef(err, "ensuring operator namespace %v", k.namespace) 421 } 422 423 operatorName := k.operatorName(appName) 424 // TODO(caas) use secrets for storing agent password? 425 if config.AgentConf == nil { 426 // We expect that the config map already exists, 427 // so make sure it does. 428 configMaps := k.CoreV1().ConfigMaps(k.namespace) 429 _, err := configMaps.Get(operatorConfigMapName(operatorName), v1.GetOptions{IncludeUninitialized: true}) 430 if err != nil { 431 return errors.Annotatef(err, "config map for %q should already exist", appName) 432 } 433 } else { 434 if err := k.ensureConfigMap(operatorConfigMap(appName, operatorName, config)); err != nil { 435 return errors.Annotate(err, "creating or updating ConfigMap") 436 } 437 } 438 439 storageTags := make(map[string]string) 440 for k, v := range config.CharmStorage.ResourceTags { 441 storageTags[k] = v 442 } 443 storageTags[labelOperator] = appName 444 445 tags := make(map[string]string) 446 for k, v := range config.ResourceTags { 447 tags[k] = v 448 } 449 tags[labelOperator] = appName 450 451 // Set up the parameters for creating charm storage. 452 operatorVolumeClaim := "charm" 453 if isLegacyName(operatorName) { 454 operatorVolumeClaim = fmt.Sprintf("%v-operator-volume", appName) 455 } 456 457 params := volumeParams{ 458 storageConfig: &storageConfig{existingStorageClass: defaultOperatorStorageClassName}, 459 storageLabels: caas.OperatorStorageClassLabels(appName, k.namespace), 460 pvcName: operatorVolumeClaim, 461 requestedVolumeSize: fmt.Sprintf("%dMi", config.CharmStorage.Size), 462 } 463 if config.CharmStorage.Provider != K8s_ProviderType { 464 return errors.Errorf("expected charm storage provider %q, got %q", K8s_ProviderType, config.CharmStorage.Provider) 465 } 466 if storageLabel, ok := config.CharmStorage.Attributes[storageLabel]; ok { 467 params.storageLabels = append([]string{fmt.Sprintf("%v", storageLabel)}, params.storageLabels...) 468 } 469 var err error 470 params.storageConfig, err = newStorageConfig(config.CharmStorage.Attributes, defaultOperatorStorageClassName) 471 if err != nil { 472 return errors.Annotatef(err, "invalid storage configuration for %v operator", appName) 473 } 474 // We want operator storage to be deleted when the operator goes away. 475 params.storageConfig.reclaimPolicy = core.PersistentVolumeReclaimDelete 476 logger.Debugf("operator storage config %#v", *params.storageConfig) 477 478 // Attempt to get a persistent volume to store charm state etc. 479 pvcSpec, err := k.maybeGetVolumeClaimSpec(params) 480 if err != nil { 481 return errors.Annotate(err, "finding operator volume claim") 482 } 483 pvc := &core.PersistentVolumeClaim{ 484 ObjectMeta: v1.ObjectMeta{ 485 Name: params.pvcName, 486 Labels: storageTags}, 487 Spec: *pvcSpec, 488 } 489 pod := operatorPod(operatorName, appName, agentPath, config.OperatorImagePath, config.Version.String(), tags) 490 // Take a copy for use with statefulset. 491 podWithoutStorage := pod 492 493 numPods := int32(1) 494 logger.Debugf("using persistent volume claim for operator %s: %+v", appName, pvc) 495 statefulset := &apps.StatefulSet{ 496 ObjectMeta: v1.ObjectMeta{ 497 Name: operatorName, 498 Labels: pod.Labels}, 499 Spec: apps.StatefulSetSpec{ 500 Replicas: &numPods, 501 Selector: &v1.LabelSelector{ 502 MatchLabels: map[string]string{labelOperator: appName}, 503 }, 504 Template: core.PodTemplateSpec{ 505 ObjectMeta: v1.ObjectMeta{ 506 Labels: pod.Labels, 507 }, 508 }, 509 PodManagementPolicy: apps.ParallelPodManagement, 510 VolumeClaimTemplates: []core.PersistentVolumeClaim{*pvc}, 511 }, 512 } 513 pod.Spec.Containers[0].VolumeMounts = append(pod.Spec.Containers[0].VolumeMounts, core.VolumeMount{ 514 Name: pvc.Name, 515 MountPath: agent.BaseDir(agentPath), 516 }) 517 518 statefulset.Spec.Template.Spec = pod.Spec 519 err = k.ensureStatefulSet(statefulset, podWithoutStorage.Spec) 520 return errors.Annotatef(err, "creating or updating %v operator StatefulSet", appName) 521 } 522 523 func (k *kubernetesClient) GetStorageClassName(labels ...string) (string, error) { 524 sc, err := k.maybeGetStorageClass(labels...) 525 if err != nil { 526 return "", errors.Trace(err) 527 } 528 return sc.Name, nil 529 } 530 531 // maybeGetStorageClass looks for a storage class to use when creating 532 // a persistent volume, using the specified name (if supplied), or a class 533 // matching the specified labels. 534 func (k *kubernetesClient) maybeGetStorageClass(labels ...string) (*k8sstorage.StorageClass, error) { 535 // First try looking for a storage class with a Juju label. 536 selector := fmt.Sprintf("%v in (%v)", labelStorage, strings.Join(labels, ", ")) 537 modelTerm := fmt.Sprintf("%s==%s", labelModel, k.namespace) 538 modelSelector := selector + "," + modelTerm 539 540 // Attempt to get a storage class tied to this model. 541 storageClasses, err := k.StorageV1().StorageClasses().List(v1.ListOptions{ 542 LabelSelector: modelSelector, 543 }) 544 if err != nil { 545 return nil, errors.Annotatef(err, "looking for existing storage class with selector %q", modelSelector) 546 } 547 548 // If no storage classes tied to this model, look for a non-model specific 549 // storage class with the relevant labels. 550 if len(storageClasses.Items) == 0 { 551 storageClasses, err = k.StorageV1().StorageClasses().List(v1.ListOptions{ 552 LabelSelector: selector, 553 }) 554 if err != nil { 555 return nil, errors.Annotatef(err, "looking for existing storage class with selector %q", modelSelector) 556 } 557 } 558 logger.Debugf("available storage classes: %v", storageClasses.Items) 559 // For now, pick the first matching storage class. 560 if len(storageClasses.Items) > 0 { 561 return &storageClasses.Items[0], nil 562 } 563 564 // Second look for the cluster default storage class, if defined. 565 storageClasses, err = k.StorageV1().StorageClasses().List(v1.ListOptions{}) 566 if err != nil { 567 return nil, errors.Annotate(err, "listing storage classes") 568 } 569 for _, sc := range storageClasses.Items { 570 if v, ok := sc.Annotations["storageclass.kubernetes.io/is-default-class"]; ok && v != "false" { 571 logger.Debugf("using default storage class: %v", sc.Name) 572 return &sc, nil 573 } 574 } 575 return nil, errors.NotFoundf("storage class for any %q", labels) 576 } 577 578 type volumeParams struct { 579 storageLabels []string 580 storageConfig *storageConfig 581 pvcName string 582 requestedVolumeSize string 583 accessMode core.PersistentVolumeAccessMode 584 } 585 586 // maybeGetVolumeClaimSpec returns a persistent volume claim spec for the given 587 // parameters. If no suitable storage class is available, return a NotFound error. 588 func (k *kubernetesClient) maybeGetVolumeClaimSpec(params volumeParams) (*core.PersistentVolumeClaimSpec, error) { 589 storageClassName := params.storageConfig.storageClass 590 existingStorageClassName := params.storageConfig.existingStorageClass 591 haveStorageClass := false 592 // If no specific storage class has been specified but there's a default 593 // fallback one, try and look for that first. 594 if storageClassName == "" && existingStorageClassName != "" { 595 sc, err := k.getStorageClass(existingStorageClassName) 596 if err != nil && !k8serrors.IsNotFound(err) { 597 return nil, errors.Annotatef(err, "looking for existing storage class %q", existingStorageClassName) 598 } 599 if err == nil { 600 haveStorageClass = true 601 storageClassName = sc.Name 602 } 603 } 604 // If no storage class has been found or asked for, 605 // look for one by matching labels. 606 if storageClassName == "" && !haveStorageClass { 607 sc, err := k.maybeGetStorageClass(params.storageLabels...) 608 if err != nil && !errors.IsNotFound(err) { 609 return nil, errors.Trace(err) 610 } 611 if err == nil { 612 haveStorageClass = true 613 storageClassName = sc.Name 614 } 615 } 616 // If a specific storage class has been requested, make sure it exists. 617 if storageClassName != "" && !haveStorageClass { 618 params.storageConfig.storageClass = storageClassName 619 sc, err := k.ensureStorageClass(params.storageConfig) 620 if err != nil && !errors.IsNotFound(err) { 621 return nil, errors.Trace(err) 622 } 623 if err == nil { 624 haveStorageClass = true 625 storageClassName = sc.Name 626 } 627 } 628 if !haveStorageClass { 629 return nil, errors.NewNotFound(nil, fmt.Sprintf( 630 "cannot create persistent volume as no storage class matching %q exists and no default storage class is defined", 631 params.storageLabels)) 632 } 633 accessMode := params.accessMode 634 if accessMode == "" { 635 accessMode = core.ReadWriteOnce 636 } 637 fsSize, err := resource.ParseQuantity(params.requestedVolumeSize) 638 if err != nil { 639 return nil, errors.Annotatef(err, "invalid volume size %v", params.requestedVolumeSize) 640 } 641 return &core.PersistentVolumeClaimSpec{ 642 StorageClassName: &storageClassName, 643 Resources: core.ResourceRequirements{ 644 Requests: core.ResourceList{ 645 core.ResourceStorage: fsSize, 646 }, 647 }, 648 AccessModes: []core.PersistentVolumeAccessMode{accessMode}, 649 }, nil 650 } 651 652 // getStorageClass returns a named storage class, first looking for 653 // one which is qualified by the current namespace if it's available. 654 func (k *kubernetesClient) getStorageClass(name string) (*k8sstorage.StorageClass, error) { 655 storageClasses := k.StorageV1().StorageClasses() 656 qualifiedName := qualifiedStorageClassName(k.namespace, name) 657 sc, err := storageClasses.Get(qualifiedName, v1.GetOptions{}) 658 if err == nil { 659 return sc, nil 660 } 661 if !k8serrors.IsNotFound(err) { 662 return nil, errors.Trace(err) 663 } 664 return storageClasses.Get(name, v1.GetOptions{}) 665 } 666 667 func (k *kubernetesClient) ensureStorageClass(cfg *storageConfig) (*k8sstorage.StorageClass, error) { 668 // First see if the named storage class exists. 669 sc, err := k.getStorageClass(cfg.storageClass) 670 if err == nil { 671 return sc, nil 672 } 673 if !k8serrors.IsNotFound(err) { 674 return nil, errors.Annotatef(err, "getting storage class %q", cfg.storageClass) 675 } 676 // If it's not found but there's no provisioner specified, we can't 677 // create it so just return not found. 678 if err != nil && cfg.storageProvisioner == "" { 679 return nil, errors.NewNotFound(nil, 680 fmt.Sprintf("storage class %q doesn't exist, but no storage provisioner has been specified", 681 cfg.storageClass)) 682 } 683 684 // Create the storage class with the specified provisioner. 685 storageClasses := k.StorageV1().StorageClasses() 686 sc, err = storageClasses.Create(&k8sstorage.StorageClass{ 687 ObjectMeta: v1.ObjectMeta{ 688 Name: qualifiedStorageClassName(k.namespace, cfg.storageClass), 689 Labels: map[string]string{labelModel: k.namespace}, 690 }, 691 Provisioner: cfg.storageProvisioner, 692 ReclaimPolicy: &cfg.reclaimPolicy, 693 Parameters: cfg.parameters, 694 }) 695 return sc, errors.Annotatef(err, "creating storage class %q", cfg.storageClass) 696 } 697 698 // DeleteOperator deletes the specified operator. 699 func (k *kubernetesClient) DeleteOperator(appName string) (err error) { 700 logger.Debugf("deleting %s operator", appName) 701 702 operatorName := k.operatorName(appName) 703 legacy := isLegacyName(operatorName) 704 705 // First delete the config map(s). 706 configMaps := k.CoreV1().ConfigMaps(k.namespace) 707 configMapName := operatorConfigMapName(operatorName) 708 err = configMaps.Delete(configMapName, &v1.DeleteOptions{ 709 PropagationPolicy: &defaultPropagationPolicy, 710 }) 711 if err != nil && !k8serrors.IsNotFound(err) { 712 return nil 713 } 714 715 // Delete artefacts created by k8s itself. 716 configMapName = appName + "-configurations-config" 717 if legacy { 718 configMapName = "juju-" + configMapName 719 } 720 err = configMaps.Delete(configMapName, &v1.DeleteOptions{ 721 PropagationPolicy: &defaultPropagationPolicy, 722 }) 723 if err != nil && !k8serrors.IsNotFound(err) { 724 return nil 725 } 726 727 // Finally the operator itself. 728 if err := k.deleteStatefulSet(operatorName); err != nil { 729 return errors.Trace(err) 730 } 731 pods := k.CoreV1().Pods(k.namespace) 732 podsList, err := pods.List(v1.ListOptions{ 733 LabelSelector: operatorSelector(appName), 734 }) 735 if err != nil { 736 return errors.Trace(err) 737 } 738 739 deploymentName := appName 740 if legacy { 741 deploymentName = "juju-" + appName 742 } 743 pvs := k.CoreV1().PersistentVolumes() 744 for _, p := range podsList.Items { 745 // Delete secrets. 746 for _, c := range p.Spec.Containers { 747 secretName := appSecretName(deploymentName, c.Name) 748 if err := k.deleteSecret(secretName); err != nil { 749 return errors.Annotatef(err, "deleting %s secret for container %s", appName, c.Name) 750 } 751 } 752 // Delete operator storage volumes. 753 volumeNames, err := k.deleteVolumeClaims(appName, &p) 754 if err != nil { 755 return errors.Trace(err) 756 } 757 // Just in case the volume reclaim policy is retain, we force deletion 758 // for operators as the volume is an inseparable part of the operator. 759 for _, volName := range volumeNames { 760 err = pvs.Delete(volName, &v1.DeleteOptions{ 761 PropagationPolicy: &defaultPropagationPolicy, 762 }) 763 if err != nil && !k8serrors.IsNotFound(err) { 764 return errors.Annotatef(err, "deleting operator persistent volume %v for %v", 765 volName, appName) 766 } 767 } 768 } 769 return errors.Trace(k.deleteDeployment(operatorName)) 770 } 771 772 // Service returns the service for the specified application. 773 func (k *kubernetesClient) Service(appName string) (*caas.Service, error) { 774 services := k.CoreV1().Services(k.namespace) 775 servicesList, err := services.List(v1.ListOptions{ 776 LabelSelector: applicationSelector(appName), 777 }) 778 if err != nil { 779 return nil, errors.Trace(err) 780 } 781 if len(servicesList.Items) == 0 { 782 return nil, errors.NotFoundf("service for %q", appName) 783 } 784 service := servicesList.Items[0] 785 result := caas.Service{ 786 Id: string(service.UID), 787 } 788 if service.Spec.ClusterIP != "" { 789 result.Addresses = append(result.Addresses, network.Address{ 790 Value: service.Spec.ClusterIP, 791 Type: network.DeriveAddressType(service.Spec.ClusterIP), 792 Scope: network.ScopeCloudLocal, 793 }) 794 } 795 if service.Spec.LoadBalancerIP != "" { 796 result.Addresses = append(result.Addresses, network.Address{ 797 Value: service.Spec.LoadBalancerIP, 798 Type: network.DeriveAddressType(service.Spec.LoadBalancerIP), 799 Scope: network.ScopePublic, 800 }) 801 } 802 for _, addr := range service.Spec.ExternalIPs { 803 result.Addresses = append(result.Addresses, network.Address{ 804 Value: addr, 805 Type: network.DeriveAddressType(addr), 806 Scope: network.ScopePublic, 807 }) 808 } 809 return &result, nil 810 } 811 812 // DeleteService deletes the specified service. 813 func (k *kubernetesClient) DeleteService(appName string) (err error) { 814 logger.Debugf("deleting application %s", appName) 815 816 deploymentName := k.deploymentName(appName) 817 if err := k.deleteService(deploymentName); err != nil { 818 return errors.Trace(err) 819 } 820 if err := k.deleteStatefulSet(deploymentName); err != nil { 821 return errors.Trace(err) 822 } 823 if err := k.deleteDeployment(deploymentName); err != nil { 824 return errors.Trace(err) 825 } 826 pods := k.CoreV1().Pods(k.namespace) 827 podsList, err := pods.List(v1.ListOptions{ 828 LabelSelector: applicationSelector(appName), 829 }) 830 if err != nil { 831 return errors.Trace(err) 832 } 833 for _, p := range podsList.Items { 834 if _, err := k.deleteVolumeClaims(appName, &p); err != nil { 835 return errors.Trace(err) 836 } 837 } 838 secrets := k.CoreV1().Secrets(k.namespace) 839 secretList, err := secrets.List(v1.ListOptions{ 840 LabelSelector: applicationSelector(appName), 841 }) 842 if err != nil { 843 return errors.Trace(err) 844 } 845 for _, s := range secretList.Items { 846 if err := k.deleteSecret(s.Name); err != nil { 847 return errors.Trace(err) 848 } 849 } 850 return nil 851 } 852 853 // EnsureCustomResourceDefinition creates or updates a custom resource definition resource. 854 func (k *kubernetesClient) EnsureCustomResourceDefinition(appName string, podSpec *caas.PodSpec) error { 855 for _, t := range podSpec.CustomResourceDefinitions { 856 crd, err := k.ensureCustomResourceDefinitionTemplate(&t) 857 if err != nil { 858 return errors.Annotate(err, fmt.Sprintf("ensure custom resource definition %q", t.Kind)) 859 } 860 logger.Debugf("ensured custom resource definition %q", crd.ObjectMeta.Name) 861 } 862 return nil 863 } 864 865 func (k *kubernetesClient) ensureCustomResourceDefinitionTemplate(t *caas.CustomResourceDefinition) ( 866 crd *apiextensionsv1beta1.CustomResourceDefinition, err error) { 867 singularName := strings.ToLower(t.Kind) 868 pluralName := fmt.Sprintf("%ss", singularName) 869 crdFullName := fmt.Sprintf("%s.%s", pluralName, t.Group) 870 crdIn := &apiextensionsv1beta1.CustomResourceDefinition{ 871 ObjectMeta: v1.ObjectMeta{ 872 Name: crdFullName, 873 Namespace: k.namespace, 874 }, 875 Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{ 876 Group: t.Group, 877 Version: t.Version, 878 Scope: apiextensionsv1beta1.ResourceScope(t.Scope), 879 Names: apiextensionsv1beta1.CustomResourceDefinitionNames{ 880 Plural: pluralName, 881 Kind: t.Kind, 882 Singular: singularName, 883 }, 884 Validation: &apiextensionsv1beta1.CustomResourceValidation{ 885 OpenAPIV3Schema: &apiextensionsv1beta1.JSONSchemaProps{ 886 Properties: t.Validation.Properties, 887 }, 888 }, 889 }, 890 } 891 apiextensionsV1beta1 := k.apiextensionsClient.ApiextensionsV1beta1() 892 logger.Debugf("creating crd %#v", crdIn) 893 crd, err = apiextensionsV1beta1.CustomResourceDefinitions().Create(crdIn) 894 if k8serrors.IsAlreadyExists(err) { 895 crd, err = apiextensionsV1beta1.CustomResourceDefinitions().Get(crdFullName, v1.GetOptions{}) 896 resourceVersion := crd.ObjectMeta.GetResourceVersion() 897 crdIn.ObjectMeta.SetResourceVersion(resourceVersion) 898 logger.Debugf("existing crd with resource version %q found, so update it %#v", resourceVersion, crdIn) 899 crd, err = apiextensionsV1beta1.CustomResourceDefinitions().Update(crdIn) 900 } 901 return 902 } 903 904 // EnsureService creates or updates a service for pods with the given params. 905 func (k *kubernetesClient) EnsureService( 906 appName string, statusCallback caas.StatusCallbackFunc, params *caas.ServiceParams, numUnits int, config application.ConfigAttributes, 907 ) (err error) { 908 defer func() { 909 if err != nil { 910 statusCallback(appName, status.Error, err.Error(), nil) 911 } 912 }() 913 914 logger.Debugf("creating/updating application %s", appName) 915 deploymentName := k.deploymentName(appName) 916 917 if numUnits < 0 { 918 return errors.Errorf("number of units must be >= 0") 919 } 920 if numUnits == 0 { 921 return k.deleteAllPods(appName, deploymentName) 922 } 923 if params == nil || params.PodSpec == nil { 924 return errors.Errorf("missing pod spec") 925 } 926 if params.PodSpec.OmitServiceFrontend && len(params.Filesystems) == 0 { 927 return errors.Errorf("kubernetes service is required when using storage") 928 } 929 930 var cleanups []func() 931 defer func() { 932 if err == nil { 933 return 934 } 935 for _, f := range cleanups { 936 f() 937 } 938 }() 939 940 unitSpec, err := makeUnitSpec(appName, deploymentName, params.PodSpec) 941 if err != nil { 942 return errors.Annotatef(err, "parsing unit spec for %s", appName) 943 } 944 if len(params.Devices) > 0 { 945 if err = k.configureDevices(unitSpec, params.Devices); err != nil { 946 return errors.Annotatef(err, "configuring devices for %s", appName) 947 } 948 } 949 if mem := params.Constraints.Mem; mem != nil { 950 if err = k.configureConstraint(unitSpec, "memory", fmt.Sprintf("%dMi", *mem)); err != nil { 951 return errors.Annotatef(err, "configuring memory constraint for %s", appName) 952 } 953 } 954 if cpu := params.Constraints.CpuPower; cpu != nil { 955 if err = k.configureConstraint(unitSpec, "cpu", fmt.Sprintf("%dm", *cpu)); err != nil { 956 return errors.Annotatef(err, "configuring cpu constraint for %s", appName) 957 } 958 } 959 if params.Placement != "" { 960 affinityLabels, err := keyvalues.Parse(strings.Split(params.Placement, ","), false) 961 if err != nil { 962 return errors.Annotatef(err, "invalid placement directive %q", params.Placement) 963 } 964 unitSpec.Pod.NodeSelector = affinityLabels 965 } 966 967 resourceTags := make(map[string]string) 968 for k, v := range params.ResourceTags { 969 resourceTags[k] = v 970 } 971 resourceTags[labelApplication] = appName 972 for _, c := range params.PodSpec.Containers { 973 if c.ImageDetails.Password == "" { 974 continue 975 } 976 imageSecretName := appSecretName(deploymentName, c.Name) 977 if err := k.ensureSecret(imageSecretName, appName, &c.ImageDetails, resourceTags); err != nil { 978 return errors.Annotatef(err, "creating secrets for container: %s", c.Name) 979 } 980 cleanups = append(cleanups, func() { k.deleteSecret(imageSecretName) }) 981 } 982 983 // Add a deployment controller or stateful set configured to create the specified number of units/pods. 984 // Defensively check to see if a stateful set is already used. 985 useStatefulSet := len(params.Filesystems) > 0 986 if !useStatefulSet { 987 statefulsets := k.AppsV1().StatefulSets(k.namespace) 988 _, err := statefulsets.Get(deploymentName, v1.GetOptions{IncludeUninitialized: true}) 989 if err != nil && !k8serrors.IsNotFound(err) { 990 return errors.Trace(err) 991 } 992 useStatefulSet = err == nil 993 if useStatefulSet { 994 logger.Debugf("no updated filesystems but already using stateful set for %v", appName) 995 } 996 } 997 998 numPods := int32(numUnits) 999 if useStatefulSet { 1000 if err := k.configureStatefulSet(appName, deploymentName, resourceTags, unitSpec, params.PodSpec.Containers, &numPods, params.Filesystems); err != nil { 1001 return errors.Annotate(err, "creating or updating StatefulSet") 1002 } 1003 cleanups = append(cleanups, func() { k.deleteDeployment(appName) }) 1004 } else { 1005 if err := k.configureDeployment(appName, deploymentName, resourceTags, unitSpec, params.PodSpec.Containers, &numPods); err != nil { 1006 return errors.Annotate(err, "creating or updating DeploymentController") 1007 } 1008 cleanups = append(cleanups, func() { k.deleteDeployment(appName) }) 1009 } 1010 1011 var ports []core.ContainerPort 1012 for _, c := range unitSpec.Pod.Containers { 1013 for _, p := range c.Ports { 1014 if p.ContainerPort == 0 { 1015 continue 1016 } 1017 ports = append(ports, p) 1018 } 1019 } 1020 if !params.PodSpec.OmitServiceFrontend { 1021 if err := k.configureService(appName, deploymentName, ports, resourceTags, config); err != nil { 1022 return errors.Annotatef(err, "creating or updating service for %v", appName) 1023 } 1024 } 1025 return nil 1026 } 1027 1028 func (k *kubernetesClient) deleteAllPods(appName, deploymentName string) error { 1029 zero := int32(0) 1030 statefulsets := k.AppsV1().StatefulSets(k.namespace) 1031 statefulSet, err := statefulsets.Get(deploymentName, v1.GetOptions{IncludeUninitialized: true}) 1032 if err != nil && !k8serrors.IsNotFound(err) { 1033 return errors.Trace(err) 1034 } 1035 if err == nil { 1036 statefulSet.Spec.Replicas = &zero 1037 _, err = statefulsets.Update(statefulSet) 1038 return errors.Trace(err) 1039 } 1040 1041 deployments := k.AppsV1().Deployments(k.namespace) 1042 deployment, err := deployments.Get(deploymentName, v1.GetOptions{IncludeUninitialized: true}) 1043 if k8serrors.IsNotFound(err) { 1044 return nil 1045 } 1046 if err != nil { 1047 return errors.Trace(err) 1048 } 1049 deployment.Spec.Replicas = &zero 1050 _, err = deployments.Update(deployment) 1051 return errors.Trace(err) 1052 } 1053 1054 func (k *kubernetesClient) configureStorage( 1055 podSpec *core.PodSpec, statefulSet *apps.StatefulSetSpec, appName string, legacy bool, filesystems []storage.KubernetesFilesystemParams, 1056 ) error { 1057 baseDir, err := paths.StorageDir("kubernetes") 1058 if err != nil { 1059 return errors.Trace(err) 1060 } 1061 logger.Debugf("configuring pod filesystems: %+v", filesystems) 1062 for i, fs := range filesystems { 1063 if fs.Provider != K8s_ProviderType { 1064 return errors.Errorf("invalid storage provider type %q for %v", fs.Provider, fs.StorageName) 1065 } 1066 var mountPath string 1067 if fs.Attachment != nil { 1068 mountPath = fs.Attachment.Path 1069 } 1070 if mountPath == "" { 1071 mountPath = fmt.Sprintf("%s/fs/%s/%s/%d", baseDir, appName, fs.StorageName, i) 1072 } 1073 pvcNamePrefix := fmt.Sprintf("%s-%d", fs.StorageName, i) 1074 if legacy { 1075 pvcNamePrefix = "juju-" + pvcNamePrefix 1076 } 1077 params := volumeParams{ 1078 storageLabels: caas.UnitStorageClassLabels(appName, k.namespace), 1079 pvcName: pvcNamePrefix, 1080 requestedVolumeSize: fmt.Sprintf("%dMi", fs.Size), 1081 } 1082 if storageLabel, ok := fs.Attributes[storageLabel]; ok { 1083 params.storageLabels = append([]string{fmt.Sprintf("%v", storageLabel)}, params.storageLabels...) 1084 } 1085 params.storageConfig, err = newStorageConfig(fs.Attributes, defaultStorageClass) 1086 if err != nil { 1087 return errors.Annotatef(err, "invalid storage configuration for %v", fs.StorageName) 1088 } 1089 1090 pvcSpec, err := k.maybeGetVolumeClaimSpec(params) 1091 if err != nil { 1092 return errors.Annotatef(err, "finding volume for %s", fs.StorageName) 1093 } 1094 tags := make(map[string]string) 1095 for k, v := range fs.ResourceTags { 1096 tags[k] = v 1097 } 1098 tags[labelStorage] = fs.StorageName 1099 tags[labelApplication] = appName 1100 pvc := core.PersistentVolumeClaim{ 1101 ObjectMeta: v1.ObjectMeta{ 1102 Name: params.pvcName, 1103 Labels: tags}, 1104 Spec: *pvcSpec, 1105 } 1106 logger.Debugf("using persistent volume claim for %s filesystem %s: %+v", appName, fs.StorageName, pvc) 1107 statefulSet.VolumeClaimTemplates = append(statefulSet.VolumeClaimTemplates, pvc) 1108 podSpec.Containers[0].VolumeMounts = append(podSpec.Containers[0].VolumeMounts, core.VolumeMount{ 1109 Name: pvc.Name, 1110 MountPath: mountPath, 1111 }) 1112 } 1113 return nil 1114 } 1115 1116 func (k *kubernetesClient) configureDevices(unitSpec *unitSpec, devices []devices.KubernetesDeviceParams) error { 1117 for i := range unitSpec.Pod.Containers { 1118 resources := unitSpec.Pod.Containers[i].Resources 1119 for _, dev := range devices { 1120 err := mergeDeviceConstraints(dev, &resources) 1121 if err != nil { 1122 return errors.Annotatef(err, "merging device constraint %+v to %#v", dev, resources) 1123 } 1124 } 1125 unitSpec.Pod.Containers[i].Resources = resources 1126 } 1127 nodeLabel, err := getNodeSelectorFromDeviceConstraints(devices) 1128 if err != nil { 1129 return err 1130 } 1131 if nodeLabel != "" { 1132 unitSpec.Pod.NodeSelector = buildNodeSelector(nodeLabel) 1133 } 1134 return nil 1135 } 1136 1137 func (k *kubernetesClient) configureConstraint(unitSpec *unitSpec, constraint, value string) error { 1138 for i := range unitSpec.Pod.Containers { 1139 resources := unitSpec.Pod.Containers[i].Resources 1140 err := mergeConstraint(constraint, value, &resources) 1141 if err != nil { 1142 return errors.Annotatef(err, "merging constraint %q to %#v", constraint, resources) 1143 } 1144 unitSpec.Pod.Containers[i].Resources = resources 1145 } 1146 return nil 1147 } 1148 1149 type configMapNameFunc func(fileSetName string) string 1150 1151 func (k *kubernetesClient) configurePodFiles(podSpec *core.PodSpec, containers []caas.ContainerSpec, cfgMapName configMapNameFunc) error { 1152 for i, container := range containers { 1153 for _, fileSet := range container.Files { 1154 cfgName := cfgMapName(fileSet.Name) 1155 vol := core.Volume{Name: cfgName} 1156 if err := k.ensureConfigMap(filesetConfigMap(cfgName, &fileSet)); err != nil { 1157 return errors.Annotatef(err, "creating or updating ConfigMap for file set %v", cfgName) 1158 } 1159 vol.ConfigMap = &core.ConfigMapVolumeSource{ 1160 LocalObjectReference: core.LocalObjectReference{ 1161 Name: cfgName, 1162 }, 1163 } 1164 podSpec.Volumes = append(podSpec.Volumes, vol) 1165 podSpec.Containers[i].VolumeMounts = append(podSpec.Containers[i].VolumeMounts, core.VolumeMount{ 1166 Name: cfgName, 1167 MountPath: fileSet.MountPath, 1168 }) 1169 } 1170 } 1171 return nil 1172 } 1173 1174 func (k *kubernetesClient) configureDeployment( 1175 appName, deploymentName string, labels map[string]string, unitSpec *unitSpec, containers []caas.ContainerSpec, replicas *int32, 1176 ) error { 1177 logger.Debugf("creating/updating deployment for %s", appName) 1178 1179 // Add the specified file to the pod spec. 1180 cfgName := func(fileSetName string) string { 1181 return applicationConfigMapName(deploymentName, fileSetName) 1182 } 1183 podSpec := unitSpec.Pod 1184 if err := k.configurePodFiles(&podSpec, containers, cfgName); err != nil { 1185 return errors.Trace(err) 1186 } 1187 1188 deployment := &apps.Deployment{ 1189 ObjectMeta: v1.ObjectMeta{ 1190 Name: deploymentName, 1191 Labels: labels}, 1192 Spec: apps.DeploymentSpec{ 1193 Replicas: replicas, 1194 Selector: &v1.LabelSelector{ 1195 MatchLabels: map[string]string{labelApplication: appName}, 1196 }, 1197 Template: core.PodTemplateSpec{ 1198 ObjectMeta: v1.ObjectMeta{ 1199 GenerateName: deploymentName + "-", 1200 Labels: labels, 1201 }, 1202 Spec: podSpec, 1203 }, 1204 }, 1205 } 1206 return k.ensureDeployment(deployment) 1207 } 1208 1209 func (k *kubernetesClient) ensureDeployment(spec *apps.Deployment) error { 1210 deployments := k.AppsV1().Deployments(k.namespace) 1211 _, err := deployments.Update(spec) 1212 if k8serrors.IsNotFound(err) { 1213 _, err = deployments.Create(spec) 1214 } 1215 return errors.Trace(err) 1216 } 1217 1218 func (k *kubernetesClient) deleteDeployment(name string) error { 1219 deployments := k.AppsV1().Deployments(k.namespace) 1220 err := deployments.Delete(name, &v1.DeleteOptions{ 1221 PropagationPolicy: &defaultPropagationPolicy, 1222 }) 1223 if k8serrors.IsNotFound(err) { 1224 return nil 1225 } 1226 return errors.Trace(err) 1227 } 1228 1229 func (k *kubernetesClient) configureStatefulSet( 1230 appName, deploymentName string, labels map[string]string, unitSpec *unitSpec, 1231 containers []caas.ContainerSpec, replicas *int32, filesystems []storage.KubernetesFilesystemParams, 1232 ) error { 1233 logger.Debugf("creating/updating stateful set for %s", appName) 1234 1235 // Add the specified file to the pod spec. 1236 cfgName := func(fileSetName string) string { 1237 return applicationConfigMapName(deploymentName, fileSetName) 1238 } 1239 statefulset := &apps.StatefulSet{ 1240 ObjectMeta: v1.ObjectMeta{ 1241 Name: deploymentName, 1242 Labels: labels}, 1243 Spec: apps.StatefulSetSpec{ 1244 Replicas: replicas, 1245 Selector: &v1.LabelSelector{ 1246 MatchLabels: map[string]string{labelApplication: appName}, 1247 }, 1248 Template: core.PodTemplateSpec{ 1249 ObjectMeta: v1.ObjectMeta{ 1250 Labels: labels, 1251 }, 1252 }, 1253 PodManagementPolicy: apps.ParallelPodManagement, 1254 }, 1255 } 1256 podSpec := unitSpec.Pod 1257 if err := k.configurePodFiles(&podSpec, containers, cfgName); err != nil { 1258 return errors.Trace(err) 1259 } 1260 existingPodSpec := podSpec 1261 1262 // Create a new stateful set with the necessary storage config. 1263 legacy := isLegacyName(deploymentName) 1264 if err := k.configureStorage(&podSpec, &statefulset.Spec, appName, legacy, filesystems); err != nil { 1265 return errors.Annotatef(err, "configuring storage for %s", appName) 1266 } 1267 statefulset.Spec.Template.Spec = podSpec 1268 return k.ensureStatefulSet(statefulset, existingPodSpec) 1269 } 1270 1271 func (k *kubernetesClient) ensureStatefulSet(spec *apps.StatefulSet, existingPodSpec core.PodSpec) error { 1272 statefulsets := k.AppsV1().StatefulSets(k.namespace) 1273 _, err := statefulsets.Update(spec) 1274 if k8serrors.IsNotFound(err) { 1275 _, err = statefulsets.Create(spec) 1276 } 1277 if !k8serrors.IsInvalid(err) { 1278 return errors.Trace(err) 1279 } 1280 1281 // The statefulset already exists so all we are allowed to update is replicas, 1282 // template, update strategy. Juju may hand out info with a slightly different 1283 // requested volume size due to trying to adapt the unit model to the k8s world. 1284 existing, err := statefulsets.Get(spec.Name, v1.GetOptions{IncludeUninitialized: true}) 1285 if err != nil { 1286 return errors.Trace(err) 1287 } 1288 // TODO(caas) - allow extra storage to be added 1289 existing.Spec.Replicas = spec.Spec.Replicas 1290 existing.Spec.Template.Spec.Containers = existingPodSpec.Containers 1291 _, err = statefulsets.Update(existing) 1292 return errors.Trace(err) 1293 } 1294 1295 func (k *kubernetesClient) deleteStatefulSet(name string) error { 1296 deployments := k.AppsV1().StatefulSets(k.namespace) 1297 err := deployments.Delete(name, &v1.DeleteOptions{ 1298 PropagationPolicy: &defaultPropagationPolicy, 1299 }) 1300 if k8serrors.IsNotFound(err) { 1301 return nil 1302 } 1303 return errors.Trace(err) 1304 } 1305 1306 func (k *kubernetesClient) deleteVolumeClaims(appName string, p *core.Pod) ([]string, error) { 1307 volumesByName := make(map[string]core.Volume) 1308 for _, pv := range p.Spec.Volumes { 1309 volumesByName[pv.Name] = pv 1310 } 1311 1312 var deletedClaimVolumes []string 1313 for _, volMount := range p.Spec.Containers[0].VolumeMounts { 1314 vol, ok := volumesByName[volMount.Name] 1315 if !ok { 1316 logger.Warningf("volume for volume mount %q not found", volMount.Name) 1317 continue 1318 } 1319 if vol.PersistentVolumeClaim == nil { 1320 // Ignore volumes which are not Juju managed filesystems. 1321 continue 1322 } 1323 pvClaims := k.CoreV1().PersistentVolumeClaims(k.namespace) 1324 err := pvClaims.Delete(vol.PersistentVolumeClaim.ClaimName, &v1.DeleteOptions{ 1325 PropagationPolicy: &defaultPropagationPolicy, 1326 }) 1327 if err != nil && !k8serrors.IsNotFound(err) { 1328 return nil, errors.Annotatef(err, "deleting persistent volume claim %v for %v", 1329 vol.PersistentVolumeClaim.ClaimName, p.Name) 1330 } 1331 deletedClaimVolumes = append(deletedClaimVolumes, vol.Name) 1332 } 1333 return deletedClaimVolumes, nil 1334 } 1335 1336 func (k *kubernetesClient) configureService( 1337 appName, deploymentName string, containerPorts []core.ContainerPort, 1338 tags map[string]string, config application.ConfigAttributes, 1339 ) error { 1340 logger.Debugf("creating/updating service for %s", appName) 1341 1342 var ports []core.ServicePort 1343 for i, cp := range containerPorts { 1344 // We normally expect a single container port for most use cases. 1345 // We allow the user to specify what first service port should be, 1346 // otherwise it just defaults to the container port. 1347 // TODO(caas) - consider allowing all service ports to be specified 1348 var targetPort intstr.IntOrString 1349 if i == 0 { 1350 targetPort = intstr.FromInt(config.GetInt(serviceTargetPortConfigKey, int(cp.ContainerPort))) 1351 } 1352 ports = append(ports, core.ServicePort{ 1353 Name: cp.Name, 1354 Protocol: cp.Protocol, 1355 Port: cp.ContainerPort, 1356 TargetPort: targetPort, 1357 }) 1358 } 1359 1360 serviceType := core.ServiceType(config.GetString(serviceTypeConfigKey, defaultServiceType)) 1361 annotations, err := config.GetStringMap(serviceAnnotationsKey, nil) 1362 if err != nil { 1363 return errors.Annotatef(err, "unexpected annotations: %#v", config.Get(serviceAnnotationsKey, nil)) 1364 } 1365 service := &core.Service{ 1366 ObjectMeta: v1.ObjectMeta{ 1367 Name: deploymentName, 1368 Labels: tags, 1369 Annotations: annotations, 1370 }, 1371 Spec: core.ServiceSpec{ 1372 Selector: map[string]string{labelApplication: appName}, 1373 Type: serviceType, 1374 Ports: ports, 1375 ExternalIPs: config.Get(serviceExternalIPsConfigKey, []string(nil)).([]string), 1376 LoadBalancerIP: config.GetString(serviceLoadBalancerIPKey, ""), 1377 LoadBalancerSourceRanges: config.Get(serviceLoadBalancerSourceRangesKey, []string(nil)).([]string), 1378 ExternalName: config.GetString(serviceExternalNameKey, ""), 1379 }, 1380 } 1381 return k.ensureService(service) 1382 } 1383 1384 func (k *kubernetesClient) ensureService(spec *core.Service) error { 1385 services := k.CoreV1().Services(k.namespace) 1386 // Set any immutable fields if the service already exists. 1387 existing, err := services.Get(spec.Name, v1.GetOptions{IncludeUninitialized: true}) 1388 if err == nil { 1389 spec.Spec.ClusterIP = existing.Spec.ClusterIP 1390 spec.ObjectMeta.ResourceVersion = existing.ObjectMeta.ResourceVersion 1391 } 1392 _, err = services.Update(spec) 1393 if k8serrors.IsNotFound(err) { 1394 _, err = services.Create(spec) 1395 } 1396 return errors.Trace(err) 1397 } 1398 1399 func (k *kubernetesClient) deleteService(deploymentName string) error { 1400 services := k.CoreV1().Services(k.namespace) 1401 err := services.Delete(deploymentName, &v1.DeleteOptions{ 1402 PropagationPolicy: &defaultPropagationPolicy, 1403 }) 1404 if k8serrors.IsNotFound(err) { 1405 return nil 1406 } 1407 return errors.Trace(err) 1408 } 1409 1410 // ExposeService sets up external access to the specified application. 1411 func (k *kubernetesClient) ExposeService(appName string, resourceTags map[string]string, config application.ConfigAttributes) error { 1412 logger.Debugf("creating/updating ingress resource for %s", appName) 1413 1414 host := config.GetString(caas.JujuExternalHostNameKey, "") 1415 if host == "" { 1416 return errors.Errorf("external hostname required") 1417 } 1418 ingressClass := config.GetString(ingressClassKey, defaultIngressClass) 1419 ingressSSLRedirect := config.GetBool(ingressSSLRedirectKey, defaultIngressSSLRedirect) 1420 ingressSSLPassthrough := config.GetBool(ingressSSLPassthroughKey, defaultIngressSSLPassthrough) 1421 ingressAllowHTTP := config.GetBool(ingressAllowHTTPKey, defaultIngressAllowHTTPKey) 1422 httpPath := config.GetString(caas.JujuApplicationPath, caas.JujuDefaultApplicationPath) 1423 if httpPath == "$appname" { 1424 httpPath = appName 1425 } 1426 if !strings.HasPrefix(httpPath, "/") { 1427 httpPath = "/" + httpPath 1428 } 1429 1430 deploymentName := k.deploymentName(appName) 1431 svc, err := k.CoreV1().Services(k.namespace).Get(deploymentName, v1.GetOptions{}) 1432 if err != nil { 1433 return errors.Trace(err) 1434 } 1435 if len(svc.Spec.Ports) == 0 { 1436 return errors.Errorf("cannot create ingress rule for service %q without a port", svc.Name) 1437 } 1438 spec := &v1beta1.Ingress{ 1439 ObjectMeta: v1.ObjectMeta{ 1440 Name: deploymentName, 1441 Labels: resourceTags, 1442 Annotations: map[string]string{ 1443 "ingress.kubernetes.io/rewrite-target": "", 1444 "ingress.kubernetes.io/ssl-redirect": strconv.FormatBool(ingressSSLRedirect), 1445 "kubernetes.io/ingress.class": ingressClass, 1446 "kubernetes.io/ingress.allow-http": strconv.FormatBool(ingressAllowHTTP), 1447 "ingress.kubernetes.io/ssl-passthrough": strconv.FormatBool(ingressSSLPassthrough), 1448 }, 1449 }, 1450 Spec: v1beta1.IngressSpec{ 1451 Rules: []v1beta1.IngressRule{{ 1452 Host: host, 1453 IngressRuleValue: v1beta1.IngressRuleValue{ 1454 HTTP: &v1beta1.HTTPIngressRuleValue{ 1455 Paths: []v1beta1.HTTPIngressPath{{ 1456 Path: httpPath, 1457 Backend: v1beta1.IngressBackend{ 1458 ServiceName: svc.Name, ServicePort: svc.Spec.Ports[0].TargetPort}, 1459 }}}, 1460 }}}, 1461 }, 1462 } 1463 return k.ensureIngress(spec) 1464 } 1465 1466 // UnexposeService removes external access to the specified service. 1467 func (k *kubernetesClient) UnexposeService(appName string) error { 1468 logger.Debugf("deleting ingress resource for %s", appName) 1469 return k.deleteIngress(appName) 1470 } 1471 1472 func (k *kubernetesClient) ensureIngress(spec *v1beta1.Ingress) error { 1473 ingress := k.ExtensionsV1beta1().Ingresses(k.namespace) 1474 _, err := ingress.Update(spec) 1475 if k8serrors.IsNotFound(err) { 1476 _, err = ingress.Create(spec) 1477 } 1478 return errors.Trace(err) 1479 } 1480 1481 func (k *kubernetesClient) deleteIngress(appName string) error { 1482 deploymentName := k.deploymentName(appName) 1483 ingress := k.ExtensionsV1beta1().Ingresses(k.namespace) 1484 err := ingress.Delete(deploymentName, &v1.DeleteOptions{ 1485 PropagationPolicy: &defaultPropagationPolicy, 1486 }) 1487 if k8serrors.IsNotFound(err) { 1488 return nil 1489 } 1490 return errors.Trace(err) 1491 } 1492 1493 func operatorSelector(appName string) string { 1494 return fmt.Sprintf("%v==%v", labelOperator, appName) 1495 } 1496 1497 func applicationSelector(appName string) string { 1498 return fmt.Sprintf("%v==%v", labelApplication, appName) 1499 } 1500 1501 // WatchUnits returns a watcher which notifies when there 1502 // are changes to units of the specified application. 1503 func (k *kubernetesClient) WatchUnits(appName string) (watcher.NotifyWatcher, error) { 1504 pods := k.CoreV1().Pods(k.namespace) 1505 w, err := pods.Watch(v1.ListOptions{ 1506 LabelSelector: applicationSelector(appName), 1507 Watch: true, 1508 }) 1509 if err != nil { 1510 return nil, errors.Trace(err) 1511 } 1512 return k.newWatcher(w, appName, k.clock) 1513 } 1514 1515 // WatchOperator returns a watcher which notifies when there 1516 // are changes to the operator of the specified application. 1517 func (k *kubernetesClient) WatchOperator(appName string) (watcher.NotifyWatcher, error) { 1518 pods := k.CoreV1().Pods(k.namespace) 1519 w, err := pods.Watch(v1.ListOptions{ 1520 LabelSelector: operatorSelector(appName), 1521 Watch: true, 1522 }) 1523 if err != nil { 1524 return nil, errors.Trace(err) 1525 } 1526 return k.newWatcher(w, appName, k.clock) 1527 } 1528 1529 // legacyJujuPVNameRegexp matches how Juju labels persistent volumes. 1530 // The pattern is: juju-<storagename>-<digit> 1531 var legacyJujuPVNameRegexp = regexp.MustCompile(`^juju-(?P<storageName>\D+)-\d+$`) 1532 1533 // jujuPVNameRegexp matches how Juju labels persistent volumes. 1534 // The pattern is: <storagename>-<digit> 1535 var jujuPVNameRegexp = regexp.MustCompile(`^(?P<storageName>\D+)-\d+$`) 1536 1537 // Units returns all units and any associated filesystems of the specified application. 1538 // Filesystems are mounted via volumes bound to the unit. 1539 func (k *kubernetesClient) Units(appName string) ([]caas.Unit, error) { 1540 pods := k.CoreV1().Pods(k.namespace) 1541 podsList, err := pods.List(v1.ListOptions{ 1542 LabelSelector: applicationSelector(appName), 1543 }) 1544 if err != nil { 1545 return nil, errors.Trace(err) 1546 } 1547 1548 var units []caas.Unit 1549 now := time.Now() 1550 for _, p := range podsList.Items { 1551 var ports []string 1552 for _, c := range p.Spec.Containers { 1553 for _, p := range c.Ports { 1554 ports = append(ports, fmt.Sprintf("%v/%v", p.ContainerPort, p.Protocol)) 1555 } 1556 } 1557 terminated := p.DeletionTimestamp != nil 1558 statusMessage, unitStatus, since, err := k.getPODStatus(p, now) 1559 if err != nil { 1560 return nil, errors.Trace(err) 1561 } 1562 unitInfo := caas.Unit{ 1563 Id: string(p.UID), 1564 Address: p.Status.PodIP, 1565 Ports: ports, 1566 Dying: terminated, 1567 Status: status.StatusInfo{ 1568 Status: unitStatus, 1569 Message: statusMessage, 1570 Since: &since, 1571 }, 1572 } 1573 1574 volumesByName := make(map[string]core.Volume) 1575 for _, pv := range p.Spec.Volumes { 1576 volumesByName[pv.Name] = pv 1577 } 1578 pVolumes := k.CoreV1().PersistentVolumes() 1579 1580 // Gather info about how filesystems are attached/mounted to the pod. 1581 // The mount name represents the filesystem tag name used by Juju. 1582 for _, volMount := range p.Spec.Containers[0].VolumeMounts { 1583 vol, ok := volumesByName[volMount.Name] 1584 if !ok { 1585 logger.Warningf("volume for volume mount %q not found", volMount.Name) 1586 continue 1587 } 1588 if vol.PersistentVolumeClaim == nil || vol.PersistentVolumeClaim.ClaimName == "" { 1589 // Ignore volumes which are not Juju managed filesystems. 1590 logger.Debugf("Ignoring blank PersistentVolumeClaim or ClaimName") 1591 continue 1592 } 1593 pvClaims := k.CoreV1().PersistentVolumeClaims(k.namespace) 1594 pvc, err := pvClaims.Get(vol.PersistentVolumeClaim.ClaimName, v1.GetOptions{}) 1595 if k8serrors.IsNotFound(err) { 1596 // Ignore claims which don't exist (yet). 1597 continue 1598 } 1599 if err != nil { 1600 return nil, errors.Annotate(err, "unable to get persistent volume claim") 1601 } 1602 1603 if pvc.Status.Phase == core.ClaimPending { 1604 logger.Debugf(fmt.Sprintf("PersistentVolumeClaim for %v is pending", vol.PersistentVolumeClaim.ClaimName)) 1605 continue 1606 } 1607 pv, err := pVolumes.Get(pvc.Spec.VolumeName, v1.GetOptions{}) 1608 if k8serrors.IsNotFound(err) { 1609 // Ignore volumes which don't exist (yet). 1610 continue 1611 } 1612 if err != nil { 1613 return nil, errors.Annotate(err, "unable to get persistent volume") 1614 } 1615 1616 storageName := pvc.Labels[labelStorage] 1617 if storageName == "" { 1618 if valid := legacyJujuPVNameRegexp.MatchString(volMount.Name); valid { 1619 storageName = legacyJujuPVNameRegexp.ReplaceAllString(volMount.Name, "$storageName") 1620 } else if valid := jujuPVNameRegexp.MatchString(volMount.Name); valid { 1621 storageName = jujuPVNameRegexp.ReplaceAllString(volMount.Name, "$storageName") 1622 } 1623 } 1624 statusMessage := "" 1625 since = now 1626 if len(pvc.Status.Conditions) > 0 { 1627 statusMessage = pvc.Status.Conditions[0].Message 1628 since = pvc.Status.Conditions[0].LastProbeTime.Time 1629 } 1630 if statusMessage == "" { 1631 // If there are any events for this pvc we can use the 1632 // most recent to set the status. 1633 events := k.CoreV1().Events(k.namespace) 1634 eventList, err := events.List(v1.ListOptions{ 1635 IncludeUninitialized: true, 1636 FieldSelector: fields.OneTermEqualSelector("involvedObject.name", pvc.Name).String(), 1637 }) 1638 if err != nil { 1639 return nil, errors.Annotate(err, "unable to get events for PVC") 1640 } 1641 // Take the most recent event. 1642 if count := len(eventList.Items); count > 0 { 1643 statusMessage = eventList.Items[count-1].Message 1644 } 1645 } 1646 1647 unitInfo.FilesystemInfo = append(unitInfo.FilesystemInfo, caas.FilesystemInfo{ 1648 StorageName: storageName, 1649 Size: uint64(vol.PersistentVolumeClaim.Size()), 1650 FilesystemId: pvc.Name, 1651 MountPoint: volMount.MountPath, 1652 ReadOnly: volMount.ReadOnly, 1653 Status: status.StatusInfo{ 1654 Status: k.jujuFilesystemStatus(pvc.Status.Phase), 1655 Message: statusMessage, 1656 Since: &since, 1657 }, 1658 Volume: caas.VolumeInfo{ 1659 VolumeId: pv.Name, 1660 Size: uint64(pv.Size()), 1661 Persistent: pv.Spec.PersistentVolumeReclaimPolicy == core.PersistentVolumeReclaimRetain, 1662 Status: status.StatusInfo{ 1663 Status: k.jujuVolumeStatus(pv.Status.Phase), 1664 Message: pv.Status.Message, 1665 Since: &since, 1666 }, 1667 }, 1668 }) 1669 } 1670 units = append(units, unitInfo) 1671 } 1672 return units, nil 1673 } 1674 1675 // Operator returns an Operator with current status and life details. 1676 func (k *kubernetesClient) Operator(appName string) (*caas.Operator, error) { 1677 pods := k.CoreV1().Pods(k.namespace) 1678 podsList, err := pods.List(v1.ListOptions{ 1679 LabelSelector: operatorSelector(appName), 1680 }) 1681 if err != nil { 1682 return nil, errors.Trace(err) 1683 } 1684 if len(podsList.Items) == 0 { 1685 return nil, errors.NotFoundf("operator pod for application %q", appName) 1686 } 1687 1688 opPod := podsList.Items[0] 1689 terminated := opPod.DeletionTimestamp != nil 1690 now := time.Now() 1691 statusMessage, opStatus, since, err := k.getPODStatus(opPod, now) 1692 return &caas.Operator{ 1693 Id: string(opPod.UID), 1694 Dying: terminated, 1695 Status: status.StatusInfo{ 1696 Status: opStatus, 1697 Message: statusMessage, 1698 Since: &since, 1699 }, 1700 }, nil 1701 } 1702 1703 func (k *kubernetesClient) getPODStatus(pod core.Pod, now time.Time) (string, status.Status, time.Time, error) { 1704 terminated := pod.DeletionTimestamp != nil 1705 jujuStatus := k.jujuStatus(pod.Status.Phase, terminated) 1706 statusMessage := pod.Status.Message 1707 since := now 1708 if statusMessage == "" { 1709 for _, cond := range pod.Status.Conditions { 1710 statusMessage = cond.Message 1711 since = cond.LastProbeTime.Time 1712 if cond.Type == core.PodScheduled && cond.Reason == core.PodReasonUnschedulable { 1713 jujuStatus = status.Blocked 1714 break 1715 } 1716 } 1717 } 1718 1719 if statusMessage == "" { 1720 // If there are any events for this pod we can use the 1721 // most recent to set the status. 1722 events := k.CoreV1().Events(k.namespace) 1723 eventList, err := events.List(v1.ListOptions{ 1724 IncludeUninitialized: true, 1725 FieldSelector: fields.OneTermEqualSelector("involvedObject.name", pod.Name).String(), 1726 }) 1727 if err != nil { 1728 return "", "", time.Time{}, errors.Trace(err) 1729 } 1730 // Take the most recent event. 1731 if count := len(eventList.Items); count > 0 { 1732 statusMessage = eventList.Items[count-1].Message 1733 } 1734 } 1735 1736 return statusMessage, jujuStatus, since, nil 1737 } 1738 1739 func (k *kubernetesClient) jujuStatus(podPhase core.PodPhase, terminated bool) status.Status { 1740 if terminated { 1741 return status.Terminated 1742 } 1743 switch podPhase { 1744 case core.PodRunning: 1745 return status.Running 1746 case core.PodFailed: 1747 return status.Error 1748 case core.PodPending: 1749 return status.Allocating 1750 default: 1751 return status.Unknown 1752 } 1753 } 1754 1755 func (k *kubernetesClient) jujuFilesystemStatus(pvcPhase core.PersistentVolumeClaimPhase) status.Status { 1756 switch pvcPhase { 1757 case core.ClaimPending: 1758 return status.Pending 1759 case core.ClaimBound: 1760 return status.Attached 1761 case core.ClaimLost: 1762 return status.Detached 1763 default: 1764 return status.Unknown 1765 } 1766 } 1767 1768 func (k *kubernetesClient) jujuVolumeStatus(pvPhase core.PersistentVolumePhase) status.Status { 1769 switch pvPhase { 1770 case core.VolumePending: 1771 return status.Pending 1772 case core.VolumeBound: 1773 return status.Attached 1774 case core.VolumeAvailable, core.VolumeReleased: 1775 return status.Detached 1776 case core.VolumeFailed: 1777 return status.Error 1778 default: 1779 return status.Unknown 1780 } 1781 } 1782 1783 // filesetConfigMap returns a *core.ConfigMap for a pod 1784 // of the specified unit, with the specified files. 1785 func filesetConfigMap(configMapName string, files *caas.FileSet) *core.ConfigMap { 1786 result := &core.ConfigMap{ 1787 ObjectMeta: v1.ObjectMeta{ 1788 Name: configMapName, 1789 }, 1790 Data: map[string]string{}, 1791 } 1792 for name, data := range files.Files { 1793 result.Data[name] = data 1794 } 1795 return result 1796 } 1797 1798 func (k *kubernetesClient) ensureConfigMap(configMap *core.ConfigMap) error { 1799 configMaps := k.CoreV1().ConfigMaps(k.namespace) 1800 _, err := configMaps.Update(configMap) 1801 if k8serrors.IsNotFound(err) { 1802 _, err = configMaps.Create(configMap) 1803 } 1804 return errors.Trace(err) 1805 } 1806 1807 // operatorPod returns a *core.Pod for the operator pod 1808 // of the specified application. 1809 func operatorPod(podName, appName, agentPath, operatorImagePath, version string, tags map[string]string) *core.Pod { 1810 configMapName := operatorConfigMapName(podName) 1811 configVolName := configMapName 1812 1813 if isLegacyName(podName) { 1814 configVolName += "-volume" 1815 } 1816 1817 appTag := names.NewApplicationTag(appName) 1818 podLabels := make(map[string]string) 1819 for k, v := range tags { 1820 podLabels[k] = v 1821 } 1822 podLabels[labelVersion] = version 1823 return &core.Pod{ 1824 ObjectMeta: v1.ObjectMeta{ 1825 Name: podName, 1826 Labels: podLabels, 1827 }, 1828 Spec: core.PodSpec{ 1829 Containers: []core.Container{{ 1830 Name: "juju-operator", 1831 ImagePullPolicy: core.PullIfNotPresent, 1832 Image: operatorImagePath, 1833 Env: []core.EnvVar{ 1834 {Name: "JUJU_APPLICATION", Value: appName}, 1835 }, 1836 VolumeMounts: []core.VolumeMount{{ 1837 Name: configVolName, 1838 MountPath: filepath.Join(agent.Dir(agentPath, appTag), "template-agent.conf"), 1839 SubPath: "template-agent.conf", 1840 }}, 1841 }}, 1842 Volumes: []core.Volume{{ 1843 Name: configVolName, 1844 VolumeSource: core.VolumeSource{ 1845 ConfigMap: &core.ConfigMapVolumeSource{ 1846 LocalObjectReference: core.LocalObjectReference{ 1847 Name: configMapName, 1848 }, 1849 Items: []core.KeyToPath{{ 1850 Key: appName + "-agent.conf", 1851 Path: "template-agent.conf", 1852 }}, 1853 }, 1854 }, 1855 }}, 1856 }, 1857 } 1858 } 1859 1860 // operatorConfigMap returns a *core.ConfigMap for the operator pod 1861 // of the specified application, with the specified configuration. 1862 func operatorConfigMap(appName, operatorName string, config *caas.OperatorConfig) *core.ConfigMap { 1863 configMapName := operatorConfigMapName(operatorName) 1864 return &core.ConfigMap{ 1865 ObjectMeta: v1.ObjectMeta{ 1866 Name: configMapName, 1867 }, 1868 Data: map[string]string{ 1869 appName + "-agent.conf": string(config.AgentConf), 1870 }, 1871 } 1872 } 1873 1874 type unitSpec struct { 1875 Pod core.PodSpec `json:"pod"` 1876 } 1877 1878 var defaultPodTemplate = ` 1879 pod: 1880 containers: 1881 {{- range .Containers }} 1882 - name: {{.Name}} 1883 {{if .Ports}} 1884 ports: 1885 {{- range .Ports }} 1886 - containerPort: {{.ContainerPort}} 1887 {{if .Name}}name: {{.Name}}{{end}} 1888 {{if .Protocol}}protocol: {{.Protocol}}{{end}} 1889 {{- end}} 1890 {{end}} 1891 {{if .Command}} 1892 command: [{{- range $idx, $c := .Command -}}{{if ne $idx 0}},{{end}}"{{$c}}"{{- end -}}] 1893 {{end}} 1894 {{if .Args}} 1895 args: [{{- range $idx, $a := .Args -}}{{if ne $idx 0}},{{end}}"{{$a}}"{{- end -}}] 1896 {{end}} 1897 {{if .WorkingDir}} 1898 workingDir: {{.WorkingDir}} 1899 {{end}} 1900 {{if .Config}} 1901 env: 1902 {{- range $k, $v := .Config }} 1903 - name: {{$k}} 1904 value: {{$v}} 1905 {{- end}} 1906 {{end}} 1907 {{- end}} 1908 `[1:] 1909 1910 func makeUnitSpec(appName, deploymentName string, podSpec *caas.PodSpec) (*unitSpec, error) { 1911 // Fill out the easy bits using a template. 1912 tmpl := template.Must(template.New("").Parse(defaultPodTemplate)) 1913 var buf bytes.Buffer 1914 if err := tmpl.Execute(&buf, podSpec); err != nil { 1915 return nil, errors.Trace(err) 1916 } 1917 unitSpecString := buf.String() 1918 1919 var unitSpec unitSpec 1920 decoder := yaml.NewYAMLOrJSONDecoder(strings.NewReader(unitSpecString), len(unitSpecString)) 1921 if err := decoder.Decode(&unitSpec); err != nil { 1922 logger.Errorf("unable to parse %q pod spec: %+v\n%v", appName, *podSpec, unitSpecString) 1923 return nil, errors.Trace(err) 1924 } 1925 1926 var imageSecretNames []core.LocalObjectReference 1927 // Now fill in the hard bits progamatically. 1928 for i, c := range podSpec.Containers { 1929 if c.Image != "" { 1930 logger.Warningf("Image parameter deprecated, use ImageDetails") 1931 unitSpec.Pod.Containers[i].Image = c.Image 1932 } else { 1933 unitSpec.Pod.Containers[i].Image = c.ImageDetails.ImagePath 1934 } 1935 if c.ImageDetails.Password != "" { 1936 imageSecretNames = append(imageSecretNames, core.LocalObjectReference{Name: appSecretName(deploymentName, c.Name)}) 1937 } 1938 1939 if c.ProviderContainer == nil { 1940 continue 1941 } 1942 spec, ok := c.ProviderContainer.(*K8sContainerSpec) 1943 if !ok { 1944 return nil, errors.Errorf("unexpected kubernetes container spec type %T", c.ProviderContainer) 1945 } 1946 unitSpec.Pod.Containers[i].ImagePullPolicy = spec.ImagePullPolicy 1947 if spec.LivenessProbe != nil { 1948 unitSpec.Pod.Containers[i].LivenessProbe = spec.LivenessProbe 1949 } 1950 if spec.ReadinessProbe != nil { 1951 unitSpec.Pod.Containers[i].ReadinessProbe = spec.ReadinessProbe 1952 } 1953 } 1954 unitSpec.Pod.ImagePullSecrets = imageSecretNames 1955 if podSpec.ProviderPod != nil { 1956 spec, ok := podSpec.ProviderPod.(*K8sPodSpec) 1957 if !ok { 1958 return nil, errors.Errorf("unexpected kubernetes pod spec type %T", podSpec.ProviderPod) 1959 } 1960 unitSpec.Pod.ActiveDeadlineSeconds = spec.ActiveDeadlineSeconds 1961 unitSpec.Pod.ServiceAccountName = spec.ServiceAccountName 1962 unitSpec.Pod.TerminationGracePeriodSeconds = spec.TerminationGracePeriodSeconds 1963 unitSpec.Pod.Hostname = spec.Hostname 1964 unitSpec.Pod.Subdomain = spec.Subdomain 1965 unitSpec.Pod.DNSConfig = spec.DNSConfig 1966 unitSpec.Pod.DNSPolicy = spec.DNSPolicy 1967 unitSpec.Pod.Priority = spec.Priority 1968 unitSpec.Pod.PriorityClassName = spec.PriorityClassName 1969 unitSpec.Pod.SecurityContext = spec.SecurityContext 1970 unitSpec.Pod.RestartPolicy = spec.RestartPolicy 1971 unitSpec.Pod.AutomountServiceAccountToken = spec.AutomountServiceAccountToken 1972 unitSpec.Pod.ReadinessGates = spec.ReadinessGates 1973 } 1974 return &unitSpec, nil 1975 } 1976 1977 // legacyAppName returns true if there are any artifacts for 1978 // appName which indicate that this deployment was for Juju 2.5.0. 1979 func (k *kubernetesClient) legacyAppName(appName string) bool { 1980 statefulsets := k.AppsV1().StatefulSets(k.namespace) 1981 legacyName := "juju-operator-" + appName 1982 _, err := statefulsets.Get(legacyName, v1.GetOptions{IncludeUninitialized: true}) 1983 return err == nil 1984 } 1985 1986 func (k *kubernetesClient) operatorName(appName string) string { 1987 if k.legacyAppName(appName) { 1988 return "juju-operator-" + appName 1989 } 1990 return appName + "-operator" 1991 } 1992 1993 func (k *kubernetesClient) deploymentName(appName string) string { 1994 if k.legacyAppName(appName) { 1995 return "juju-" + appName 1996 } 1997 return appName 1998 } 1999 2000 func isLegacyName(resourceName string) bool { 2001 return strings.HasPrefix(resourceName, "juju-") 2002 } 2003 2004 func operatorConfigMapName(operatorName string) string { 2005 return operatorName + "-config" 2006 } 2007 2008 func applicationConfigMapName(deploymentName, fileSetName string) string { 2009 return fmt.Sprintf("%v-%v-config", deploymentName, fileSetName) 2010 } 2011 2012 func appSecretName(deploymentName, containerName string) string { 2013 // A pod may have multiple containers with different images and thus different secrets 2014 return deploymentName + "-" + containerName + "-secret" 2015 } 2016 2017 func qualifiedStorageClassName(namespace, storageClass string) string { 2018 return namespace + "-" + storageClass 2019 } 2020 2021 func mergeDeviceConstraints(device devices.KubernetesDeviceParams, resources *core.ResourceRequirements) error { 2022 if resources.Limits == nil { 2023 resources.Limits = core.ResourceList{} 2024 } 2025 if resources.Requests == nil { 2026 resources.Requests = core.ResourceList{} 2027 } 2028 2029 resourceName := core.ResourceName(device.Type) 2030 if v, ok := resources.Limits[resourceName]; ok { 2031 return errors.NotValidf("resource limit for %q has already been set to %v! resource limit %q", resourceName, v, resourceName) 2032 } 2033 if v, ok := resources.Requests[resourceName]; ok { 2034 return errors.NotValidf("resource request for %q has already been set to %v! resource limit %q", resourceName, v, resourceName) 2035 } 2036 // GPU request/limit have to be set to same value equals to the Count. 2037 // - https://kubernetes.io/docs/tasks/manage-gpus/scheduling-gpus/#clusters-containing-different-types-of-nvidia-gpus 2038 resources.Limits[resourceName] = *resource.NewQuantity(device.Count, resource.DecimalSI) 2039 resources.Requests[resourceName] = *resource.NewQuantity(device.Count, resource.DecimalSI) 2040 return nil 2041 } 2042 2043 func mergeConstraint(constraint string, value string, resources *core.ResourceRequirements) error { 2044 if resources.Limits == nil { 2045 resources.Limits = core.ResourceList{} 2046 } 2047 resourceName := core.ResourceName(constraint) 2048 if v, ok := resources.Limits[resourceName]; ok { 2049 return errors.NotValidf("resource limit for %q has already been set to %v!", resourceName, v) 2050 } 2051 parsedValue, err := resource.ParseQuantity(value) 2052 if err != nil { 2053 return errors.Annotatef(err, "invalid constraint value %q for %v", value, constraint) 2054 } 2055 resources.Limits[resourceName] = parsedValue 2056 return nil 2057 } 2058 2059 func buildNodeSelector(nodeLabel string) map[string]string { 2060 // TODO(caas): to support GKE, set it to `cloud.google.com/gke-accelerator`, 2061 // current only set to generic `accelerator` because we do not have k8s provider concept yet. 2062 key := "accelerator" 2063 return map[string]string{key: nodeLabel} 2064 } 2065 2066 func getNodeSelectorFromDeviceConstraints(devices []devices.KubernetesDeviceParams) (string, error) { 2067 var nodeSelector string 2068 for _, device := range devices { 2069 if device.Attributes == nil { 2070 continue 2071 } 2072 if label, ok := device.Attributes[gpuAffinityNodeSelectorKey]; ok { 2073 if nodeSelector != "" && nodeSelector != label { 2074 return "", errors.NotValidf( 2075 "node affinity labels have to be same for all device constraints in same pod - containers in same pod are scheduled in same node.") 2076 } 2077 nodeSelector = label 2078 } 2079 } 2080 return nodeSelector, nil 2081 }