1"""
2Module to import docker-compose via saltstack
3
4.. versionadded:: 2016.3.0
5
6:maintainer: Jean Praloran <jeanpralo@gmail.com>
7:maturity: new
8:depends: docker-compose>=1.5
9:platform: all
10
11Introduction
12------------
13This module allows one to deal with docker-compose file in a directory.
14
15This is  a first version only, the following commands are missing at the moment
16but will be built later on if the community is interested in this module:
17
18- run
19- logs
20- port
21- scale
22
23Installation Prerequisites
24--------------------------
25
26This execution module requires at least version 1.4.0 of both docker-compose_ and
27Docker_. docker-compose can easily be installed using :py:func:`pip.install
28<salt.modules.pip.install>`:
29
30.. code-block:: bash
31
32    salt myminion pip.install docker-compose>=1.5.0
33
34.. _docker-compose: https://pypi.python.org/pypi/docker-compose
35.. _Docker: https://www.docker.com/
36
37
38How to use this module?
39-----------------------
40In order to use the module if you have no docker-compose file on the server you
41can issue the command create, it takes two arguments the path where the
42docker-compose.yml will be stored and the content of this latter:
43
44.. code-block:: text
45
46    # salt-call -l debug dockercompose.create /tmp/toto '
47    database:
48    image: mongo:3.0
49    command: mongod --smallfiles --quiet --logpath=/dev/null
50    '
51
52Then you can execute a list of method defined at the bottom with at least one
53argument (the path where the docker-compose.yml will be read) and an optional
54python list which corresponds to the services names:
55
56.. code-block:: bash
57
58    # salt-call -l debug dockercompose.up /tmp/toto
59    # salt-call -l debug dockercompose.restart /tmp/toto '[database]'
60    # salt-call -l debug dockercompose.stop /tmp/toto
61    # salt-call -l debug dockercompose.rm /tmp/toto
62
63Docker-compose method supported
64-------------------------------
65- up
66- restart
67- stop
68- start
69- pause
70- unpause
71- kill
72- rm
73- ps
74- pull
75- build
76
77Functions
78---------
79- docker-compose.yml management
80    - :py:func:`dockercompose.create <salt.modules.dockercompose.create>`
81    - :py:func:`dockercompose.get <salt.modules.dockercompose.get>`
82- Manage containers
83    - :py:func:`dockercompose.restart <salt.modules.dockercompose.restart>`
84    - :py:func:`dockercompose.stop <salt.modules.dockercompose.stop>`
85    - :py:func:`dockercompose.pause <salt.modules.dockercompose.pause>`
86    - :py:func:`dockercompose.unpause <salt.modules.dockercompose.unpause>`
87    - :py:func:`dockercompose.start <salt.modules.dockercompose.start>`
88    - :py:func:`dockercompose.kill <salt.modules.dockercompose.kill>`
89    - :py:func:`dockercompose.rm <salt.modules.dockercompose.rm>`
90    - :py:func:`dockercompose.up <salt.modules.dockercompose.up>`
91- Manage containers image:
92    - :py:func:`dockercompose.pull <salt.modules.dockercompose.pull>`
93    - :py:func:`dockercompose.build <salt.modules.dockercompose.build>`
94- Gather information about containers:
95    - :py:func:`dockercompose.ps <salt.modules.dockercompose.ps>`
96- Manage service definitions:
97    - :py:func:`dockercompose.service_create <salt.modules.dockercompose.ps>`
98    - :py:func:`dockercompose.service_upsert <salt.modules.dockercompose.ps>`
99    - :py:func:`dockercompose.service_remove <salt.modules.dockercompose.ps>`
100    - :py:func:`dockercompose.service_set_tag <salt.modules.dockercompose.ps>`
101
102Detailed Function Documentation
103-------------------------------
104"""
105
106
107import inspect
108import logging
109import os
110import re
111from operator import attrgetter
112
113import salt.utils.files
114import salt.utils.stringutils
115from salt.serializers import json
116from salt.utils import yaml
117
118try:
119    import compose
120    from compose.cli.command import get_project
121    from compose.service import ConvergenceStrategy
122
123    HAS_DOCKERCOMPOSE = True
124except ImportError:
125    HAS_DOCKERCOMPOSE = False
126
127try:
128    from compose.project import OneOffFilter
129
130    USE_FILTERCLASS = True
131except ImportError:
132    USE_FILTERCLASS = False
133
134MIN_DOCKERCOMPOSE = (1, 5, 0)
135VERSION_RE = r"([\d.]+)"
136
137log = logging.getLogger(__name__)
138debug = False
139
140__virtualname__ = "dockercompose"
141DEFAULT_DC_FILENAMES = ("docker-compose.yml", "docker-compose.yaml")
142
143
144def __virtual__():
145    if HAS_DOCKERCOMPOSE:
146        match = re.match(VERSION_RE, str(compose.__version__))
147        if match:
148            version = tuple([int(x) for x in match.group(1).split(".")])
149            if version >= MIN_DOCKERCOMPOSE:
150                return __virtualname__
151    return (
152        False,
153        "The dockercompose execution module not loaded: "
154        "compose python library not available.",
155    )
156
157
158def __standardize_result(status, message, data=None, debug_msg=None):
159    """
160    Standardizes all responses
161
162    :param status:
163    :param message:
164    :param data:
165    :param debug_msg:
166    :return:
167    """
168    result = {"status": status, "message": message}
169
170    if data is not None:
171        result["return"] = data
172
173    if debug_msg is not None and debug:
174        result["debug"] = debug_msg
175
176    return result
177
178
179def __get_docker_file_path(path):
180    """
181    Determines the filepath to use
182
183    :param path:
184    :return:
185    """
186    if os.path.isfile(path):
187        return path
188    for dc_filename in DEFAULT_DC_FILENAMES:
189        file_path = os.path.join(path, dc_filename)
190        if os.path.isfile(file_path):
191            return file_path
192    # implicitly return None
193
194
195def __read_docker_compose_file(file_path):
196    """
197    Read the compose file if it exists in the directory
198
199    :param file_path:
200    :return:
201    """
202    if not os.path.isfile(file_path):
203        return __standardize_result(
204            False, "Path {} is not present".format(file_path), None, None
205        )
206    try:
207        with salt.utils.files.fopen(file_path, "r") as fl:
208            file_name = os.path.basename(file_path)
209            result = {file_name: ""}
210            for line in fl:
211                result[file_name] += salt.utils.stringutils.to_unicode(line)
212    except OSError:
213        return __standardize_result(
214            False, "Could not read {}".format(file_path), None, None
215        )
216    return __standardize_result(
217        True, "Reading content of {}".format(file_path), result, None
218    )
219
220
221def __load_docker_compose(path):
222    """
223    Read the compose file and load its' contents
224
225    :param path:
226    :return:
227    """
228    file_path = __get_docker_file_path(path)
229    if file_path is None:
230        msg = "Could not find docker-compose file at {}".format(path)
231        return None, __standardize_result(False, msg, None, None)
232    if not os.path.isfile(file_path):
233        return (
234            None,
235            __standardize_result(
236                False, "Path {} is not present".format(file_path), None, None
237            ),
238        )
239    try:
240        with salt.utils.files.fopen(file_path, "r") as fl:
241            loaded = yaml.load(fl)
242    except OSError:
243        return (
244            None,
245            __standardize_result(
246                False, "Could not read {}".format(file_path), None, None
247            ),
248        )
249    except yaml.YAMLError as yerr:
250        msg = "Could not parse {} {}".format(file_path, yerr)
251        return None, __standardize_result(False, msg, None, None)
252    if not loaded:
253        msg = "Got empty compose file at {}".format(file_path)
254        return None, __standardize_result(False, msg, None, None)
255    if "services" not in loaded:
256        loaded["services"] = {}
257    result = {"compose_content": loaded, "file_name": os.path.basename(file_path)}
258    return result, None
259
260
261def __dump_docker_compose(path, content, already_existed):
262    """
263    Dumps
264
265    :param path:
266    :param content: the not-yet dumped content
267    :return:
268    """
269    try:
270        dumped = yaml.safe_dump(content, indent=2, default_flow_style=False)
271        return __write_docker_compose(path, dumped, already_existed)
272    except TypeError as t_err:
273        msg = "Could not dump {} {}".format(content, t_err)
274        return __standardize_result(False, msg, None, None)
275
276
277def __write_docker_compose(path, docker_compose, already_existed):
278    """
279    Write docker-compose to a path
280    in order to use it with docker-compose ( config check )
281
282    :param path:
283
284    docker_compose
285        contains the docker-compose file
286
287    :return:
288    """
289    if path.lower().endswith((".yml", ".yaml")):
290        file_path = path
291        dir_name = os.path.dirname(path)
292    else:
293        dir_name = path
294        file_path = os.path.join(dir_name, DEFAULT_DC_FILENAMES[0])
295    if os.path.isdir(dir_name) is False:
296        os.mkdir(dir_name)
297    try:
298        with salt.utils.files.fopen(file_path, "w") as fl:
299            fl.write(salt.utils.stringutils.to_str(docker_compose))
300    except OSError:
301        return __standardize_result(
302            False, "Could not write {}".format(file_path), None, None
303        )
304    project = __load_project_from_file_path(file_path)
305    if isinstance(project, dict):
306        if not already_existed:
307            os.remove(file_path)
308        return project
309    return file_path
310
311
312def __load_project(path):
313    """
314    Load a docker-compose project from path
315
316    :param path:
317    :return:
318    """
319    file_path = __get_docker_file_path(path)
320    if file_path is None:
321        msg = "Could not find docker-compose file at {}".format(path)
322        return __standardize_result(False, msg, None, None)
323    return __load_project_from_file_path(file_path)
324
325
326def __load_project_from_file_path(file_path):
327    """
328    Load a docker-compose project from file path
329
330    :param path:
331    :return:
332    """
333    try:
334        project = get_project(
335            project_dir=os.path.dirname(file_path),
336            config_path=[os.path.basename(file_path)],
337        )
338    except Exception as inst:  # pylint: disable=broad-except
339        return __handle_except(inst)
340    return project
341
342
343def __load_compose_definitions(path, definition):
344    """
345    Will load the compose file located at path
346    Then determines the format/contents of the sent definition
347
348    err or results are only set if there were any
349
350    :param path:
351    :param definition:
352    :return tuple(compose_result, loaded_definition, err):
353    """
354    compose_result, err = __load_docker_compose(path)
355    if err:
356        return None, None, err
357    if isinstance(definition, dict):
358        return compose_result, definition, None
359    elif definition.strip().startswith("{"):
360        try:
361            loaded_definition = json.deserialize(definition)
362        except json.DeserializationError as jerr:
363            msg = "Could not parse {} {}".format(definition, jerr)
364            return None, None, __standardize_result(False, msg, None, None)
365    else:
366        try:
367            loaded_definition = yaml.load(definition)
368        except yaml.YAMLError as yerr:
369            msg = "Could not parse {} {}".format(definition, yerr)
370            return None, None, __standardize_result(False, msg, None, None)
371    return compose_result, loaded_definition, None
372
373
374def __dump_compose_file(path, compose_result, success_msg, already_existed):
375    """
376    Utility function to dump the compose result to a file.
377
378    :param path:
379    :param compose_result:
380    :param success_msg: the message to give upon success
381    :return:
382    """
383    ret = __dump_docker_compose(
384        path, compose_result["compose_content"], already_existed
385    )
386    if isinstance(ret, dict):
387        return ret
388    return __standardize_result(
389        True, success_msg, compose_result["compose_content"], None
390    )
391
392
393def __handle_except(inst):
394    """
395    Handle exception and return a standard result
396
397    :param inst:
398    :return:
399    """
400    return __standardize_result(
401        False,
402        "Docker-compose command {} failed".format(inspect.stack()[1][3]),
403        "{}".format(inst),
404        None,
405    )
406
407
408def _get_convergence_plans(project, service_names):
409    """
410    Get action executed for each container
411
412    :param project:
413    :param service_names:
414    :return:
415    """
416    ret = {}
417    plans = project._get_convergence_plans(
418        project.get_services(service_names), ConvergenceStrategy.changed
419    )
420    for cont in plans:
421        (action, container) = plans[cont]
422        if action == "create":
423            ret[cont] = "Creating container"
424        elif action == "recreate":
425            ret[cont] = "Re-creating container"
426        elif action == "start":
427            ret[cont] = "Starting container"
428        elif action == "noop":
429            ret[cont] = "Container is up to date"
430    return ret
431
432
433def get(path):
434    """
435    Get the content of the docker-compose file into a directory
436
437    path
438        Path where the docker-compose file is stored on the server
439
440    CLI Example:
441
442    .. code-block:: bash
443
444        salt myminion dockercompose.get /path/where/docker-compose/stored
445    """
446    file_path = __get_docker_file_path(path)
447    if file_path is None:
448        return __standardize_result(
449            False, "Path {} is not present".format(path), None, None
450        )
451    salt_result = __read_docker_compose_file(file_path)
452    if not salt_result["status"]:
453        return salt_result
454    project = __load_project(path)
455    if isinstance(project, dict):
456        salt_result["return"]["valid"] = False
457    else:
458        salt_result["return"]["valid"] = True
459    return salt_result
460
461
462def create(path, docker_compose):
463    """
464    Create and validate a docker-compose file into a directory
465
466    path
467        Path where the docker-compose file will be stored on the server
468
469    docker_compose
470        docker_compose file
471
472    CLI Example:
473
474    .. code-block:: bash
475
476        salt myminion dockercompose.create /path/where/docker-compose/stored content
477    """
478    if docker_compose:
479        ret = __write_docker_compose(path, docker_compose, already_existed=False)
480        if isinstance(ret, dict):
481            return ret
482    else:
483        return __standardize_result(
484            False,
485            "Creating a docker-compose project failed, you must send a valid"
486            " docker-compose file",
487            None,
488            None,
489        )
490    return __standardize_result(
491        True,
492        "Successfully created the docker-compose file",
493        {"compose.base_dir": path},
494        None,
495    )
496
497
498def pull(path, service_names=None):
499    """
500    Pull image for containers in the docker-compose file, service_names is a
501    python list, if omitted pull all images
502
503    path
504        Path where the docker-compose file is stored on the server
505    service_names
506        If specified will pull only the image for the specified services
507
508    CLI Example:
509
510    .. code-block:: bash
511
512        salt myminion dockercompose.pull /path/where/docker-compose/stored
513        salt myminion dockercompose.pull /path/where/docker-compose/stored '[janus]'
514    """
515
516    project = __load_project(path)
517    if isinstance(project, dict):
518        return project
519    else:
520        try:
521            project.pull(service_names)
522        except Exception as inst:  # pylint: disable=broad-except
523            return __handle_except(inst)
524    return __standardize_result(
525        True, "Pulling containers images via docker-compose succeeded", None, None
526    )
527
528
529def build(path, service_names=None):
530    """
531    Build image for containers in the docker-compose file, service_names is a
532    python list, if omitted build images for all containers. Please note
533    that at the moment the module does not allow you to upload your Dockerfile,
534    nor any other file you could need with your docker-compose.yml, you will
535    have to make sure the files you need are actually in the directory specified
536    in the `build` keyword
537
538    path
539        Path where the docker-compose file is stored on the server
540    service_names
541        If specified will pull only the image for the specified services
542
543    CLI Example:
544
545    .. code-block:: bash
546
547        salt myminion dockercompose.build /path/where/docker-compose/stored
548        salt myminion dockercompose.build /path/where/docker-compose/stored '[janus]'
549    """
550
551    project = __load_project(path)
552    if isinstance(project, dict):
553        return project
554    else:
555        try:
556            project.build(service_names)
557        except Exception as inst:  # pylint: disable=broad-except
558            return __handle_except(inst)
559    return __standardize_result(
560        True, "Building containers images via docker-compose succeeded", None, None
561    )
562
563
564def restart(path, service_names=None):
565    """
566    Restart container(s) in the docker-compose file, service_names is a python
567    list, if omitted restart all containers
568
569    path
570        Path where the docker-compose file is stored on the server
571
572    service_names
573        If specified will restart only the specified services
574
575    CLI Example:
576
577    .. code-block:: bash
578
579        salt myminion dockercompose.restart /path/where/docker-compose/stored
580        salt myminion dockercompose.restart /path/where/docker-compose/stored '[janus]'
581    """
582
583    project = __load_project(path)
584    debug_ret = {}
585    result = {}
586    if isinstance(project, dict):
587        return project
588    else:
589        try:
590            project.restart(service_names)
591            if debug:
592                for container in project.containers():
593                    if (
594                        service_names is None
595                        or container.get("Name")[1:] in service_names
596                    ):
597                        container.inspect_if_not_inspected()
598                        debug_ret[container.get("Name")] = container.inspect()
599                        result[container.get("Name")] = "restarted"
600        except Exception as inst:  # pylint: disable=broad-except
601            return __handle_except(inst)
602    return __standardize_result(
603        True, "Restarting containers via docker-compose", result, debug_ret
604    )
605
606
607def stop(path, service_names=None):
608    """
609    Stop running containers in the docker-compose file, service_names is a python
610    list, if omitted stop all containers
611
612    path
613        Path where the docker-compose file is stored on the server
614    service_names
615        If specified will stop only the specified services
616
617    CLI Example:
618
619    .. code-block:: bash
620
621        salt myminion dockercompose.stop /path/where/docker-compose/stored
622        salt myminion dockercompose.stop  /path/where/docker-compose/stored '[janus]'
623    """
624
625    project = __load_project(path)
626    debug_ret = {}
627    result = {}
628    if isinstance(project, dict):
629        return project
630    else:
631        try:
632            project.stop(service_names)
633            if debug:
634                for container in project.containers(stopped=True):
635                    if (
636                        service_names is None
637                        or container.get("Name")[1:] in service_names
638                    ):
639                        container.inspect_if_not_inspected()
640                        debug_ret[container.get("Name")] = container.inspect()
641                        result[container.get("Name")] = "stopped"
642        except Exception as inst:  # pylint: disable=broad-except
643            return __handle_except(inst)
644    return __standardize_result(
645        True, "Stopping containers via docker-compose", result, debug_ret
646    )
647
648
649def pause(path, service_names=None):
650    """
651    Pause running containers in the docker-compose file, service_names is a python
652    list, if omitted pause all containers
653
654    path
655        Path where the docker-compose file is stored on the server
656    service_names
657        If specified will pause only the specified services
658
659    CLI Example:
660
661    .. code-block:: bash
662
663        salt myminion dockercompose.pause /path/where/docker-compose/stored
664        salt myminion dockercompose.pause /path/where/docker-compose/stored '[janus]'
665    """
666
667    project = __load_project(path)
668    debug_ret = {}
669    result = {}
670    if isinstance(project, dict):
671        return project
672    else:
673        try:
674            project.pause(service_names)
675            if debug:
676                for container in project.containers():
677                    if (
678                        service_names is None
679                        or container.get("Name")[1:] in service_names
680                    ):
681                        container.inspect_if_not_inspected()
682                        debug_ret[container.get("Name")] = container.inspect()
683                        result[container.get("Name")] = "paused"
684        except Exception as inst:  # pylint: disable=broad-except
685            return __handle_except(inst)
686    return __standardize_result(
687        True, "Pausing containers via docker-compose", result, debug_ret
688    )
689
690
691def unpause(path, service_names=None):
692    """
693    Un-Pause containers in the docker-compose file, service_names is a python
694    list, if omitted unpause all containers
695
696    path
697        Path where the docker-compose file is stored on the server
698    service_names
699        If specified will un-pause only the specified services
700
701    CLI Example:
702
703    .. code-block:: bash
704
705        salt myminion dockercompose.pause /path/where/docker-compose/stored
706        salt myminion dockercompose.pause /path/where/docker-compose/stored '[janus]'
707    """
708
709    project = __load_project(path)
710    debug_ret = {}
711    result = {}
712    if isinstance(project, dict):
713        return project
714    else:
715        try:
716            project.unpause(service_names)
717            if debug:
718                for container in project.containers():
719                    if (
720                        service_names is None
721                        or container.get("Name")[1:] in service_names
722                    ):
723                        container.inspect_if_not_inspected()
724                        debug_ret[container.get("Name")] = container.inspect()
725                        result[container.get("Name")] = "unpaused"
726        except Exception as inst:  # pylint: disable=broad-except
727            return __handle_except(inst)
728    return __standardize_result(
729        True, "Un-Pausing containers via docker-compose", result, debug_ret
730    )
731
732
733def start(path, service_names=None):
734    """
735    Start containers in the docker-compose file, service_names is a python
736    list, if omitted start all containers
737
738    path
739        Path where the docker-compose file is stored on the server
740    service_names
741        If specified will start only the specified services
742
743    CLI Example:
744
745    .. code-block:: bash
746
747        salt myminion dockercompose.start /path/where/docker-compose/stored
748        salt myminion dockercompose.start /path/where/docker-compose/stored '[janus]'
749    """
750
751    project = __load_project(path)
752    debug_ret = {}
753    result = {}
754    if isinstance(project, dict):
755        return project
756    else:
757        try:
758            project.start(service_names)
759            if debug:
760                for container in project.containers():
761                    if (
762                        service_names is None
763                        or container.get("Name")[1:] in service_names
764                    ):
765                        container.inspect_if_not_inspected()
766                        debug_ret[container.get("Name")] = container.inspect()
767                        result[container.get("Name")] = "started"
768        except Exception as inst:  # pylint: disable=broad-except
769            return __handle_except(inst)
770    return __standardize_result(
771        True, "Starting containers via docker-compose", result, debug_ret
772    )
773
774
775def kill(path, service_names=None):
776    """
777    Kill containers in the docker-compose file, service_names is a python
778    list, if omitted kill all containers
779
780    path
781        Path where the docker-compose file is stored on the server
782    service_names
783        If specified will kill only the specified services
784
785    CLI Example:
786
787    .. code-block:: bash
788
789        salt myminion dockercompose.kill /path/where/docker-compose/stored
790        salt myminion dockercompose.kill /path/where/docker-compose/stored '[janus]'
791    """
792
793    project = __load_project(path)
794    debug_ret = {}
795    result = {}
796    if isinstance(project, dict):
797        return project
798    else:
799        try:
800            project.kill(service_names)
801            if debug:
802                for container in project.containers(stopped=True):
803                    if (
804                        service_names is None
805                        or container.get("Name")[1:] in service_names
806                    ):
807                        container.inspect_if_not_inspected()
808                        debug_ret[container.get("Name")] = container.inspect()
809                        result[container.get("Name")] = "killed"
810        except Exception as inst:  # pylint: disable=broad-except
811            return __handle_except(inst)
812    return __standardize_result(
813        True, "Killing containers via docker-compose", result, debug_ret
814    )
815
816
817def rm(path, service_names=None):
818    """
819    Remove stopped containers in the docker-compose file, service_names is a python
820    list, if omitted remove all stopped containers
821
822    path
823        Path where the docker-compose file is stored on the server
824    service_names
825        If specified will remove only the specified stopped services
826
827    CLI Example:
828
829    .. code-block:: bash
830
831        salt myminion dockercompose.rm /path/where/docker-compose/stored
832        salt myminion dockercompose.rm /path/where/docker-compose/stored '[janus]'
833    """
834
835    project = __load_project(path)
836    if isinstance(project, dict):
837        return project
838    else:
839        try:
840            project.remove_stopped(service_names)
841        except Exception as inst:  # pylint: disable=broad-except
842            return __handle_except(inst)
843    return __standardize_result(
844        True, "Removing stopped containers via docker-compose", None, None
845    )
846
847
848def ps(path):
849    """
850    List all running containers and report some information about them
851
852    path
853        Path where the docker-compose file is stored on the server
854
855    CLI Example:
856
857    .. code-block:: bash
858
859        salt myminion dockercompose.ps /path/where/docker-compose/stored
860    """
861
862    project = __load_project(path)
863    result = {}
864    if isinstance(project, dict):
865        return project
866    else:
867        if USE_FILTERCLASS:
868            containers = sorted(
869                project.containers(None, stopped=True)
870                + project.containers(None, OneOffFilter.only),
871                key=attrgetter("name"),
872            )
873        else:
874            containers = sorted(
875                project.containers(None, stopped=True)
876                + project.containers(None, one_off=True),
877                key=attrgetter("name"),
878            )
879        for container in containers:
880            command = container.human_readable_command
881            if len(command) > 30:
882                command = "{} ...".format(command[:26])
883            result[container.name] = {
884                "id": container.id,
885                "name": container.name,
886                "command": command,
887                "state": container.human_readable_state,
888                "ports": container.human_readable_ports,
889            }
890    return __standardize_result(True, "Listing docker-compose containers", result, None)
891
892
893def up(path, service_names=None):
894    """
895    Create and start containers defined in the docker-compose.yml file
896    located in path, service_names is a python list, if omitted create and
897    start all containers
898
899    path
900        Path where the docker-compose file is stored on the server
901    service_names
902        If specified will create and start only the specified services
903
904    CLI Example:
905
906    .. code-block:: bash
907
908        salt myminion dockercompose.up /path/where/docker-compose/stored
909        salt myminion dockercompose.up /path/where/docker-compose/stored '[janus]'
910    """
911
912    debug_ret = {}
913    project = __load_project(path)
914    if isinstance(project, dict):
915        return project
916    else:
917        try:
918            result = _get_convergence_plans(project, service_names)
919            ret = project.up(service_names)
920            if debug:
921                for container in ret:
922                    if (
923                        service_names is None
924                        or container.get("Name")[1:] in service_names
925                    ):
926                        container.inspect_if_not_inspected()
927                        debug_ret[container.get("Name")] = container.inspect()
928        except Exception as inst:  # pylint: disable=broad-except
929            return __handle_except(inst)
930    return __standardize_result(
931        True, "Adding containers via docker-compose", result, debug_ret
932    )
933
934
935def service_create(path, service_name, definition):
936    """
937    Create the definition of a docker-compose service
938    This fails when the service already exists
939    This does not pull or up the service
940    This wil re-write your yaml file. Comments will be lost. Indentation is set to 2 spaces
941
942    path
943        Path where the docker-compose file is stored on the server
944    service_name
945        Name of the service to create
946    definition
947        Service definition as yaml or json string
948
949    CLI Example:
950
951    .. code-block:: bash
952
953        salt myminion dockercompose.service_create /path/where/docker-compose/stored service_name definition
954    """
955    compose_result, loaded_definition, err = __load_compose_definitions(
956        path, definition
957    )
958    if err:
959        return err
960    services = compose_result["compose_content"]["services"]
961    if service_name in services:
962        msg = "Service {} already exists".format(service_name)
963        return __standardize_result(False, msg, None, None)
964    services[service_name] = loaded_definition
965    return __dump_compose_file(
966        path,
967        compose_result,
968        "Service {} created".format(service_name),
969        already_existed=True,
970    )
971
972
973def service_upsert(path, service_name, definition):
974    """
975    Create or update the definition of a docker-compose service
976    This does not pull or up the service
977    This wil re-write your yaml file. Comments will be lost. Indentation is set to 2 spaces
978
979    path
980        Path where the docker-compose file is stored on the server
981    service_name
982        Name of the service to create
983    definition
984        Service definition as yaml or json string
985
986    CLI Example:
987
988    .. code-block:: bash
989
990        salt myminion dockercompose.service_upsert /path/where/docker-compose/stored service_name definition
991    """
992    compose_result, loaded_definition, err = __load_compose_definitions(
993        path, definition
994    )
995    if err:
996        return err
997    services = compose_result["compose_content"]["services"]
998    if service_name in services:
999        msg = "Service {} already exists".format(service_name)
1000        return __standardize_result(False, msg, None, None)
1001    services[service_name] = loaded_definition
1002    return __dump_compose_file(
1003        path,
1004        compose_result,
1005        "Service definition for {} is set".format(service_name),
1006        already_existed=True,
1007    )
1008
1009
1010def service_remove(path, service_name):
1011    """
1012    Remove the definition of a docker-compose service
1013    This does not rm the container
1014    This wil re-write your yaml file. Comments will be lost. Indentation is set to 2 spaces
1015
1016    path
1017        Path where the docker-compose file is stored on the server
1018    service_name
1019        Name of the service to remove
1020
1021    CLI Example:
1022
1023    .. code-block:: bash
1024
1025        salt myminion dockercompose.service_remove /path/where/docker-compose/stored service_name
1026    """
1027    compose_result, err = __load_docker_compose(path)
1028    if err:
1029        return err
1030    services = compose_result["compose_content"]["services"]
1031    if service_name not in services:
1032        return __standardize_result(
1033            False, "Service {} did not exists".format(service_name), None, None
1034        )
1035    del services[service_name]
1036    return __dump_compose_file(
1037        path,
1038        compose_result,
1039        "Service {} is removed from {}".format(service_name, path),
1040        already_existed=True,
1041    )
1042
1043
1044def service_set_tag(path, service_name, tag):
1045    """
1046    Change the tag of a docker-compose service
1047    This does not pull or up the service
1048    This wil re-write your yaml file. Comments will be lost. Indentation is set to 2 spaces
1049
1050    path
1051        Path where the docker-compose file is stored on the server
1052    service_name
1053        Name of the service to remove
1054    tag
1055        Name of the tag (often used as version) that the service image should have
1056
1057    CLI Example:
1058
1059    .. code-block:: bash
1060
1061        salt myminion dockercompose.service_create /path/where/docker-compose/stored service_name tag
1062    """
1063    compose_result, err = __load_docker_compose(path)
1064    if err:
1065        return err
1066    services = compose_result["compose_content"]["services"]
1067    if service_name not in services:
1068        return __standardize_result(
1069            False, "Service {} did not exists".format(service_name), None, None
1070        )
1071    if "image" not in services[service_name]:
1072        return __standardize_result(
1073            False,
1074            'Service {} did not contain the variable "image"'.format(service_name),
1075            None,
1076            None,
1077        )
1078    image = services[service_name]["image"].split(":")[0]
1079    services[service_name]["image"] = "{}:{}".format(image, tag)
1080    return __dump_compose_file(
1081        path,
1082        compose_result,
1083        'Service {} is set to tag "{}"'.format(service_name, tag),
1084        already_existed=True,
1085    )
1086