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