1""" 2Manage kubernetes resources as salt states 3========================================== 4 5NOTE: This module requires the proper pillar values set. See 6salt.modules.kubernetesmod for more information. 7 8.. warning:: 9 10 Configuration options will change in 2019.2.0. 11 12The kubernetes module is used to manage different kubernetes resources. 13 14 15.. code-block:: yaml 16 17 my-nginx: 18 kubernetes.deployment_present: 19 - namespace: default 20 metadata: 21 app: frontend 22 spec: 23 replicas: 1 24 template: 25 metadata: 26 labels: 27 run: my-nginx 28 spec: 29 containers: 30 - name: my-nginx 31 image: nginx 32 ports: 33 - containerPort: 80 34 35 my-mariadb: 36 kubernetes.deployment_absent: 37 - namespace: default 38 39 # kubernetes deployment as specified inside of 40 # a file containing the definition of the the 41 # deployment using the official kubernetes format 42 redis-master-deployment: 43 kubernetes.deployment_present: 44 - name: redis-master 45 - source: salt://k8s/redis-master-deployment.yml 46 require: 47 - pip: kubernetes-python-module 48 49 # kubernetes service as specified inside of 50 # a file containing the definition of the the 51 # service using the official kubernetes format 52 redis-master-service: 53 kubernetes.service_present: 54 - name: redis-master 55 - source: salt://k8s/redis-master-service.yml 56 require: 57 - kubernetes.deployment_present: redis-master 58 59 # kubernetes deployment as specified inside of 60 # a file containing the definition of the the 61 # deployment using the official kubernetes format 62 # plus some jinja directives 63 nginx-source-template: 64 kubernetes.deployment_present: 65 - source: salt://k8s/nginx.yml.jinja 66 - template: jinja 67 require: 68 - pip: kubernetes-python-module 69 70 71 # Kubernetes secret 72 k8s-secret: 73 kubernetes.secret_present: 74 - name: top-secret 75 data: 76 key1: value1 77 key2: value2 78 key3: value3 79 80.. versionadded:: 2017.7.0 81""" 82 83import copy 84import logging 85 86log = logging.getLogger(__name__) 87 88 89def __virtual__(): 90 """ 91 Only load if the kubernetes module is available in __salt__ 92 """ 93 if "kubernetes.ping" in __salt__: 94 return True 95 return (False, "kubernetes module could not be loaded") 96 97 98def _error(ret, err_msg): 99 """ 100 Helper function to propagate errors to 101 the end user. 102 """ 103 ret["result"] = False 104 ret["comment"] = err_msg 105 return ret 106 107 108def deployment_absent(name, namespace="default", **kwargs): 109 """ 110 Ensures that the named deployment is absent from the given namespace. 111 112 name 113 The name of the deployment 114 115 namespace 116 The name of the namespace 117 """ 118 119 ret = {"name": name, "changes": {}, "result": False, "comment": ""} 120 121 deployment = __salt__["kubernetes.show_deployment"](name, namespace, **kwargs) 122 123 if deployment is None: 124 ret["result"] = True if not __opts__["test"] else None 125 ret["comment"] = "The deployment does not exist" 126 return ret 127 128 if __opts__["test"]: 129 ret["comment"] = "The deployment is going to be deleted" 130 ret["result"] = None 131 return ret 132 133 res = __salt__["kubernetes.delete_deployment"](name, namespace, **kwargs) 134 if res["code"] == 200: 135 ret["result"] = True 136 ret["changes"] = {"kubernetes.deployment": {"new": "absent", "old": "present"}} 137 ret["comment"] = res["message"] 138 else: 139 ret["comment"] = "Something went wrong, response: {}".format(res) 140 141 return ret 142 143 144def deployment_present( 145 name, 146 namespace="default", 147 metadata=None, 148 spec=None, 149 source="", 150 template="", 151 **kwargs 152): 153 """ 154 Ensures that the named deployment is present inside of the specified 155 namespace with the given metadata and spec. 156 If the deployment exists it will be replaced. 157 158 name 159 The name of the deployment. 160 161 namespace 162 The namespace holding the deployment. The 'default' one is going to be 163 used unless a different one is specified. 164 165 metadata 166 The metadata of the deployment object. 167 168 spec 169 The spec of the deployment object. 170 171 source 172 A file containing the definition of the deployment (metadata and 173 spec) in the official kubernetes format. 174 175 template 176 Template engine to be used to render the source file. 177 """ 178 ret = {"name": name, "changes": {}, "result": False, "comment": ""} 179 180 if (metadata or spec) and source: 181 return _error( 182 ret, "'source' cannot be used in combination with 'metadata' or 'spec'" 183 ) 184 185 if metadata is None: 186 metadata = {} 187 188 if spec is None: 189 spec = {} 190 191 deployment = __salt__["kubernetes.show_deployment"](name, namespace, **kwargs) 192 193 if deployment is None: 194 if __opts__["test"]: 195 ret["result"] = None 196 ret["comment"] = "The deployment is going to be created" 197 return ret 198 res = __salt__["kubernetes.create_deployment"]( 199 name=name, 200 namespace=namespace, 201 metadata=metadata, 202 spec=spec, 203 source=source, 204 template=template, 205 saltenv=__env__, 206 **kwargs 207 ) 208 ret["changes"]["{}.{}".format(namespace, name)] = {"old": {}, "new": res} 209 else: 210 if __opts__["test"]: 211 ret["result"] = None 212 return ret 213 214 # TODO: improve checks # pylint: disable=fixme 215 log.info("Forcing the recreation of the deployment") 216 ret["comment"] = "The deployment is already present. Forcing recreation" 217 res = __salt__["kubernetes.replace_deployment"]( 218 name=name, 219 namespace=namespace, 220 metadata=metadata, 221 spec=spec, 222 source=source, 223 template=template, 224 saltenv=__env__, 225 **kwargs 226 ) 227 228 ret["changes"] = {"metadata": metadata, "spec": spec} 229 ret["result"] = True 230 return ret 231 232 233def service_present( 234 name, 235 namespace="default", 236 metadata=None, 237 spec=None, 238 source="", 239 template="", 240 **kwargs 241): 242 """ 243 Ensures that the named service is present inside of the specified namespace 244 with the given metadata and spec. 245 If the deployment exists it will be replaced. 246 247 name 248 The name of the service. 249 250 namespace 251 The namespace holding the service. The 'default' one is going to be 252 used unless a different one is specified. 253 254 metadata 255 The metadata of the service object. 256 257 spec 258 The spec of the service object. 259 260 source 261 A file containing the definition of the service (metadata and 262 spec) in the official kubernetes format. 263 264 template 265 Template engine to be used to render the source file. 266 """ 267 ret = {"name": name, "changes": {}, "result": False, "comment": ""} 268 269 if (metadata or spec) and source: 270 return _error( 271 ret, "'source' cannot be used in combination with 'metadata' or 'spec'" 272 ) 273 274 if metadata is None: 275 metadata = {} 276 277 if spec is None: 278 spec = {} 279 280 service = __salt__["kubernetes.show_service"](name, namespace, **kwargs) 281 282 if service is None: 283 if __opts__["test"]: 284 ret["result"] = None 285 ret["comment"] = "The service is going to be created" 286 return ret 287 res = __salt__["kubernetes.create_service"]( 288 name=name, 289 namespace=namespace, 290 metadata=metadata, 291 spec=spec, 292 source=source, 293 template=template, 294 saltenv=__env__, 295 **kwargs 296 ) 297 ret["changes"]["{}.{}".format(namespace, name)] = {"old": {}, "new": res} 298 else: 299 if __opts__["test"]: 300 ret["result"] = None 301 return ret 302 303 # TODO: improve checks # pylint: disable=fixme 304 log.info("Forcing the recreation of the service") 305 ret["comment"] = "The service is already present. Forcing recreation" 306 res = __salt__["kubernetes.replace_service"]( 307 name=name, 308 namespace=namespace, 309 metadata=metadata, 310 spec=spec, 311 source=source, 312 template=template, 313 old_service=service, 314 saltenv=__env__, 315 **kwargs 316 ) 317 318 ret["changes"] = {"metadata": metadata, "spec": spec} 319 ret["result"] = True 320 return ret 321 322 323def service_absent(name, namespace="default", **kwargs): 324 """ 325 Ensures that the named service is absent from the given namespace. 326 327 name 328 The name of the service 329 330 namespace 331 The name of the namespace 332 """ 333 334 ret = {"name": name, "changes": {}, "result": False, "comment": ""} 335 336 service = __salt__["kubernetes.show_service"](name, namespace, **kwargs) 337 338 if service is None: 339 ret["result"] = True if not __opts__["test"] else None 340 ret["comment"] = "The service does not exist" 341 return ret 342 343 if __opts__["test"]: 344 ret["comment"] = "The service is going to be deleted" 345 ret["result"] = None 346 return ret 347 348 res = __salt__["kubernetes.delete_service"](name, namespace, **kwargs) 349 if res["code"] == 200: 350 ret["result"] = True 351 ret["changes"] = {"kubernetes.service": {"new": "absent", "old": "present"}} 352 ret["comment"] = res["message"] 353 else: 354 ret["comment"] = "Something went wrong, response: {}".format(res) 355 356 return ret 357 358 359def namespace_absent(name, **kwargs): 360 """ 361 Ensures that the named namespace is absent. 362 363 name 364 The name of the namespace 365 """ 366 367 ret = {"name": name, "changes": {}, "result": False, "comment": ""} 368 369 namespace = __salt__["kubernetes.show_namespace"](name, **kwargs) 370 371 if namespace is None: 372 ret["result"] = True if not __opts__["test"] else None 373 ret["comment"] = "The namespace does not exist" 374 return ret 375 376 if __opts__["test"]: 377 ret["comment"] = "The namespace is going to be deleted" 378 ret["result"] = None 379 return ret 380 381 res = __salt__["kubernetes.delete_namespace"](name, **kwargs) 382 if ( 383 res["code"] == 200 384 or (isinstance(res["status"], str) and "Terminating" in res["status"]) 385 or (isinstance(res["status"], dict) and res["status"]["phase"] == "Terminating") 386 ): 387 ret["result"] = True 388 ret["changes"] = {"kubernetes.namespace": {"new": "absent", "old": "present"}} 389 if res["message"]: 390 ret["comment"] = res["message"] 391 else: 392 ret["comment"] = "Terminating" 393 else: 394 ret["comment"] = "Something went wrong, response: {}".format(res) 395 396 return ret 397 398 399def namespace_present(name, **kwargs): 400 """ 401 Ensures that the named namespace is present. 402 403 name 404 The name of the namespace. 405 406 """ 407 ret = {"name": name, "changes": {}, "result": False, "comment": ""} 408 409 namespace = __salt__["kubernetes.show_namespace"](name, **kwargs) 410 411 if namespace is None: 412 if __opts__["test"]: 413 ret["result"] = None 414 ret["comment"] = "The namespace is going to be created" 415 return ret 416 417 res = __salt__["kubernetes.create_namespace"](name, **kwargs) 418 ret["result"] = True 419 ret["changes"]["namespace"] = {"old": {}, "new": res} 420 else: 421 ret["result"] = True if not __opts__["test"] else None 422 ret["comment"] = "The namespace already exists" 423 424 return ret 425 426 427def secret_absent(name, namespace="default", **kwargs): 428 """ 429 Ensures that the named secret is absent from the given namespace. 430 431 name 432 The name of the secret 433 434 namespace 435 The name of the namespace 436 """ 437 438 ret = {"name": name, "changes": {}, "result": False, "comment": ""} 439 440 secret = __salt__["kubernetes.show_secret"](name, namespace, **kwargs) 441 442 if secret is None: 443 ret["result"] = True if not __opts__["test"] else None 444 ret["comment"] = "The secret does not exist" 445 return ret 446 447 if __opts__["test"]: 448 ret["comment"] = "The secret is going to be deleted" 449 ret["result"] = None 450 return ret 451 452 __salt__["kubernetes.delete_secret"](name, namespace, **kwargs) 453 454 # As for kubernetes 1.6.4 doesn't set a code when deleting a secret 455 # The kubernetes module will raise an exception if the kubernetes 456 # server will return an error 457 ret["result"] = True 458 ret["changes"] = {"kubernetes.secret": {"new": "absent", "old": "present"}} 459 ret["comment"] = "Secret deleted" 460 return ret 461 462 463def secret_present( 464 name, namespace="default", data=None, source=None, template=None, **kwargs 465): 466 """ 467 Ensures that the named secret is present inside of the specified namespace 468 with the given data. 469 If the secret exists it will be replaced. 470 471 name 472 The name of the secret. 473 474 namespace 475 The namespace holding the secret. The 'default' one is going to be 476 used unless a different one is specified. 477 478 data 479 The dictionary holding the secrets. 480 481 source 482 A file containing the data of the secret in plain format. 483 484 template 485 Template engine to be used to render the source file. 486 """ 487 ret = {"name": name, "changes": {}, "result": False, "comment": ""} 488 489 if data and source: 490 return _error(ret, "'source' cannot be used in combination with 'data'") 491 492 secret = __salt__["kubernetes.show_secret"](name, namespace, **kwargs) 493 494 if secret is None: 495 if data is None: 496 data = {} 497 498 if __opts__["test"]: 499 ret["result"] = None 500 ret["comment"] = "The secret is going to be created" 501 return ret 502 res = __salt__["kubernetes.create_secret"]( 503 name=name, 504 namespace=namespace, 505 data=data, 506 source=source, 507 template=template, 508 saltenv=__env__, 509 **kwargs 510 ) 511 ret["changes"]["{}.{}".format(namespace, name)] = {"old": {}, "new": res} 512 else: 513 if __opts__["test"]: 514 ret["result"] = None 515 ret["comment"] = "The secret is going to be replaced" 516 return ret 517 518 # TODO: improve checks # pylint: disable=fixme 519 log.info("Forcing the recreation of the service") 520 ret["comment"] = "The secret is already present. Forcing recreation" 521 res = __salt__["kubernetes.replace_secret"]( 522 name=name, 523 namespace=namespace, 524 data=data, 525 source=source, 526 template=template, 527 saltenv=__env__, 528 **kwargs 529 ) 530 531 ret["changes"] = { 532 # Omit values from the return. They are unencrypted 533 # and can contain sensitive data. 534 "data": list(res["data"]) 535 } 536 ret["result"] = True 537 538 return ret 539 540 541def configmap_absent(name, namespace="default", **kwargs): 542 """ 543 Ensures that the named configmap is absent from the given namespace. 544 545 name 546 The name of the configmap 547 548 namespace 549 The namespace holding the configmap. The 'default' one is going to be 550 used unless a different one is specified. 551 """ 552 553 ret = {"name": name, "changes": {}, "result": False, "comment": ""} 554 555 configmap = __salt__["kubernetes.show_configmap"](name, namespace, **kwargs) 556 557 if configmap is None: 558 ret["result"] = True if not __opts__["test"] else None 559 ret["comment"] = "The configmap does not exist" 560 return ret 561 562 if __opts__["test"]: 563 ret["comment"] = "The configmap is going to be deleted" 564 ret["result"] = None 565 return ret 566 567 __salt__["kubernetes.delete_configmap"](name, namespace, **kwargs) 568 # As for kubernetes 1.6.4 doesn't set a code when deleting a configmap 569 # The kubernetes module will raise an exception if the kubernetes 570 # server will return an error 571 ret["result"] = True 572 ret["changes"] = {"kubernetes.configmap": {"new": "absent", "old": "present"}} 573 ret["comment"] = "ConfigMap deleted" 574 575 return ret 576 577 578def configmap_present( 579 name, namespace="default", data=None, source=None, template=None, **kwargs 580): 581 """ 582 Ensures that the named configmap is present inside of the specified namespace 583 with the given data. 584 If the configmap exists it will be replaced. 585 586 name 587 The name of the configmap. 588 589 namespace 590 The namespace holding the configmap. The 'default' one is going to be 591 used unless a different one is specified. 592 593 data 594 The dictionary holding the configmaps. 595 596 source 597 A file containing the data of the configmap in plain format. 598 599 template 600 Template engine to be used to render the source file. 601 """ 602 ret = {"name": name, "changes": {}, "result": False, "comment": ""} 603 604 if data and source: 605 return _error(ret, "'source' cannot be used in combination with 'data'") 606 elif data is None: 607 data = {} 608 609 configmap = __salt__["kubernetes.show_configmap"](name, namespace, **kwargs) 610 611 if configmap is None: 612 if __opts__["test"]: 613 ret["result"] = None 614 ret["comment"] = "The configmap is going to be created" 615 return ret 616 res = __salt__["kubernetes.create_configmap"]( 617 name=name, 618 namespace=namespace, 619 data=data, 620 source=source, 621 template=template, 622 saltenv=__env__, 623 **kwargs 624 ) 625 ret["changes"]["{}.{}".format(namespace, name)] = {"old": {}, "new": res} 626 else: 627 if __opts__["test"]: 628 ret["result"] = None 629 ret["comment"] = "The configmap is going to be replaced" 630 return ret 631 632 # TODO: improve checks # pylint: disable=fixme 633 log.info("Forcing the recreation of the service") 634 ret["comment"] = "The configmap is already present. Forcing recreation" 635 res = __salt__["kubernetes.replace_configmap"]( 636 name=name, 637 namespace=namespace, 638 data=data, 639 source=source, 640 template=template, 641 saltenv=__env__, 642 **kwargs 643 ) 644 645 ret["changes"] = {"data": res["data"]} 646 ret["result"] = True 647 return ret 648 649 650def pod_absent(name, namespace="default", **kwargs): 651 """ 652 Ensures that the named pod is absent from the given namespace. 653 654 name 655 The name of the pod 656 657 namespace 658 The name of the namespace 659 """ 660 661 ret = {"name": name, "changes": {}, "result": False, "comment": ""} 662 663 pod = __salt__["kubernetes.show_pod"](name, namespace, **kwargs) 664 665 if pod is None: 666 ret["result"] = True if not __opts__["test"] else None 667 ret["comment"] = "The pod does not exist" 668 return ret 669 670 if __opts__["test"]: 671 ret["comment"] = "The pod is going to be deleted" 672 ret["result"] = None 673 return ret 674 675 res = __salt__["kubernetes.delete_pod"](name, namespace, **kwargs) 676 if res["code"] == 200 or res["code"] is None: 677 ret["result"] = True 678 ret["changes"] = {"kubernetes.pod": {"new": "absent", "old": "present"}} 679 if res["code"] is None: 680 ret["comment"] = "In progress" 681 else: 682 ret["comment"] = res["message"] 683 else: 684 ret["comment"] = "Something went wrong, response: {}".format(res) 685 686 return ret 687 688 689def pod_present( 690 name, 691 namespace="default", 692 metadata=None, 693 spec=None, 694 source="", 695 template="", 696 **kwargs 697): 698 """ 699 Ensures that the named pod is present inside of the specified 700 namespace with the given metadata and spec. 701 If the pod exists it will be replaced. 702 703 name 704 The name of the pod. 705 706 namespace 707 The namespace holding the pod. The 'default' one is going to be 708 used unless a different one is specified. 709 710 metadata 711 The metadata of the pod object. 712 713 spec 714 The spec of the pod object. 715 716 source 717 A file containing the definition of the pod (metadata and 718 spec) in the official kubernetes format. 719 720 template 721 Template engine to be used to render the source file. 722 """ 723 ret = {"name": name, "changes": {}, "result": False, "comment": ""} 724 725 if (metadata or spec) and source: 726 return _error( 727 ret, "'source' cannot be used in combination with 'metadata' or 'spec'" 728 ) 729 730 if metadata is None: 731 metadata = {} 732 733 if spec is None: 734 spec = {} 735 736 pod = __salt__["kubernetes.show_pod"](name, namespace, **kwargs) 737 738 if pod is None: 739 if __opts__["test"]: 740 ret["result"] = None 741 ret["comment"] = "The pod is going to be created" 742 return ret 743 res = __salt__["kubernetes.create_pod"]( 744 name=name, 745 namespace=namespace, 746 metadata=metadata, 747 spec=spec, 748 source=source, 749 template=template, 750 saltenv=__env__, 751 **kwargs 752 ) 753 ret["changes"]["{}.{}".format(namespace, name)] = {"old": {}, "new": res} 754 else: 755 if __opts__["test"]: 756 ret["result"] = None 757 return ret 758 759 # TODO: fix replace_namespaced_pod validation issues 760 ret["comment"] = ( 761 "salt is currently unable to replace a pod without " 762 "deleting it. Please perform the removal of the pod requiring " 763 "the 'pod_absent' state if this is the desired behaviour." 764 ) 765 ret["result"] = False 766 return ret 767 768 ret["changes"] = {"metadata": metadata, "spec": spec} 769 ret["result"] = True 770 return ret 771 772 773def node_label_absent(name, node, **kwargs): 774 """ 775 Ensures that the named label is absent from the node. 776 777 name 778 The name of the label 779 780 node 781 The name of the node 782 """ 783 784 ret = {"name": name, "changes": {}, "result": False, "comment": ""} 785 786 labels = __salt__["kubernetes.node_labels"](node, **kwargs) 787 788 if name not in labels: 789 ret["result"] = True if not __opts__["test"] else None 790 ret["comment"] = "The label does not exist" 791 return ret 792 793 if __opts__["test"]: 794 ret["comment"] = "The label is going to be deleted" 795 ret["result"] = None 796 return ret 797 798 __salt__["kubernetes.node_remove_label"](node_name=node, label_name=name, **kwargs) 799 800 ret["result"] = True 801 ret["changes"] = {"kubernetes.node_label": {"new": "absent", "old": "present"}} 802 ret["comment"] = "Label removed from node" 803 804 return ret 805 806 807def node_label_folder_absent(name, node, **kwargs): 808 """ 809 Ensures the label folder doesn't exist on the specified node. 810 811 name 812 The name of label folder 813 814 node 815 The name of the node 816 """ 817 818 ret = {"name": name, "changes": {}, "result": False, "comment": ""} 819 labels = __salt__["kubernetes.node_labels"](node, **kwargs) 820 821 folder = name.strip("/") + "/" 822 labels_to_drop = [] 823 new_labels = [] 824 for label in labels: 825 if label.startswith(folder): 826 labels_to_drop.append(label) 827 else: 828 new_labels.append(label) 829 830 if not labels_to_drop: 831 ret["result"] = True if not __opts__["test"] else None 832 ret["comment"] = "The label folder does not exist" 833 return ret 834 835 if __opts__["test"]: 836 ret["comment"] = "The label folder is going to be deleted" 837 ret["result"] = None 838 return ret 839 840 for label in labels_to_drop: 841 __salt__["kubernetes.node_remove_label"]( 842 node_name=node, label_name=label, **kwargs 843 ) 844 845 ret["result"] = True 846 ret["changes"] = { 847 "kubernetes.node_label_folder_absent": {"old": list(labels), "new": new_labels} 848 } 849 ret["comment"] = "Label folder removed from node" 850 851 return ret 852 853 854def node_label_present(name, node, value, **kwargs): 855 """ 856 Ensures that the named label is set on the named node 857 with the given value. 858 If the label exists it will be replaced. 859 860 name 861 The name of the label. 862 863 value 864 Value of the label. 865 866 node 867 Node to change. 868 """ 869 ret = {"name": name, "changes": {}, "result": False, "comment": ""} 870 871 labels = __salt__["kubernetes.node_labels"](node, **kwargs) 872 873 if name not in labels: 874 if __opts__["test"]: 875 ret["result"] = None 876 ret["comment"] = "The label is going to be set" 877 return ret 878 __salt__["kubernetes.node_add_label"]( 879 label_name=name, label_value=value, node_name=node, **kwargs 880 ) 881 elif labels[name] == value: 882 ret["result"] = True 883 ret["comment"] = "The label is already set and has the specified value" 884 return ret 885 else: 886 if __opts__["test"]: 887 ret["result"] = None 888 ret["comment"] = "The label is going to be updated" 889 return ret 890 891 ret["comment"] = "The label is already set, changing the value" 892 __salt__["kubernetes.node_add_label"]( 893 node_name=node, label_name=name, label_value=value, **kwargs 894 ) 895 896 old_labels = copy.copy(labels) 897 labels[name] = value 898 899 ret["changes"]["{}.{}".format(node, name)] = {"old": old_labels, "new": labels} 900 ret["result"] = True 901 902 return ret 903